温度と乾物に関するテクノロジー共有の著者になろう — Qborfy
バックグラウンド
最近、簡単な個人記録ウェブサイトを開発しています。技術スタックは Vite + Vue3 を使用しています。単一サーバーを使用するため、サーバーの帯域幅が制限される場合があり、通常はアクセスが遅くなります。そのため、オフライン アプリケーションを実装したいと考えていますが、現時点では PWA アプリケーションが最適なソリューションです。
この記事に含まれる知識ポイントは次のとおりです。
- PWAの概念
- Service Worker が使用する
- ビルドツールを使用して PWA アプリケーションを構築する
PWA
プログレッシブ Web アプリ (PWA) は、Web プラットフォーム テクノロジを使用して構築されたアプリケーションですが、プラットフォーム固有のアプリケーションのようなユーザー エクスペリエンスを提供します。— MDN プログレッシブ ウェブ アプリ (PWA)
前述したように、PWA の最終的な目標は、Web サイトでユーザーにアプリのようなオフライン体験を提供できるようにすることであり、簡単に言うと、ネットワークがなくてもサイトの一部のリソースに通常どおりアクセスできるようになります。
PWA は技術的には 3 つの部分に分かれています。
- メイン アプリケーションは、HTML、JS、CSS など、通常開発する Web サイトに含まれるコンテンツです。
- Web アプリ マニフェストは主に
manifest.json
、アプリケーション名やアイコンなど、ブラウザーが PWA をインストールするために必要な情報を提供します。 - 主に js ファイル用の Service Worker は、基本的なオフライン キャッシュ リソース機能を提供します
マニフェスト.json
manifest.json
Web サイトに関する情報 (名前、作成者、アイコン、説明など) を記述する JSON ファイルとその例を以下に示します。
manifest.json
次のように、Web サイトの HTML ファイルの先頭で引用する必要があります。
<link rel="manifest" href="/manifest.json" />
完全なmanifest.json
例:
{
"name": "网站完整名称",
"short_name": "网站简称", // 在没有足够空间显示 Web 应用程序的全名时使用
"start_url": ".", // 从启动应用程序时加载的 URL。如果以相对 URL 的形式给出,则基本 URL 将是 manifest 的 URL
"display": "standalone", // 访问网站窗口展示模式,如:fullscreen/standalone
"background_color": "#fff", // 背景颜色
"description": "网站描述",
"icons": [ // 网站图标
{
"src": "images/touch/homescreen48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "images/touch/homescreen72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/touch/homescreen96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/touch/homescreen144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/touch/homescreen168.png",
"sizes": "168x168",
"type": "image/png"
},
{
"src": "images/touch/homescreen192.png",
"sizes": "192x192",
"type": "image/png"
}
],
}
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文件,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="./manifest.json" />
<title>Service Worker测试页面</title>
<link rel="stylesheet" href="./test.css">
</head>
<body>
<h1>测试 Service Worker</h1>
<script src="./test.js"></script>
<script>
// 这里开始注册 Service Worker
</script>
</body>
</html>
第二步 注册 Service Worker
这一步有两个 事情:
- 写Service Worker的相关逻辑的js文件 (且叫
sw.js
) - 将
sw.js
注册到html文件中 具体代码如下:
// 注册 Service worker
if ('serviceWorker' in window.navigator) {
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("./sw.js", {
scope: "/",
});
if (registration.installing) {
console.log("正在安装 Service worker");
} else if (registration.waiting) {
console.log("已安装 Service worker installed");
} else if (registration.active) {
console.log("激活 Service worker");
}
} catch (error) {
console.error(`注册失败:${error}`);
}
}
};
registerServiceWorker();
}
//sw.js
// self等同于 this
self.addEventListener('install', function(event) {
console.log('install');
// ... 安装完成 可以开始拦截请求加入缓存 cache 中
});
self.addEventListener('activate', function(event) {
console.log('activate');
// ... 激活完成 可以开始拦截请求加入缓存 cache 中
});
第三步 缓存管理
缓存管理包括两部分,一个是缓存资源,另外一个同步更新资源,在 ServiceWorker 代码中是通过Cache
与 CacheStorage
去控制,代码如下:
//sw.js
self.addEventListener('install', function(event) {
// 确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成
event.waitUntil(
// 创建了叫做 v1 的新缓存
caches.open('v1').then(function(cache) {
cache.addAll([
'./index.html', // 相对于 sw.js 的路径 缓存 index.html
]);
})
);
});
// 缓存优先
const cacheFirst = async (request) => {
// 从缓存中读取 respondWith表示拦截请求并返回自定义的响应
const responseFromCache = await caches.match(request);
console.log('responseFromCache', responseFromCache);
if (responseFromCache) {
return responseFromCache
}
return fetch(request);
}
self.addEventListener("fetch", (event) => {
// 拦截请求
console.log('caches match',);
console.log('fetch', event.request.url);
event.respondWith(cacheFirst(event.request));
});
动态缓存
当然,上面是将固定的资源进行缓存,如果是需要对整个页面请求资源进行缓存管理,那么可以通过fetch
事件拦截请求实现动态缓存,代码如下:
/**
* 缓存优先
* @param {*} request
* @returns
*/
const cacheFirst = async (request) => {
// 从缓存中读取 respondWith表示拦截请求并返回自定义的响应
const responseFromCache = await caches.match(request);
console.log('responseFromCache', responseFromCache);
if (responseFromCache) {
return responseFromCache
}
// 如果缓存中没有,就从网络中请求
const responseFromServer = await fetch(request);
const cache = await caches.open(cacheName);
// 将请求到的资源添加到缓存中
cache.put(request, responseFromServer.clone());
return responseFromServer;
}
self.addEventListener("fetch", (event) => {
// 拦截请求
console.log('caches match',);
console.log('fetch', event.request.url);
event.respondWith(cacheFirst(event.request));
});
缓存成功后,可以在 DevTools找到 网络请求状态,会标识是从 Service Worker 获取资源,具体如下图:
第四步 更新缓存池
当你的Service Worker js文件有更新,需要删除旧的缓存,同时启动新的 Service Worker cache,代码如下:
const deleteCache = async (key) => {
await caches.delete(key);
};
const deleteOldCaches = async () => {
const cacheKeepList = ["v2"];
const keyList = await caches.keys();
const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
await Promise.all(cachesToDelete.map(deleteCache));
};
self.addEventListener("activate", (event) => {
event.waitUntil(deleteOldCaches());
});
讲完了这些,可能还需要实际体验一把,可以访问在线Service Worker Demo,源码在这里Github qborfy/service worker。
项目实战
上面讲述了 Service Worker 的概念和使用,但是在实际项目中,如果要按照这一套去实现,会遇到很多问题,如:经过打包后我们的 js , css等文件是动态生成的,从而导致每次都需要更新 Service Worker的 Cache 版本池。
所以需要结合构建工具去让项目更快支持 PWA应用开发,具体有以下几个。
Vite构建
Vite官方推荐使用插件vite-plugin-pwa,使用如下:
注意: vite
版本需要 3+
npm i vite-plugin-pwa -D
调整vite
的配置文件vite.config.js
,最小配置如下:
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate', // 注册更新模式方式 默认是autoUpdate,将会自动更新,其他还有prompt和skipWaiting
injectRegister: 'auto', // 控制如何在应用程序中注册ServiceWorker 默认值是 'auto' ,其他如:'inline' 则是注入一个简单的注册脚本,内联在应用程序入口点中
manifest: { // manifest.json 文件配置
name: 'qborfy study website',
short_name: 'qborfyStudy',
description: 'qborfy study website',
theme_color: '#ffffff',
icons: [
{
src: 'favicon.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'favicon.png',
sizes: '512x512',
type: 'image/png'
}
]
}
}),
]
})
最终会在 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工具库中支持的插件之一。
具体用法如下:
- 安装依赖
npm install workbox-webpack-plugin --save-dev
- webpack.config.js增加插件配置
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
...,
plugins: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true, // 快速启用服务
skipWaiting: true
}),
]
};
- 在index.html注册 service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
console.log('page load...');
let res = await navigator.serviceWorker.register('/service-worker.js');
console.log(res, 'serviceWorker res');
if (res) {
console.log('register success!');
} else {
console.log('register fail!');
}
});
}
更多帮助可以到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策略
这里简单使用一下,代码如下:
//sw.js
// 引入 workbox importScripts是 Service Worker 中的全局方法,用于引入外部脚本
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
// 下面就可以直接使用workbox对象的方法,如:workbox.precaching.*, workbox.routing.*等
// 这里表示当请求的资源是图片时,使用 CacheFirst 策略,也就是优先从缓存中读取,如果缓存中没有,就从网络中请求
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst()
);
その他の使用説明は、workbox の公式ドキュメントで参照できます。
その他関連する
ここでは、後で PWA の開発に使用できるいくつかのポイントもまとめました。ぜひご覧ください。
サービスワーカーその他
この記事では主に PWA を通じて個人 Web サイトのアクセス速度を最適化したいと考えていますが、PWA はキャッシュの最適化を行うだけでなく、次の点も含みます。
- 通知 通知。バックグラウンドでサーバー通知を受け入れ、ユーザーに通知できます。
- メインの JS スレッドと通信できる通信メッセージ
- バックグラウンド更新。ユーザーがページにアクセスしていないときにバックグラウンドで定期的に更新できます。
PWA アプリケーションを公開する方法
- PWA アプリケーションを Google Play ストアに公開する方法
- PWA アプリケーションを Microsoft Store に公開する方法
- PWA アプリケーションを Meta Quest ストアに公開する方法
予防
- Service Worker のキャッシュ容量制限、Chrome にはサイズ制限なし、Safari には 50MB の制限あり
- 初めてページにアクセスするときは、リソース要求が Service Worker よりも早いため、静的リソースはキャッシュできません。Service Worker がインストールされ、ユーザーが 2 回目にページにアクセスした場合にのみ、これらのリソースがキャッシュされます。したがって、Service Worker は 3 回目の訪問時に実際に有効になります。