面条实验室

使用 Workbox 快速让你的网站变身 PWA

PWA 在过去的一年可以说十分火爆,各种前端技术会议都有相关课题,我最早知道 PWA 也是在技术会议上。目前 Google 系的浏览器基本已经支持,Apple 系的浏览器也已经在开发者版本中支持(浏览器支持情况1)。作为一个前端开发者,是时候掌握这一门技能了。

PWA 主要有 Service Worker、Web App Manifest、Web Push 等特性。这次我们先实现 Service Worker 和 Web App Manifest,GoogleChrome 团队提供了一个 Workbox ,对缓存控制进行封装,是十分有用的几个工具。

Web App Manifest

Web App Manifest 的实现十分简单,我们可以借助 App Manifest Generator2 快速生成,放置对应资源到合适的目录,然后在页面 head 中增加<link rel="manifest" href="/manifest.json">即可。

Service Worker

Service Worker 可以帮你预加载资源,拦截或修改网络请求。我们这次主要实现静态资源预加载,网络请求缓存,图片自动支持 webp。前端代码构建完成以后,我们可以自定义一些规则,整理出需要预加载的资源,进行缓存。我们使用 Workbox 命令行工具,扫描出需要预加载的目录,生成 Service Worker 文件。

Workbox 配置文件

主要定义静态文件存放的目录,处理 CDN 地址规则。

// 需要先安装orkbox 命令行工具 npm i workbox-cli -g 
// workbox-cli-config.js 
const fs = require('fs') 
 
// 业务相关,每个项目需要自己修改 
const buildStats = require('./build/build-stats.json') 
const buildID = fs.readFileSync('./build/BUILD_ID').toString() 
const cdnServer = 'https://o88aw8tfp.qnssl.com' 
 
module.exports = { 
  globDirectory: './build/', // 静态资源存放目录 
  globPatterns: [ 
    'app.js', // 自定义扫描规则,与业务相关 
    'bundles/pages/*.js', 
    'chunks/*.js' 
  ], 
  swSrc: 'static/workbox.js', // Service Worker 模板文件,相关业务文件写在这里 
  swDest: 'static/sw.js', // 最终生成的 Service Worker 文件,不要手动修改 
  modifyUrlPrefix: { // 静态资源 CDN 路径处理,与业务相关 
    'app.js': `${cdnServer}/_next/${buildStats['app.js'].hash}/app.js`, 
    'bundles/pages/': `${cdnServer}/_next/${buildID}/page/`, 
    'chunks/': `${cdnServer}/_next/${buildID}/webpack/chunks/` 
  } 
} 

Workbox 模板文件

实现图片自动转换为 webp 格式,缓存网络请求,以备离线时使用。

// static/workbox.js 
/* eslint-env worker */ 
/* global FetchEvent WorkboxSW */ 
 
var workboxCDN = 'https://cdn.jsdelivr.net/npm/workbox-sw@2.1.2/build/importScripts/workbox-sw.prod.v2.1.2.min.js' 
importScripts(workboxCDN) // 加载 Workbox 库 
 
;(function () { 
  var workbox = new WorkboxSW({ 
    clientsClaim: true, 
    skipWaiting: true 
  }) // 生成 Workbox 实例 
  var networkFirstStrategy = workbox.strategies.networkFirst({ 
    cacheExpiration: { 
      maxEntries: 3000, 
      maxAgeSeconds: 3600 * 24 * 7 
    } 
  }) // 创建一个网络请求优先的缓存仓库 
  var cacheFirstStrategy = workbox.strategies.cacheFirst({ 
    cacheExpiration: { 
      maxEntries: 3000, 
      maxAgeSeconds: 3600 * 24 * 7 
    } 
  }) // 创建一个缓存优先的缓存仓库 
 
  /** 
   * webp 处理 
   * 匹配非 webp 的 cdn 图片资源,缓存其 webp 格式 
   */ 
  workbox.router.registerRoute(function (args) { 
    var useWebp = false 
    var supportWebp = /image\/webp/i.test(args.event.request.headers.get('Accept')) // 通过 HTTP Accept 头信息判断浏览器是否支持 webp 
    var url = args.url.href.split('?')[0] 
 
    if (!supportWebp) { 
      return useWebp 
    } 
 
    // 判断哪些图片需要转为 webp 格式,业务相关 
    if (/qnssl\.com/i.test(url) && /(jpg|png)$/i.test(url)) { 
      useWebp = true 
    } 
 
    return useWebp // 觉得是否需要拦截路由,true 拦截,false 不拦截 
  }, function (args) { 
    // 拦截到的请求处理规则 
    var url = args.url.href.split('?')[0] 
 
    // 重新构造 fetch 请求 
    args.event = new FetchEvent(args.event.type, { 
      request: new Request(url + '?imageView2/2/q/85/format/webp'), // CDN 转换图片格式参数,各 CDN 不同 
      clientId: args.event.clientId, 
      isReload: args.event.isReload 
    }) 
 
    return cacheFirstStrategy.handle(args) // 将请求交给缓存仓库处理,如无缓存,会从网络加载 
  }) 
 
  workbox.router.registerRoute(/\/.*/i, networkFirstStrategy) // 所有同域下的请求进行缓存,以备离线使用,此处实现比较粗糙,具体项目需要细致化到各类路由 
 
  workbox.precache([workboxCDN]) // 缓存 Workbox 库文件 
  workbox.precache([]) // 占位符,Workbox 命令行工具会转换为具体的静态资源 
}()) 

生成 Service Worker 文件

在你的项目构建流程中增加 workbox inject:manifest 代码, 或者命令行手动执行 workbox inject:manifest。构建后就可以查看生成的文件。

// 生成的文件存放在 static/sw.js 
/* eslint-env worker */ 
/* global FetchEvent WorkboxSW */ 
 
var workboxCDN = 'https://cdn.jsdelivr.net/npm/workbox-sw@2.1.2/build/importScripts/workbox-sw.prod.v2.1.2.min.js' 
importScripts(workboxCDN) 
 
;(function () { 
  var workbox = new WorkboxSW({ 
    clientsClaim: true, 
    skipWaiting: true 
  }) 
  var networkFirstStrategy = workbox.strategies.networkFirst({ 
    cacheExpiration: { 
      maxEntries: 3000, 
      maxAgeSeconds: 3600 * 24 * 7 
    } 
  }) 
  var cacheFirstStrategy = workbox.strategies.cacheFirst({ 
    cacheExpiration: { 
      maxEntries: 3000, 
      maxAgeSeconds: 3600 * 24 * 7 
    } 
  }) 
 
  /** 
   * webp 处理 
   * 匹配非 webp 的 cdn 图片资源,缓存其 webp 格式 
   */ 
  workbox.router.registerRoute(function (args) { 
    var useWebp = false 
    var supportWebp = /image\/webp/i.test(args.event.request.headers.get('Accept')) 
    var url = args.url.href.split('?')[0] 
 
    if (!supportWebp) { 
      return useWebp 
    } 
 
    if (/qnssl\.com/i.test(url) && /(jpg|png)$/i.test(url)) { 
      useWebp = true 
    } 
 
    return useWebp 
  }, function (args) { 
    var url = args.url.href.split('?')[0] 
 
    // 重新构造 fetch 请求 
    args.event = new FetchEvent(args.event.type, { 
      request: new Request(url + '?imageView2/2/q/85/format/webp'), 
      clientId: args.event.clientId, 
      isReload: args.event.isReload 
    }) 
 
    return cacheFirstStrategy.handle(args) 
  }) 
 
  workbox.router.registerRoute(/\/.*/i, networkFirstStrategy) 
 
  workbox.precache([workboxCDN]) 
  workbox.precache([ 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/fc0a873a0fb8865c733eafd4361ee446/app.js", 
    "revision": "fc0a873a0fb8865c733eafd4361ee446" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/_document.js", 
    "revision": "35b6335d59f61b737f1b156c134cf2c2" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/_error.js", 
    "revision": "ebb56565bb86beef4465afad58568033" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/list.js", 
    "revision": "b48b7fbc258e95243ecacd6ba28c5415" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/post.js", 
    "revision": "2319d8d3db08749e2b2aa42cdad347e7" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/tag.js", 
    "revision": "58e377967c2f6212faffe819b2d642be" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/baguettebox_7753f32ed8b1b354d9d9a9f46540e36c.js", 
    "revision": "25b303719d46f152f12649f043dda480" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/components_Disqus_330b0a098ae588ad4dde7ddac1342b19.js", 
    "revision": "a23c97a37071b2128700eb97b569ff70" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/nprogress_cc31a9d7a86a6f32932208d2b28f4f1c.js", 
    "revision": "bb5f8920b823835f1828dc98a17ffd82" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/utils_inject_57190832e9ae540e620d616b9e53c208.js", 
    "revision": "f78bb021995310ff2cd09892e6d496aa" 
  }, 
  { 
    "url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/utils_lozad_e11a80f493f09965eee540dea545759d.js", 
    "revision": "2cab0d134cb64502de91469894863741" 
  } 
]) 
}()) 

可以看到,生成的 Service Worker 文件已经包含需要预加载的文件。

注册 Service Worker

function register () { 
  if (navigator && navigator.serviceWorker && navigator.serviceWorker.register) { 
    return navigator.serviceWorker.register(`/sw.js`, { // 注册 Service Worker,与作用域相关,不能乱放 
      scope: '/' 
    }) 
  } else { 
    return Promise.resolve() 
  } 
} 
 
function unregister () { 
  if (navigator && navigator.serviceWorker && navigator.serviceWorker.getRegistrations) { 
    return navigator.serviceWorker.getRegistrations().then(registrations => { 
      for (let registration of registrations) { 
        registration.unregister() 
      } 
    }) 
  } else { 
    return Promise.resolve() 
  } 
} 
 
window.addEventListener('load', function () { 
  if ('serviceWorker' in navigator) { 
    register().catch(unregister) 
  } 
}) 

测试

经过前面的流程,基本开发工作已经完成,现在可以在浏览器进行测试了,上线前一定记得测浏览器兼容。

几张成功的截图

  1. Web App Manifest 注册成功
    Web App Manifest 注册成功
  2. Service Worker 注册成功
    Service Worker 注册成功
  3. LightHouse PWA 测试工具满分 100 分
    LightHouse PWA 测试工具满分 100 分