Service Worker 是一段脚本,与 web Worker 一样,也是在后台运行。作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与 Web 交互行为。Native App 可以做到离线使用、消息推送、后台自动更新,Service Worker 的出现是正是为了使得 Web App 也可以具有类似的能力。
关于手工编写 Service Worker 脚本, 可以阅读上一篇文章 PWA 入门: 写个非常简单的 PWA 页面 或者查看 Google 给出的例子 。这篇文章主要介绍 Service Worker 生命周期, 缓存策略以及如何快速生成配置。
Service Worker 的生命周期
Service Worker 脚本通过 navigator.serviceWorker.register 方法注册到页面, 之后可以脱离页面在浏览器后台运行:
- if (navigator.serviceWorker) {
- navigator.serviceWorker.register('service-worker.js').then(function(registration) {
- console.log('service worker 注册成功');
- }).
- catch(function(err) {
- console.log('servcie worker 注册失败');
- });
- }
处于安全原因, Service Worker 脚本的作用范围不能超出脚本文件所在的路径。比如地址是 "/sw-test/sw.js" 的脚本只能控制 "/sw-test/" 下的页面。
Service Worker 从注册开始需要先 install, 如果 install 成功, 接下来需要 activate, 然后才能接管页面。但是如果页面被先前的 Service Worker 控制着, 那么它会停留在 installed(waiting) 这个阶段等到页面重新打开才会接管页面, 或者可以通过调用 self.skipWaiting() 方法跳过等待。所以一个 Service Worker 脚本的生命周期有这样一些阶段 (从左往右):
- parsed: 注册完成, 脚本解析成功, 尚未安装
- installing: 对应 Service Worker 脚本 install 事件执行, 如果事件里有 event.waitUntil() 则会等待传入的 Promise 完成才会成功
- installed(waiting): 页面被旧的 Service Worker 脚本控制, 所以当前的脚本尚未激活。可以通过 self.skipWaiting() 激活新的 Service Worker
- activating: 对应 Service Worker 脚本 activate 事件执行, 如果事件里有 event.waitUntil() 则会等待这个 Promise 完成才会成功。这时可以调用 Clients.claim() 接管页面
- activated: 激活成功, 可以处理 fetch, message 等事件
- redundant: 安装失败, 或者激活失败, 或者被新的 Service Worker 替代掉
Service Worker 脚本最常用的功能是截获请求和缓存资源文件, 这些行为可以绑定在下面这些事件上:
- install 事件中, 抓取资源进行缓存
- activate 事件中, 遍历缓存, 清除过期的资源
- fetch 事件中, 拦截请求, 查询缓存或者网络, 返回请求的资源
更多细节可以参考文章: Service Worker 生命周期
缓存策略
处在 activated 状态的 Service Worker 可以拦截作用范围下的页面的网络请求, 由 Service Worker 监听 fetch 事件来决定请求如何响应。Service Worker 可以访问 Cache API, Fetch API 等接口, 获取数据和完成响应。
- self.addEventListener('fetch', function(event) {
- event.respondWith(
- caches.match(event.request)
- .then(function(response) {
- // Cache hit - return response
- if (response) {
- return response;
- }
- return fetch(event.request);
- }
- )
- );
- });
event.waitUntil() 接收一个 Promise, 等到 Promise 完成时, 事件才最终完成。
有一些常用的资源缓存的策略, 比如在 sw-toolbox 当中定义的有几种:
- 网络优先: 从网络获取, 失败或者超时再尝试从缓存读取
- 缓存优先: 从缓存获取, 缓存插叙不到再尝试从网络抓取
- 最快: 同时查询缓存和网络, 返回最先拿到的
- 仅限网络: 仅从网络获取
- 仅限缓存: 仅从缓存获取
借助 Fetch API 和 Cache API 可以编写出复杂的策略用来区分不同类型或者页面的资源的处理方式。详细的解释可以看比如 Service workers explained 。
官方工具
除了前面提到的手工编写 Service Worker 脚本, Google 提供了 sw-toolbox 和 sw-precache 两个工具方便快速生成 service-worker.js 文件:
- sw-precache 可以用来生成配置使 PWA 在安装时进行静态资源的缓存
- sw-toolbox 提供了动态缓存使用的通用策略, 这些动态的资源不合适用 sw-precache 预先缓存。同时它提供了一套 类似 Express.js 路由的语法 ,
用于编写策略
sw-precache 可以通过 Node.js 直接调用来生成 sw.js 文件, 比如配合 Gulp 使用:
- gulp.task('generate-service-worker', function(callback) {
- var path = require('path');
- var swPrecache = require('sw-precache');
- var rootDir = 'app';
- swPrecache.write(`${rootDir}/service-worker.js`, {
- staticFileGlobs: [rootDir + '/**/*.{js,html,CSS,png,jpg,gif,svg,eot,ttf,woff}'],
- stripPrefix: rootDir
- }, callback);
- });
其中也可以配置 runtimeConfig 进而调用 sw-toolbox 生成动态缓存的配置, 比如:
- {
- "staticFileGlobs": [
- "app/css/**.css",
- "app/**.html",
- "app/images/**.*",
- "app/js/**.js"
- ],
- "stripPrefix": "app/",
- "runtimeCaching": [{
- "urlPattern": "/express/style/path/(.*)",
- "handler": "networkFirst"
- }]
- }
另外也推荐用 sw-precache 的 Webpack 插件 来生成 sw.js。
- var path = require('path');
- var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
- module.exports = {
- context: __dirname,
- entry: {
- main: path.resolve(__dirname, 'src/index'),
- },
- output: {
- path: path.resolve(__dirname, 'src/bundles/'),
- filename: '[name]-[hash].js',
- },
- plugins: [
- new SWPrecacheWebpackPlugin(
- {
- cacheId: 'my-project-name',
- filename: 'my-service-worker.js',
- maximumFileSizeToCacheInBytes: 4194304,
- minify: true,
- runtimeCaching: [{
- handler: 'cacheFirst',
- urlPattern: /[.]mp3$/,
- }],
- }
- )
- ]
- }
可以参考 CodeLabs: Using Cache 以及 API 文档 来了解细节。
结尾
除了拦截请求之外, Service Worker 还能用来做后台通知, Mock 请求, 离线统计等等工作。可以浏览 GoogleChrome/samples 了解更多的功能和写法。
图片来源
- 头图 developers.google.com/web/progres…
- Service Worker 生命周期 bitsofco.de/the-service…
- Service Worker 拦截请求 www.w3ctrain.com/2016/09/17/…