作者:麦乐
什么是离线应用?
离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。
优点:
- 在没有网络的情况下也能打开网页。
- 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。
离线应用的核心是离线缓存技术,目前比较成熟Service Workers。它可以通过 JavaScript 代码去控制缓存的逻辑。也就是说,这个过程可以交给前端来处理。
为什么需要?
离线应用其实就是一种缓存技术,那有的人可能会想,HTTP的缓存策略已经发展的相当成熟了,为什么还需要Service Workers?前端的开发者会觉得:“浏览器的事情,为什么要依赖于后端呢?后端就好好提供数据就行了,缓存这种事情我想自己控制”。
什么是Service Workers
Service Workers 是一个在浏览器后台运行的脚本,它生命周期完全独立于网页。它无法直接访问 DOM,但可以通过 postMessage 接口发送消息来和 UI 进程通信。 拦截网络请求是 Service Workers 的一个重要功能,通过它能完成离线缓存、编辑响应、过滤响应等功能。
Service Worker并非专门为缓存而设计,它还可以解决Web应用推送、后台长计算等问题。
判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:
// 如果 navigator 对象上存在 serviceWorker 对象,就表示支持
if (navigator.serviceWorker) {
// 通过 navigator.serviceWorker 使用
}
注册 Service Workers
要给网页接入 Service Workers,需要在网页加载后注册一个描述 Service Workers 逻辑的脚本。 通常它在 index.html
中被注册,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
if ('serivceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Service worker registered successfully!'))
.catch(err => console.log('Service worker failed to register!'));
}
</script>
</body>
</html>
第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。
在 Chrome 中可以通过打开网址 chrome://inspect/#service-workers
来查看当前浏览器中所有注册了的 Service Workers。
实现离线缓存
Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。
在 Service Workers 脚本中,引入了新的关键字 self
代表当前的 Service Workers 实例。
在 Service Workers 安装成功后会派发出 install
事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:
sw.js
// 当前缓存版本的唯一标识符,用当前时间代替
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);
})
);
});
接下来需要监听网络请求事件去拦截请求,复用缓存,代码如下:
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 线程将会启动,且将会触发其 install 事件。
- 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。
- 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。
我们就可以在activate事件中删除原来的缓存。
// 当前缓存白名单,在新脚本的 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);
}
})
);
})
);
});
这样就完成了更新。
Vue中实现离线缓存
我们目前的项目大部分都是根据脚手架集成的,比如收vue,发布以前并不确定会打包出来多少个文件,假如构建输出的文件目录结构为:
├── app_4c3e186f.js
├── app_7cc98ad0.css
└── index.html
那么 sw.js
文件中 cacheFileList
的值应该是:
var cacheFileList = [
'/index.html',
'app_4c3e186f.js',
'app_7cc98ad0.css'
];
但是这些文件的名字往往是不固定的,比如有些使用hash来命名的,这种情况下如何确定需要缓存的文件路径列表呢?
这个需要借助一个插件在webpack中去实现:
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,
}
};
需要修改上面的 sw.js
文件中写成了静态值的 cacheFileList
为如下:
// 需要被缓存的文件的 URL 列表
var cacheFileList = global.serviceWorkerOption.assets;
注意需要使用https协议。
验证结果
为了验证 Service Workers 和缓存生效了,需要通过 Chrome 的开发者工具来查看。
通过打开开发者工具的 Application-Service Workers 一栏,就能看到当前页面注册的 Service Workers。