最近在开发一个简单的个人记录网站,技术栈是使用 Vite + Vue3,由于使用的单台服务器,有时候服务器会被限制带宽,所以平时都会比较访问比较慢。所以想实现一个离线应用,而 PWA 应用则是目前最佳方案。
本文涉及知识点如下:
- PWA 的概念
- Service Worker使用
- 用构建工具搭建 PWA 应用
渐进式 Web 应用(Progressive Web App,PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体验就像一个特定平台的应用程序。 ——MDN 渐进式 Web 应用(PWA)
正如上文所描述一样 PWA 最终目的让你的 web 网站可以像 app 应用一样可以给到用户去离线体验,简单点说,就是没有网络,你也可以正常访问该网站的一些资源。
PWA从技术上分为三个部分:
- 主应用,就是平时我们开发网站所包含的内容,有:html,js,css等
- Web app manifests,主要为
manifest.json
,提供浏览器安装 PWA 所需的信息,例如应用程序名称和图标等 - Service Worker,主要为js文件,提供基本的离线缓存资源能力
manifest.json
manifest.json
描述web网站的信息(如名称,作者,图标和描述)的JSON文件,具体例子如下所示。
manifest.json
是需要在网站中html文件中 head中引用,如下:
1<link rel="manifest" href="/manifest.json" />
完整的manifest.json
示例:
1{
2 "name": "网站完整名称",
3 "short_name": "网站简称",
4 "start_url": ".",
5 "display": "standalone",
6 "background_color": "#fff",
7 "description": "网站描述",
8 "icons": [
9 {
10 "src": "images/touch/homescreen48.png",
11 "sizes": "48x48",
12 "type": "image/png"
13 },
14 {
15 "src": "images/touch/homescreen72.png",
16 "sizes": "72x72",
17 "type": "image/png"
18 },
19 {
20 "src": "images/touch/homescreen96.png",
21 "sizes": "96x96",
22 "type": "image/png"
23 },
24 {
25 "src": "images/touch/homescreen144.png",
26 "sizes": "144x144",
27 "type": "image/png"
28 },
29 {
30 "src": "images/touch/homescreen168.png",
31 "sizes": "168x168",
32 "type": "image/png"
33 },
34 {
35 "src": "images/touch/homescreen192.png",
36 "sizes": "192x192",
37 "type": "image/png"
38 }
39 ],
40}
了解基本的描述文件,下面我们将进入Service Worker
作为其中整个控制中心,我们将在下面进行着重了解。
是什么
我们先来看看官方定义:
Service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 文件的形式,控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。 —— MDN Service Worker
进行简单总结一下 Service Woker是什么:
- 是一个区别于主 JavaScript 线程,运行在其他单独线程,但是必须要注册到主 JavaScript 线程中
- 是用JavaScript编写的
- 可以拦截并修改访问和资源请求,从而实现资源缓存
出于安全考量,Service worker 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险,如果允许访问这些强大的 API,此类攻击将会变得很严重。
生命周期
Service Woker的生命周期如下:
- 注册,使用 ServiceWorkerContainer.register() 方法首次注册 service worker
- 下载,页面首次加载后会下载ServiceWorker或者过去 24 小时没有被下载会再次下载
- 安装,首次启用 service worker,页面会首先尝试安装,如果现有 service worker 已启用,新版本会在后台安装,但仍不会被激活——这个时序称为 worker in waiting。
- 激活,首次启用 service worker,安装结束后会直接激活,新版本的service worker会直到所有已加载的页面不再使用旧的 service worker 才会激活新的 service worker,但是可以通过ServiceWorkerGlobalScope.skipWaiting() 可以更快地进行激活。
Service Worker提供几个事件用来监听生命周期的变化,如下:
self.addEventListener("install")
该事件触发时的标准行为是准备 service worker 用于使用,例如使用内建的 storage API 来创建缓存,并且放置应用离线时所需资源。self.addEventListener("activate")
事件触发的时间点通常是清理旧缓存以及其他与你的 service worker 的先前版本相关的东西。self.addEventListener("fetch")
事件触发的时间点是每次获取 service worker 控制的资源时,都会触发 fetch 事件
这里的this
代表的是 Service Worker 本身对象。
常用API
了解完后,我们需要知道 Service Worker 有哪些常用的 API接口,或者当我们需要去实现一个 PWA 会用到哪些 API 接口,具体如下:
navigator.serviceWorker.register()
主 JavaScript 线程注册 Service Worker 方法Cache
与CacheStorage
用来控制缓存
尝鲜使用
第一步 写个 demo站点
我们肯定需要有一个站点,里面有 html/css/js文件,代码如下:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <link rel="manifest" href="./manifest.json" />
7 <title>Service Worker测试页面</title>
8 <link rel="stylesheet" href="./test.css">
9</head>
10<body>
11 <h1>测试 Service Worker</h1>
12 <script src="./test.js"></script>
13 <script>
14
15 </script>
16</body>
17</html>
第二步 注册 Service Worker
这一步有两个 事情:
- 写Service Worker的相关逻辑的js文件 (且叫
sw.js
) - 将
sw.js
注册到html文件中 具体代码如下:
1if ('serviceWorker' in window.navigator) {
2 const registerServiceWorker = async () => {
3 if ("serviceWorker" in navigator) {
4 try {
5 const registration = await navigator.serviceWorker.register("./sw.js", {
6 scope: "/",
7 });
8 if (registration.installing) {
9 console.log("正在安装 Service worker");
10 } else if (registration.waiting) {
11 console.log("已安装 Service worker installed");
12 } else if (registration.active) {
13 console.log("激活 Service worker");
14 }
15 } catch (error) {
16 console.error(`注册失败:${error}`);
17 }
18 }
19 };
20
21 registerServiceWorker();
22}
23
1
2self.addEventListener('install', function(event) {
3 console.log('install');
4
5});
6
7self.addEventListener('activate', function(event) {
8 console.log('activate');
9
10});
第三步 缓存管理
缓存管理包括两部分,一个是缓存资源,另外一个同步更新资源,在 ServiceWorker 代码中是通过Cache
与 CacheStorage
去控制,代码如下:
1self.addEventListener('install', function(event) {
2
3 event.waitUntil(
4
5 caches.open('v1').then(function(cache) {
6 cache.addAll([
7 './index.html',
8 ]);
9 })
10 );
11});
12
13
14const cacheFirst = async (request) => {
15
16 const responseFromCache = await caches.match(request);
17 console.log('responseFromCache', responseFromCache);
18 if (responseFromCache) {
19 return responseFromCache
20 }
21 return fetch(request);
22}
23
24self.addEventListener("fetch", (event) => {
25
26 console.log('caches match',);
27 console.log('fetch', event.request.url);
28 event.respondWith(cacheFirst(event.request));
29});
30
动态缓存
当然,上面是将固定的资源进行缓存,如果是需要对整个页面请求资源进行缓存管理,那么可以通过fetch
事件拦截请求实现动态缓存,代码如下:
1 * 缓存优先
2 * @param {*} request
3 * @returns
4 */
5const cacheFirst = async (request) => {
6
7 const responseFromCache = await caches.match(request);
8 console.log('responseFromCache', responseFromCache);
9 if (responseFromCache) {
10 return responseFromCache
11 }
12
13 const responseFromServer = await fetch(request);
14 const cache = await caches.open(cacheName);
15
16 cache.put(request, responseFromServer.clone());
17 return responseFromServer;
18}
19
20self.addEventListener("fetch", (event) => {
21
22 console.log('caches match',);
23 console.log('fetch', event.request.url);
24 event.respondWith(cacheFirst(event.request));
25});
26
缓存成功后,可以在 DevTools找到 网络请求状态,会标识是从 Service Worker 获取资源,具体如下图:
第四步 更新缓存池
当你的Service Worker js文件有更新,需要删除旧的缓存,同时启动新的 Service Worker cache,代码如下:
1const deleteCache = async (key) => {
2 await caches.delete(key);
3};
4
5const deleteOldCaches = async () => {
6 const cacheKeepList = ["v2"];
7 const keyList = await caches.keys();
8 const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
9 await Promise.all(cachesToDelete.map(deleteCache));
10};
11
12self.addEventListener("activate", (event) => {
13 event.waitUntil(deleteOldCaches());
14});
15
讲完了这些,可能还需要实际体验一把,可以访问在线Service Worker Demo,源码在这里Github qborfy/service worker。
上面讲述了 Service Worker 的概念和使用,但是在实际项目中,如果要按照这一套去实现,会遇到很多问题,如:经过打包后我们的 js , css等文件是动态生成的,从而导致每次都需要更新 Service Worker的 Cache 版本池。
所以需要结合构建工具去让项目更快支持 PWA应用开发,具体有以下几个。
Vite构建
Vite官方推荐使用插件vite-plugin-pwa,使用如下:
注意: vite
版本需要 3+
1npm i vite-plugin-pwa -D
调整vite
的配置文件vite.config.js
,最小配置如下:
1import { VitePWA } from 'vite-plugin-pwa'
2export default defineConfig({
3 plugins: [
4 VitePWA({
5 registerType: 'autoUpdate',
6 injectRegister: 'auto',
7 manifest: {
8 name: 'qborfy study website',
9 short_name: 'qborfyStudy',
10 description: 'qborfy study website',
11 theme_color: '#ffffff',
12 icons: [
13 {
14 src: 'favicon.png',
15 sizes: '192x192',
16 type: 'image/png'
17 },
18 {
19 src: 'favicon.png',
20 sizes: '512x512',
21 type: 'image/png'
22 }
23 ]
24 }
25 }),
26 ]
27})
最终会在 npm run build
后,完成以下几个事情:
- 生成
registerSW.js
,用来注册Service Worker
的sw.js
文件 - 生成
sw.js
文件,在index.html
引入 - 生成
manifest.webmanifest
,在index.html
引入,声明网站的信息,可以在manifest
配置项调整 - 生成
workbox.xxx.js
,用来管理缓存使用策略的代码,可以通过strategies
去配置
其他更多帮助文档可以到官方文档查看, vite-plugin-pwa官方文档
Webpack构建
Webpack作为前端最主流的构建工具,当然也有对应插件去实现,那就是workbox-webpack-plugin插件,其实是Chrome自己开源的workbox工具库中支持的插件之一。
具体用法如下:
- 安装依赖
1npm install workbox-webpack-plugin --save-dev
- webpack.config.js增加插件配置
1const WorkboxPlugin = require('workbox-webpack-plugin')
2module.exports = {
3 ...,
4 plugins: [
5 new WorkboxPlugin.GenerateSW({
6 clientsClaim: true,
7 skipWaiting: true
8 }),
9 ]
10};
- 在index.html注册 service worker
1if ('serviceWorker' in navigator) {
2 window.addEventListener('load', async () => {
3 console.log('page load...');
4 let res = await navigator.serviceWorker.register('/service-worker.js');
5 console.log(res, 'serviceWorker res');
6 if (res) {
7 console.log('register success!');
8 } else {
9 console.log('register fail!');
10 }
11 });
12}
更多帮助可以到workbox 官方文档中查看
workbox工具库
其实上面两个插件都是基于 Chrome 开源的 workbox工具库去做二次封装实现的,接下来我们对workbox.js
做一个简单的了解,方便后续如果我们需要自己去开发符合项目的 service worker控制。
Service Worker有很多抽象的概念和 API,如:网络请求!缓存策略!缓存管理!预缓存!等等, Workbox的作用就是将复杂的 API 进行抽象,使更易于使用。
Workbox 是一组简化常见服务工作线程路由和缓存的模块。每个可用模块都解决 Service Worker 开发的特定方面。 Workbox 旨在使 Service Worker 的使用尽可能简单,同时允许在需要时灵活地满足复杂的应用程序要求。
如何使用Workbox
,官方提供两种方式:
- 结合构建工具使用,如上面的 Vite 或者 Webpack
- 没有构建工具,官方提供了workbox-sw,让你可以利用 workbox api去实现自己的 service worker策略
这里简单使用一下,代码如下:
1importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
2
3
4
5workbox.routing.registerRoute(
6 ({ request }) => request.destination === 'image',
7 new workbox.strategies.CacheFirst()
8);
其他使用说明文档可以到workbox 官方文档中查看。
这里我还收集了一些开发 PWA 后续可能会用到的点,大家可以看看。
Service Worker其他
本文主要是想通过 PWA 去优化个人网站的访问速度,PWA 不仅仅只能做缓存优化,还包括一下几点:
- 通知 Notification,可以在后台接受服务器通知,然后告知用户
- 通讯 Message,可以和主 JS 线程通讯
- 后台更新,可以在用户没有访问页面的时候进行后台定时更新
如何发布一个 PWA 应用
注意事项
-
Service Worker 缓存空间限制,Chrome 没有限制大小, Safari 限制50MB
-
当第一次访问页面,资源的请求是早于 Service Worker 的,所以静态资源是无法缓存的;只有当 Service Worker 安装完毕,用户第二次访问页面的时候,这些资源才会被缓存起来;所以 Service Worker 真正生效是在第三次访问