前言
离线缓存应用,意思就是没有网络的情况下,被缓存的资源能从本地直接返回,让应用在一定范围内能正常运行。
离线应用有以下优点:
- 在没有网络的情况下也能打开网页。
- 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。
实现离线缓存的技术有两种:
- AppCache 又叫 Application Cache,目前已经从 Web 标准中删除,请尽量不要使用它。
- Service Workers 是目前最新的离线缓存技术,是 Web Worker 的一部分。
Service Workers 通过拦截网络请求实现离线缓存,比 AppCache 更加灵活。它也是构建 PWA 应用的关键技术之一。
出于安全原因,Service Workers 要求必须在 HTTPS 下才能运行。
在本地开发中,我们可以在启动 chrome 时让本地应用设置为安全,可以不使用 https,而使用 http,否则会报错。
如启动的本地应用地址为http://localhost:8080/
;
1 2
| // 在终端启动chrome,chrome文件路径 "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --unsafely-treat-insecure-origin-as-secure=http://localhost:8080/
|
Service Workers 兼容性
目前 Chrome、Firefox、Opera 都已经全面支持 Service Workers,但对于移动端浏览器就不太乐观了,只有高版本的 Android 支持。
由于 Service Workers 无法通过注入 polyfill 去实现兼容,所以在你打算使用它前请先调查清楚你的网页的运行场景。
判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:
1 2 3 4
| // 如果 navigator 对象上存在 serviceWorker 对象,就表示支持 if (navigator.serviceWorker) { // 通过 navigator.serviceWorker 使用 }
|
注册
要给网页接入 Service Workers,需要在网页加载后注册一个描述 Service Workers 逻辑的脚本。
所以编写一个 sw.js 专门包含 Service Workers 逻辑的脚本,比如缓存内容,sw.js 更新后重新加载缓存内容等等,而 sw.js 文件需要在应用入口文件加载注册。
如 main.js 为 webpack 的入口文件:
1 2 3 4 5 6 7 8 9 10
| require('./main.css');
if (navigator.serviceWorker) { window.addEventListener('DOMContentLoaded',function() { // 调用 serviceWorker.register 注册,参数 /sw.js 为脚本文件所在的 URL 路径 navigator.serviceWorker.register('sw.js'); }); }
window.document.getElementById('app').innerText = 'Hello,Webpack';
|
一旦这个脚本文件被加载,Service Workers 的安装就开始了。这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。
也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。
在 Chrome 中可以通过打开网址 chrome://inspect/#service-workers 来查看当前浏览器中所有注册了的 Service Workers。
使用 Service Workers 实现离线缓存
Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。
在 Service Workers 脚本中,引入了新的关键字 self 代表当前的 Service Workers 实例。
在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // 当前缓存版本的唯一标识符,用当前时间代替 var cacheKey = new Date().toISOString();
// 需要被缓存的文件的 URL 列表 var cacheFileList = [ '/index.html', '/app.js', '/app.css' ];
// 监听 install 事件 self.addEventListener('install', function (event) { // 等待所有资源缓存完成时,才可以进行下一步 event.waitUntil( caches.open(cacheKey).then(function (cache) { // 要缓存的文件 URL 列表 return cache.addAll(cacheFileList); }) ); });
|
接下来需要监听网络请求事件去拦截请求,复用缓存,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| self.addEventListener('fetch', function(event) { event.respondWith( // 去缓存中查询对应的请求 caches.match(event.request).then(function(response) { // 如果命中本地缓存,就直接返回本地的资源 if (response) { return response; } // 否则就去用 fetch 下载资源 return fetch(event.request); } ) ); });
|
以上就实现了离线缓存。
更新缓存
线上的代码有时需要更新和重新发布,如果这个文件被离线缓存了,那就需要 Service Workers 脚本中有对应的逻辑去更新缓存。
这可以通过更新 Service Workers 脚本文件做到。
浏览器针对 Service Workers 有如下机制:
- 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件(所以要注意该脚本文件不能太大),如果发现和当前已经注册过的文件存在字节差异,就将其视为“新服务工作线程”。
- 新 Service Workers 线程将会启动,且将会触发其 install 事件。
- 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。
- 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。
新 Service Workers 线程中的 activate 事件就是最佳的清理旧缓存的时间点,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 当前缓存白名单,在新脚本的 install 事件里将使用白名单里的 key var cacheWhitelist = [cacheKey];
self.addEventListener('activate', function(event) { event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { // 不在白名单的缓存全部清理掉 if (cacheWhitelist.indexOf(cacheName) === -1) { // 删除缓存 return caches.delete(cacheName); } }) ); }) ); });
|
sw.js 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| // 当前缓存版本的唯一标识符,用当前时间代替 var cacheKey = new Date().toISOString();
// 当前缓存白名单,在新脚本的 install 事件里将使用白名单里的 key var cacheWhitelist = [cacheKey];
// 需要被缓存的文件的 URL 列表 var cacheFileList = [ '/index.html', 'app.js', 'app.css' ];
// 监听 install 事件 self.addEventListener('install', function (event) { // 等待所有资源缓存完成时,才可以进行下一步 event.waitUntil( caches.open(cacheKey).then(function (cache) { // 要缓存的文件 URL 列表 return cache.addAll(cacheFileList); }) ); });
// 拦截网络请求 self.addEventListener('fetch', function (event) { event.respondWith( // 去缓存中查询对应的请求 caches.match(event.request).then(function (response) { // 如果命中本地缓存,就直接返回本地的资源 if (response) { return response; } // 否则就去用 fetch 下载资源 return fetch(event.request); } ) ); });
// 新 Service Workers 线程取得控制权后,将会触发其 activate 事件 self.addEventListener('activate', function (event) { event.waitUntil( caches.keys().then(function (cacheNames) { return Promise.all( cacheNames.map(function (cacheName) { // 不在白名单的缓存全部清理掉 if (cacheWhitelist.indexOf(cacheName) === -1) { // 删除缓存 return caches.delete(cacheName); } }) ); }) ); });
|
z 这些都是没有接入 webpack 时实现的。
接入 Webpack
使用 webpack 要使用serviceworker-webpack-plugin插件。
1
| npm i -D serviceworker-webpack-plugin webpack-dev-server
|
webpack 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const path = require("path"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const { WebPlugin } = require("web-webpack-plugin"); const ServiceWorkerWebpackPlugin = require("serviceworker-webpack-plugin");
module.exports = { entry: { app: "./main.js" // Chunk app 的 JS 执行入口文件 }, output: { filename: "[name].js", publicPath: "" }, module: { rules: [ { test: /\.css/, // 增加对 CSS 文件的支持 // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ use: ["css-loader"] // 压缩 CSS 代码 }) } ] }, plugins: [ // 一个 WebPlugin 对应一个 HTML 文件 new WebPlugin({ template: "./template.html", // HTML 模版文件所在的文件路径 filename: "index.html" // 输出的 HTML 的文件名称 }), new ExtractTextPlugin({ filename: `[name].css` // 给输出的 CSS 文件名称加上 hash 值 }), new ServiceWorkerWebpackPlugin({ // 自定义的 sw.js 文件所在路径 // ServiceWorkerWebpackPlugin 会把文件列表注入到生成的 sw.js 中 entry: path.join(__dirname, "sw.js") }) ], devServer: { // Service Workers 依赖 HTTPS,使用 DevServer 提供的 HTTPS 功能。 // https: true, } };
|
devServer 的 https 可以不开启,如果 chrome 启动设置不禁止 http;
使用了 ServiceWorkerWebpackPlugin 后可以在 sw.js 里 global.serviceWorkerOption 下获取 assets,里面包含了 webpack 输出的所有静态文件。
1 2 3 4 5 6 7 8 9
| Version: webpack 3.12.0 Time: 16ms Asset Size Chunks Chunk Names app.css 21 bytes 0 [emitted] app index.html 332 bytes [emitted] sw.js 7.13 kB [emitted]
global.serviceWorkerOption.assets --- ["/app.js", "/app.css", "/index.html"]
|
而 ServiceWorkerWebpackPlugin 内置了注册启动
1 2 3
| import runtime from "serviceworker-webpack-plugin/lib/runtime"; // 调用 serviceWorker.register 注册,参数 /sw.js 为脚本文件所在的 URL 路径 var registration = runtime.register();
|
ServiceWorkerWebpackPlugin 事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import registerEvents from "serviceworker-webpack-plugin/lib/browser/registerEvents"; registerEvents(registration, { onInstalled: () => { pushLog("onInstalled"); }, onUpdateReady: () => { pushLog("onUpdateReady", true); },
onUpdating: () => { pushLog("onUpdating"); }, onUpdateFailed: () => { pushLog("onUpdateFailed"); }, onUpdated: () => { pushLog("onUpdated"); } });
|
ServiceWorkerWebpackPlugin 更新后手动更新
1 2 3 4 5
| import applyUpdate from "serviceworker-webpack-plugin/lib/browser/applyUpdate"; // 当为onUpdateReady时,可以使用applyUpdate触发更新操作 applyUpdate().then(() => { window.location.reload(); });
|
验证
通过打开开发者工具的 Application-Service Workers 一栏,就能看到当前页面注册的 Service Workers,正常的效果如图:
通过打开开发者工具的 Application-Cache-Cache Storage 一栏,能看到当前页面缓存的资源列表,正常的效果如图:
为了验证网页在离线时能访问的能力,需要在开发者工具中的 Network 一栏中通过 Offline 选项禁用掉网络,再刷新页面能正常访问,并且网络请求的响应都来自 Service Workers,正常的效果如图:
从图上可以看到红字的为没有缓存的,而有 ServiceWorker 的为有缓存的。