Простой учебник для сервис-воркеров

JavaScript

часто спрашивают в интервьюservice workerсодержания, но полной статьи в интернете нет

Введение

W3Cорганизация уже в2014год5предложено в месяцService WorkerтакойHTML5 API, который в основном используется для постоянного автономного кэширования.
в браузереjsВ одном основном потоке одновременно может выполняться только одно действие. Если часть кода требует слишком много времени, она всегда будет занимать основной поток браузера, что приведет к снижению производительности. Исходя из этого вопроса,W3Cпридумалweb Worker, передать задачи, которые занимают слишком много времениweb worker, то черезpost MessageСообщите основному потоку, что основной поток проходитonMessageполучил ответ.
ноweb WorkerЭто временно, и результат каждой операции не может сохраняться долго, в следующий раз, когда будет сложная операция, ее нужно пересчитывать заново. Чтобы решить эту проблему,Service Worker, относительноweb workerДобавлена ​​возможность автономного кэширования.
Service WorkerВ основном имеют следующие особенности и функции:

  1. Автономный кеш
  2. сообщение
  3. запрос на перехват

как использоватьService Worker

регистр

Создаватьhtml, присоединяйтесь к регистрации внизуservice Worker

 if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js', {scope: '/'})
        .then(() => {
          console.log('Service Worker registration successful')
        })
        .catch((err) => {
          console.log('Service worker registration failed')
        })
      })
    }

Этот кодекс сначала судитservice WorkerПоддерживается ли он, то звоните егоregisterметод.
здесьscopeТо, что написано на официальном вендане, это сделатьService WorkerСмущает подкаталог контролируемого контента, например мой каталог такой

потомscopeнаписано как./config,Такservice-workerбудет только блокироватьconfigв каталогеfetchсобытия, но упомянутые нижеcache.addAllвсе еще кэшируется/последующийindex.htmlСодержание. также смservice workerОн не обслуживает ни одной страницы, поэтому необходимо обратить внимание наservice workerглобальные переменные, определенные в .
service workerв основном на основеpromiseОперация, когда регистрация завершена, генерируются обратные вызовы успеха и отказа, и результат можно увидеть в конце

Установить

зарегистрированservice Workerпосле,service workerбудет установлен, запускаяinstallсобытие, вinstallНекоторые ресурсы могут кэшироваться в событии следующим образом:

// 监听service worker的install事件
this.addEventListener('install', (event) => {
    // 如果监听到了service worker已经安装成功的话
    // 就会调用event.waitUtil回调函数
    event
        .waitUntil(
        // 安装成功后调用CacheStorage缓存,使用之前先通过caches.open()
        // 打开对应的缓存空间
            caches.open('my-test-cache-v1')
            .then((cache) => {
                // 通过cache缓存对象的addAll方法添加
                return cache.addAll([
                    '/',
                    '/index.html'
                ])
            })
        )
})

Во-первых, следитьinstallсобытие, называемоеwaitUntil, этот метод в основном используется для продления жизни события, оно должно пройти вPromise, необходимо дождаться внутреннего входящегоPromiseсталиresolve. В основном здесь, чтобы продлитьservice workerизinstallingцикл, который достигается после завершения кэширования ресурсов.installedЖизненный цикл.
Доступ к конкретному содержимому кэша можно получить черезApplicationизCache Storageсмотреть

Приведенный выше код уже может добиться эффекта кэширования ресурсов, но не использует кэшированные ресурсы, напишите следующееService WorkerИспользовать кешированный код

запрос на перехват

Service WorkerЕсть запросы на функции перехватывают, отправьте страницуHTTPпри запросе,service workerв состоянии пройтиfetchСобытие перехватывает запрос и дает свой ответ, поэтому для безопасности нужно использоватьhttps, чтобы написать конкретное содержимое ниже:

this.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
        .then((response) => {
        // 如果 service worker有自己的放回,就直接返回,减少一次http请求
            if (response) {
                return response;
            }
            // 如果service worker没有返回,那就直接请求真实远程服务
            var request = event.request.clone();
            return fetch(request)
                .then((res) => {
                    // 请求失败,直接返回失败的结果
                    if (!res || res.status !== 200) {
                        return res;
                    }
                    
                    // 请求成功的话,将请求缓存起来
                    var responseClone = res.clone;
                    caches
                        .open('my-test-cache-v1')
                        .then((cache) => {
                            cache.put(event.request, respondClone)
                        })
                    return res;
                })
        })
    )
})

слушай сначалаfetchмероприятие, затем позвонитеevent.respondWith, использование этой функции иwaitUntilАналогично, когда входящийPromise resolvedПосле этого соответствующийresponseвернуться в браузер. а такжеcacheСравните данные, чтобы увидеть, есть ли кешированный контент, если да, используйте кешированный контент, если нет, запросите удаленную службу. Эта часть кеша запросов требует внимания, потому чтоService WorkerВсе запросы будут перехвачены, поэтому вам нужно обратить внимание, чтобы определить, какой контент нужно кэшировать, а какой нет, напримерajaxНет необходимости в кэшировании. когда мы вернемсяindex.html, ты можешь видетьindex.htmlНепосредственно изservice workerполучено в

service workerвозобновить

Когда мы меняем стратегию кэширования,service workerКак его обновить, в основном существуют следующие стратегии

  1. service workerдокументURLОбновить
  2. service workerсодержимое файла изменено
  3. Пользователь неактивен24Часы могут быть автоматически обновлены

Изменятьservice workerизURL

Посмотрите, осуществимо ли это таким образом, при первом посещении, вsw1.jsБудуindex.htmlКэш, на этот раз мы меняемindex.htmlСодержимое отображаемой страницы не изменилось, его необходимо изменить в данный момент.service worker, мы можем выбратьsw1.jsизменить наsw2.js, что означает перерегистрациюservice worker, чтобы кэшировать новые файлы следующим образом:

мы изменилисьservice workerдокументurl
Но нашел в браузере,index.htmlне изменилось, потому что пользователь посетил сайт из-заsw1.jsРоль извлечения из кешаindex.htmlСсылка по-прежнемуv1, а не то, на что мы ссылались после обновленияv2, то кто-то сказал, что он напрямую не кешируетсяhtmlЕсли контента будет недостаточно, то приложение потеряет функцию автономного использования.

Изменятьservice workerсодержание документа

еслиsw.jsСодержимое обновляется, при посещении страницы сайта браузер получает новый файл и сравнивает его побайтно./sw.jsЕсли файл отличается, он подумает, что есть обновление, поэтому установит новый файл и запуститinstallфайл, а старый, который уже активен в это времяservice workerеще на ходу, новыйservice workerПосле завершения установки вы войдетеwaitingусловие. пока все открытые страницы не будут закрыты, старыйservice workerавтообновление, новоеservice workerЭто вступит в силу на следующей странице, которую вы снова откроете.
Например, вsw.jsДобавьте номер версии вservice workerОбновить

var version = '0.0.1';
// 跳过等待,直接进入active
this.addEventListener('install', funciton (event) {
    event.waitUntil(self.skipWaiting())
})
this.addEventListener('activate', function (event) {
    event.waitUntil(
        Promise.all([
            // 更新客户端
            self.clients.claim(),
            // 清理旧版本
            caches.keys().then((cacheList) => {
                return Promise.all(
                    cacheList.map((cacheName) => {
                        if (cacheName !== 'my-test-cache-v1') {
                            return caches.delete(cacheName)
                        }
                    })
                )
            })
        ])
    )
})

Сначала мы звонимself.skipWaitingпросто пропуститьinstallingэтап, взять на себя старыйservice worker, если вы не выполните этот шаг, вы найдете

Изначально на странице былservice worker, вам нужно закрыть текущую страницу после новойservice workerчтобы вступить в силу, так что звоните сюдаskipWaitingперепрыгниinstalling, непосредственно взять на себя старыйservice worker, мониторactivate, чтобы разобраться со старыми кэшами.
Но есть проблема с этим, если есть следующий сценарий:

  1. страницаindex.htmlуже установленоold_sw
  2. Пользователь открывает эту страницу и все сетевые запросы проходят черезold_swпроцесс, страница загружается
  3. потому чтоservice workerОн имеет характеристики асинхронной установки.Как правило, когда браузер бездействует, он выполняет предложение.navigator.serviceWorker.register. В это время браузер обнаружилnew_sw, поэтому установка заставила его ждать
  4. Однако из-заnew_swсуществуетinstallэтап имеетself.skipWaiting(), поэтому браузер принудительно завершает работуold_sw,Позволятьnew_swАктивируйте и управляйте страницей прямо сейчас
  5. если пользовательindex.htmlЕсли есть сетевой запрос на последующие операции, он будетnew_swиметь дело с Очевидно, та же самая страница, первая половина созданаold_swконтроль, в то время как вторая половина контролируетсяnew_swконтроль. Это может привести к непоследовательному поведению между ними и неизвестной ошибке.

Ручное обновлениеservice worker

Как и выше, используйте номер версии для обновления

var version = '1.0.1'
navigator.serviceWorker.register('/sw.js')
    .then((reg) => {
        if(localStorage.getItem('sw_version') !== version) {
            reg.update()
                .then(() => {
                    localStorage.setItem('sw_version', version)
                })
        }
    })

автоматическое обновление

Service WorkerВ дополнение к тому, что обновление инициируется браузером, применяется специальная стратегия кэширования: если файл был24часы не обновляются, когдаUpdateПринудительное обновление при срабатывании. Это означает худший случайService WorkerБудет обновляться раз в сутки.

какhtmlкэшировать

упомянутый в приведенном выше примере дляservice workerДля обновлений ресурсы могут кэшироваться в базе данных кэша. Однако в приведенном выше примере есть проблема: хотя мы говорим, что ресурсы кэшируются, отображение страницы — это измененное содержимое, когда требуется второе посещение. потому чтоhtmlВ отличие от других статических ресурсов, которые имеют дайджест файла, поэтому необходимоhtmlФайлы обрабатываются особым образом.
Поскольку старые ресурсы все еще извлекаются из кеша в первый раз, у меня есть несколько идей для этой проблемы:htmlФайл формата специально обрабатывается, при наличии сети новые ресурсы будут браться с сервера.htmlСтратегия кэширования файлов обычно использует согласованное кэширование, r если нет сети, используйте кэшированныйhtmlТаким образом, страница, которую вы посещаете каждый раз, обновляется, а также достигается эффект автономного использования. Конкретная идея состоит в том, чтобыfetchЭто чистое суждение?htmlФормат:

this.addEventListener('fetch', function (event) {
  event.respondWith(
      caches.match(event.request).then(function (response) {
            var isHtml = /\.html/.test(reponse ? response.url : '');
            var onLine = navigator.onLine;
            
              // 如果没网,就全部使用缓存内容
            if (!onLine) {
                return response;
            }
            
             // 如果有网并且不是html,而且response存在,就返回response
            if (!isHtml && response) {
                return response;
            }
            // ...
      })

Это гарантирует, что каждый раз, когда есть сетьhtmlвсе в курсе.

сообщение

У push-уведомлений очень широкие сценарии применения:

  • Новые продукты на полках, push-уведомления пользователям, нажмите, чтобы перейти на страницу сведений о продукте
  • Пользователь давно не заходил на сайт, а пуш информирует об обновлении сайта за это время Используя push-уведомления, мы можем сделать наше приложение похожим наNative AppКак улучшить пользовательский опыт

Получить авторизацию

Прежде чем подписаться на сообщение, вам необходимо получить разрешение пользователя на использование push-уведомлений.Конкретный код выглядит следующим образом:

navigator.serviceWorker.register('./sw.js')
    .then((reg) => {
        res.pushManager.getSubscription().then((subscription) => {
            // 如果用户没有订阅
            if (!subscription) {
                subscribeUser(reg)
            } else {
                console.log('you have subscribed our notification')
            }
        })
    })

Если вы уже подписались, следующее всплывающее окно больше не появится, если подписки нет, оно будет называтьсяsubscribeUser

Подключиться к пуш-сервису

В качестве источника сообщения сервер поручает службе push отправлять сообщения в браузеры, которые подписаны на сообщения, поэтому серверу необходимо сохранить уникальный идентификатор браузера.
используется здесьweb-pushСгенерируйте открытый ключ и закрытый ключ, и открытый ключ будет отправлен в браузер черезservice workerСоздайте уникальный идентификатор и отправьте его на сервер. Сервер использует этот уникальный идентификатор для отправки сообщений в браузер.

function subscribeUser(registration) {
  const applicationServerPublicKey = 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts';
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  // 订阅
  registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey,
    })
    .then((subscription) => {
      $.post({
        type: 'post',
        url: 'http://localhost:3000/add',
        data: {
          subscription: JSON.stringify(subscription)
        },
        success: (res) => {
          console.log(res)
        }
      })
    })
    .catch((err) => {
      console.log('Failed to subscribe the user: ', err)
    })
}

applicationServerPublicKeyтолько что использовалweb-pushсгенерированный открытый ключ, затем вызовитеpushManager.subscribeСгенерируйте уникальный идентификатор браузера и передайте его серверной части. этоsubscriptionСодержимое длинное в следующем формате

{"endpoint":"https://fcm.googleapis.com/fcm/send/eAWELgsiTME:APA91bGZ4UwYtr26b0JE8K4sTNNFN8Z8GJ07QgDZHJP9aAqeMsjqiJJaaXd4Ype62vm5v4EjRnD0MuSD5ouBLYy6aT6nU5tWFpp5DjSjPmt_bh-h2Nm5pLo9-xY8H83Q8MHTynY7onKk","expirationTime":null,"keys":{"p256dh":"BLpOkRk1lLRXG8kMP3Yc4D6SUmz3aagln-ysP0lslwJsPA7SQhkmeytSFRCLZKBToBwMe3qRaUAMcJ0R3B1ZND4","auth":"pWaweBbyQqi5lNDR0Rqqew"}}

После получения этой информации сервер может использовать этот идентификатор для отправки сообщений в указанный браузер в будущем.

отправка на стороне сервера

Здесь тоже нужна помощьweb-pushОтправьте сообщение, потому что для отправки сообщения требуется помощь службы Google.FCMуслуга, которая недоступна из-за сети собственной сетиFCMСлужить. Поэтому я не могу использовать браузер Google здесь.Проверив много информации, я обнаружил, чтоfirefoxТакого же эффекта можно добиться, поэтому рекомендуется использоватьfirefoxДля реализации не будет проблем со стеной.

const webpush = require('web-push');

// push的数据
const payload = {
  title: '一篇新的文章',
  body: '点开看看吧',
  icon: 'xx', // 图片链接
  data: {
    url: 'www.baidu.com'
  }
}

const vapidKeys = {
  publicKey: 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts',
  privateKey: 'm5rk4Cann9l5pp7TiLPuNmL2Ho_zmIvgM3wz07EZSSs'
}

const pushSubScription = {
  "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABeaki7zwcdJ8r-2PZhwjyeCkHN3GaFAI4NQP8awz3e5svu0xDP6Peanq7iNTRd6S8weseu8JGpJDmLF1V2CcSZRExeWfLt0p5ksuNvCQmYnC4Bwy6wBzUGt-yQRAQMdq9_RKsEnadYfWAQt6LHENfaUr0gKcJJcj1Jb6vGfel-eqjEmjE",
  "keys": {
    "auth": "QyYLx2m29E-3a5kXzqdIDg",
    "p256dh": "BEX1qgwC7MIRw-Vck7wsQPw5M8CIhkQ6thqs5ZwmPkXYy1zF-7sXvKE9hxeZtlm1rHd5lpvpjJf3q26rJje8zUc"
  }
}
webpush
  .sendNotification(pushSubScription, JSON.stringify(payload), {
    vapidDetails: {
      subject: 'mailto:18223306087@163.com',
      ...vapidKeys,
    },
  })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err)
  })

Во-первых, клиент должен сначала получить предварительно переданный намsubscriptionи созданные ранееpublickKeyа такжеprivateKey, а затем позвонитеwebpush.sendNotificationПроактивно push-сообщения

service Workerмониторpush

После того, как сервер передаст информацию клиенту, нам нужноpushСобытия отслеживаются, а затем отображается эффект

self.addEventListener('push', function (event) {
    console.log('push');
    // var notificationData = event.data.json();
    // var title = notificationData.title;
    const title = 'push works';
    const options = {
        body: 'push is working',
        icon: 'resource/logo.png',
        badge: 'resource/logo.png'
    }
    event.waitUntil(self.registration.showNotification(title, options));
})
this.addEventListener('notificationclick', function(event) {
    event.notification.close();
    event.waitUntil(
        clients.openWindow('https://baidu.com')
    )
})

Здесь у нас естьpushОтслеживайте, а затем получайте содержимое сообщения, активно отправленного сервером. Здесь просто создайте содержимое для простоты.

Затем вы можете контролироватьnotificationclickСобытие, это в основном обратный вызов, созданный при нажатии на вышеуказанный контент. Например, в приведенном выше примере, если вы нажмете на это уведомление, вы перейдете на страницу Baidu.
Таким образом, вы можете увидеть эффект.На самом деле, многие иностранные веб-сайты использовали эту технологию, но из-за стены эту технологию нельзя продвигать в Китае.

страница связи

service workerне может работать напрямуюDOM, Но можетpostMessageМетоды иWebСтраница общается и позволяет странице работатьDOM.

использоватьpostMessageпослать запрос

service workerотправить данные: вsw.jsЧтобы отправить сообщение на страницу, используйтеclient.postMessage, пример кода выглядит следующим образом:

this.clients.matchAll()
    .then(function (clients){
        if (clients && clients.length) {
            clients.forEach((client) => {
                // 发送数据
                client.postMessage('sw update')
            })
        }
    })

Данные отправки страницы: используются на главной страницеnavigator.serviceWorker.controller.postMessage()отправить данные

if (navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.postMessage('home update')
}

Получить данные

существуетservice workerПримите информацию с главной страницы в следующем примере:

self.addEventListener('message', function (event) {
    console.log(event.data); // home update
});

Принять на главной страницеservice workerПримером отправленной информации выглядит следующим образом:

navigator.serviceWorker.addEventListener('message', function (event) {
    console.log(event.data)
});

использованная литература

Документация по PWA
understanding-service-worker-scope
Внимательно относитесь к обновлениям Service Worker
Добавьте push-уведомления в веб-приложения