目前前端缓存的主要分为 http 缓存和浏览器缓存
- Expires、Cache-Control、Last-Modifed/If-Modified-Since、Etag/If-None-Match
- Storage:cookie、localStorage、sessionStorage
http缓存依赖于服务端配置,memory cache和disk cache缓存内容不可控,而且只缓存一些静态资源,localstorage适用于缓存一些全局的数据,对于静态资源很少用它。
Q:那么有没有一种无感知,能提前加载,实时更新的缓存?
A:让 Service worker 登场吧
Service worker 介绍
Service Worker 是一个独立于js主线程的一种 Web Worker 线程,本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。它能提供一种良好的统筹机制对资源缓存和网络请求进行缓存和处理,是 PWA 实现离线可访问、稳定访问、静态资源缓存的一项重要技术。 由于它是独立于浏览器主线程的工作线程,与当前的浏览器主线程是完全隔离的,并有自己独立的执行上下文(context),因此它不能访问 DOM。
优缺点
优点:
- 缓存内容开发者完全可控
- 持续性缓存
- 独立于主线程之外,不堵塞进程
缺点:
- 权限太大,能拦截所有fetch请求,需要控制一下
- 发版更新处理比较麻烦
生命周期
注册
注册阶段是通知浏览器 service worker 的存在,并且在后台开始安装
安装(install)
注册之后执行,表示开始安装,触发 install 时间回调指定一些静态资源进行缓存,预缓存也是在这个阶段进行。
激活(activating)
在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
激活后(activated)
- 完成激活之后,Service Worker 就能够控制作用域下的页面的资源请求,可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)。
其实所有站点 SW 的 install 和 active 都差不多,无非是做预缓存资源列表,更新后缓存清理的工作,逻辑不太复杂,而重点在于 fetch 事件。上面的代码,就是一个缓存的例子,可以看到代码是非常冗余的,这里的策略大概就是优先在Cache中寻找资源,如果找不到再请求资源。
Workbox
Workbox 用于支持离线web apps的js库, 是Google 根据SW的最佳实践推出的官方框架,封装了SW底层的API来监听SW的安装,激活,fetch事件以及缓存等相关逻辑处理,使用起来更简单方便。可以说是开箱即用了。
- 预缓存
- 运行时缓存
- 策略(Strategies)
- 请求路由
- 相比于 sw-precache和 sw-toolbox 具有更大的灵活性和更完备的功能
Workbox 的主要功能是它的路由和缓存策略模块。侦听网页的请求,并根据策略如何缓存和响应请求。这两个功能的具体实现对应于它们的workerbox的workbox.routing ,workbox.strategies 和workbox.precaching。
通过 workbox.routing 模块提供的路由控制和 workbox.strategies 模快提供的缓存策略控制帮助你做动态缓存。通过workbox.precaching 模块来处理 Service Worker 静态资源的预缓存。
Workbox
// sw.js
importScripts("https://g.alicdn.com/kg/workbox/3.6.3/workbox-sw.js");
Workbox 配置
Workbox 提供了默认的预缓存和动态缓存的名称,可分别通过 workbox.core.cacheNames.precache 和 workbox.core.cacheNames.runtime 获取当前定义的预缓存和动态缓存名称。在通常情况下,使用默认的缓存名称进行资源存取即可,假如遇到缓存名称冲突的情况,也可以调用 workbox.core.setCacheNameDetails 方法去修改这些默认名称。
/* globals workbox */
workbox.core.setCacheNameDetails({
prefix: "sw-cache",
suffix: "v2",
precache: "install-time",
runtime: "run-time",
googleAnalytics: "ga",
});
Workbox预缓存功能
workbox.precaching 对象提供了常用的预缓存功能,其中最常用的方法是 workbox.precaching.precacheAndRoute。它的作用是将传入的资源列表进行预缓存,同时对匹配到的预缓存请求直接从本地缓存中读取并返回。
workbox.routing.precacheAndRoute([
{
url: '/index.html',
revision: 'aaa'
},
'/index.abc.js',
'/index.bcd.css'
])
Workbox 路由功能
Workbox 对资源请求匹配和对应的缓存策略执行进行了统一管理,采用路由注册的组织形式,以此来规范化动态缓存。与前面我们封装的 Router 类似,Workbox 提供了 worbox.routing.registerRoute 方法进行路由注册,使用方法如下
// sw.js
workbox.routing.registerRoute(match, handlerCb)
workbox.routing.registerRoute 的第一个参数 match 是路由的匹配规则,支持以下几种匹配模式:
1.对资源 URL 进行字符串匹配。URL 字符串可以是完整 URL 或者是相对路径,如果是相对路径,Workbox 首先会以当前网页的 URL 为基准进行补全再进行字符串匹配。假设当前页面的 URL 为 http://127.0.0.1:8080/index.html,那么如下所示所注册的路由都是能够正常匹配到 http://127.0.0.1:8080/index.css 这个资源请求的:
workbox.routing.registerRoute('http://127.0.0.1:8080/index.css', handlerCb)
workbox.routing.registerRoute('/index.css', handlerCb)
workbox.routing.registerRoute('./index.css', handlerCb)
2.对资源 URL 进行正则匹配。假设我们注册这样一条正则匹配的路由规则:
workbox.routing.registerRoute(//index.css$/, handlerCb)
3.自定义路由匹配方法。match 允许传入一个自定义方法来实现较为复杂的资源请求匹配规则。
workbox.routing.registerRoute(
function(event) {
// 需要缓存的HTML路径列表
if (event.url.host === 'www.baidu.com') {
if (~cacheList.indexOf(event.url.pathname)) return true;
else return false;
} else {
return false;
}
},
workbox.strategies.networkFirst({
cacheName: 'tbh:html',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 10
})
]
})
);
Workbox 缓存策略
workbox.strategies 对象提供了一系列常用的动态缓存策略来实现对资源请求的处理。包括了以下几种策略:
- NetworkFirst:网络优先
- CacheFirst:缓存优先( 一般不用如果缓存错误资源后面不会更新)
- NetworkOnly:仅使用正常的网络请求
- CacheOnly:仅使用缓存中的资源
- StaleWhileRevalidate:从缓存中读取资源的同时发送网络请求更新本地缓存
这些策略与前面与前面 fetch中封装的简易实现的缓存策略做对比可以发现,其原理基本是一致的,当然在具体实现上 Workbox 考虑得更为复杂而全面以应对各式各样的生产环境。我们就不需要再手写 fetch 来控制缓存策略了。还有一些 workbox 插件机制(包括官方提供的和第三方自定义的插件)这里简单看下,主要的目的还是让我们配置缓存策略的时候更加灵活。
workbox.routing.registerRoute(
/.(?:png|gif|jpg|jpeg|svg)$/,
workbox.strategies.cacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 60, // 最大的缓存数,超过之后则走 LRU 策略清除最老最少使用缓存
maxAgeSeconds: 30 * 24 * 60 * 60, // 这只最长缓存时间为 30 天
}),
],
}),
);
workbox.routing.registerRoute(
'https://api.github.com',
workbox.strategies.cacheFirst({
cacheName: 'api-cache1',
plugins: [
// 这个插件是让匹配的请求的符合开发者指定的条件的返回结果可以被缓存
new workbox.cacheableResponse.Plugin({
statuses: [0, 200]
})
]
}),
);
webpack 插件 workbox-webpack-plugin
Google很贴心的发布了一个 webpack 插件,谷歌官方给我们提供了workbox的webpack插件,通过这个插件,我们能在项目中快速引入workbox,通过配置来定制化我们的缓存。
// config.js
const { override, addWebpackPlugin } = require("customize-cra");
const { InjectManifest } = require("workbox-webpack-plugin");
const path = require("path");
module.exports = (webpack, ...args) => {
webpack.plugins.pop();
const overridenConf = override(
addWebpackPlugin(
new InjectManifest({
swSrc: path.join(process.cwd(), './src/custom-serviceWorker.js'),
swDest: "./service-worker.js",
exclude: [
/.map$/,
/manifest$/,
/.htaccess$/,
/service-worker.js$/,
/sw.js$/,
],
})
)
)(webpack, ...args);
return overridenConf;
};
总结
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,当Service Worker 没有命中缓存的话,会根据缓存查找优先级去查找数据。
Workbox 是 Google Chrome 团队推出的一套 PWA 的解决方案 ,由于直接写原生的sw.js,比较繁琐和复杂,所以一些工具就出现了,Workbox是其中比较优秀的一款,类似的还有sw-toolbox等。workbox 提供了五种缓存策略方法方便我们进行缓存选择,搭配Google 官方提供的 webpack 插件,方便快捷接入 workbox。