创建你的第一个 Service Worker 应用

本文提到的 Service Worker Demo

先导技术

浏览器缓存

强缓存

强缓存是利用 Expires 或者 Cache-Control 这两个响应头实现的,它们都用来表示资源在客户端缓存的有效期。

Expires 的值对应一个 GMT 时间来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。

Cache-Control 标头是在 HTTP/1.1 规范中定义的,取代了之前用来定义响应缓存策略的标头(例如 Expires)

指令 说明
max-age=86400 浏览器以及任何中间缓存均可将响应(如果是“public”响应)缓存长达 1 天(60 秒 x 60 分钟 x 24 小时)
private, max-age=600 客户端的浏览器只能将响应缓存最长 10 分钟(60 秒 x 10 分钟)
no-store 不允许缓存响应,每次请求都必须完整获取

协商缓存

协商缓存是利用的是 Last-Modified,If-Modified-Since 和 ETag,If-None-Match 这两对头来管理的。

客户端自动在 If-None-Match HTTP 请求标头内提供 ETag 令牌。服务器根据当前资源核对令牌。如果它未发生变化,服务器将返回 304 Not Modified 响应,告知浏览器缓存中的响应未发生变化,不必再次下载响应,这节约了时间和带宽。

那么如何才能实现客户端缓存和快速更新

  • HTML 被标记为“no-cache”,这意味着浏览器在每次请求时都始终会重新验证文档,并在内容变化时获取最新版本。此外,在 HTML 标记内,您在 CSS 和 JavaScript 资源的 URL 中嵌入指纹,如果这些文件的内容发生变化,网页的 HTML 也会随之改变,并会下载 HTML 响应的新副本
  • 允许浏览器和中间缓存(例如 CDN)缓存 CSS,并将 CSS 设置为 1 年后到期。请注意,您可以放心地使用 1 年的“远期过期 (far future expires) ”,因为您在文件名中嵌入了文件的指纹,CSS 更新时,它的 URL 也会随之变化
  • JavaScript 同样设置为 1 年后到期,但标记为 private,这或许是因为它包含的某些用户私人数据是 CDN 不应缓存的
  • 图像缓存时不包含版本或唯一指纹,并设置为 1 天后到期

引申:如何部署前端代码?

现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。

先部署页面,再部署资源就会导致在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来

先部署资源,再部署页面会导致没有本地缓存或者缓存过期的用户访问网站,出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常

这个问题来源于资源的覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决方法就是实现 非覆盖式发布

用文件的摘要信息 ( 使用 hash 永久缓存,浏览器根本不需要发任何请求,直接从本地缓存加载) 来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。

  • 配置超长时间的本地缓存,节省带宽,提高性能
  • 采用内容摘要作为缓存更新依据,精确的缓存控制
  • 静态资源CDN部署,优化网络请求
  • 非覆盖式发布

应用缓存

该特性已经从 Web 标准中删除

为了在无网络下也能访问应用,HTML5 规范中设计了应用缓存(Application Cache)。使得应用程序可以离线运行。然而这个 API 的设计有太多的缺陷,包括无法清空缓存,manifest 编写要求严格等,最终被废弃。

CacheStorage 和 Cache

Cache API 是用于存储和查询 HTTP 请求和对应响应的系统。这些可以是在运行应用时中发起的 HTTP 请求和响应,也可以在缓存中存储一些数据。

1
2
3
4
5
6
7
8
9
10
// 操作 CacheStorage 缓存,使用之前需要先通过 caches.open() 打开对应缓存空间
caches.open('my-test-cache-v3')
.then(function (cache) {
// 通过 cache 缓存对象的 addAll 方法添加 precache 缓存
return cache.addAll([
'/',
'/index.html',
'/picture.jpeg'
]);
})

Web Workers

Web Workers 可以后台线程中运行脚本,线程可以执行任务而不干扰用户界面,它采用的消息传递模型是一个很好的多线程编程模型,不仅简单易懂,同样也可以达到很高的性能。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div style='width:100px;height:100px;background-color:red'></div>
<script>
document.querySelector('div').onclick = function () {
console.log('hello world');
};
var worker = new Worker('worker.js');
worker.postMessage(40);
worker.onmessage = function (event) {
var data = event.data;
console.log(data);
};
worker.onerror = function (event) {
console.log(event.filename, event.lineno, event.message);
};
</script>

worker.js

1
2
3
4
5
6
7
8
9
self.onmessage = function (event) {
var data = event.data;
var ans = fibonacci(data);
this.postMessage(ans);
};
function fibonacci (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

Service Worker

Service Worker 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

注册 Service Worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 判断 Service Worker 是否可用
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
// 注册 Service Worker
navigator.serviceWorker.register('./sw.js', {scope: '/'})
.then(function (registration) {
if (registration.installing) {
console.log('Service worker installing');
} else if (registration.waiting) {
console.log('Service worker installed');
} else if (registration.active) {
console.log('Service worker active');
}
console.log('注册成功');
})
.catch(function (err) {
console.log('注册失败', err);
});
});
}

填充缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 监听 Service Worker 的 install 事件
this.addEventListener('install', function (event) {
event.waitUntil(
// 安装成功后操作 CacheStorage 缓存,使用之前需要先通过 caches.open() 打开对应缓存空间
caches.open('my-test-cache-v3')
.then(function (cache) {
// 通过 cache 缓存对象的 addAll 方法添加 precache 缓存
return cache.addAll([
'/',
'/index.html',
'/picture.jpeg'
]);
})
);
});

自定义请求的响应

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
this.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
// 如果 Service Worker 有自己的返回,就直接返回,减少一次 http 请求
if (response) {
console.log(new Date(), 'fetch ', event.request.url, '有缓存,从缓存中取');
return response;
}
console.log(new Date(), 'fetch ', event.request.url, '没有缓存,网络获取');
// 如果 service worker 没有返回,那就直接请求远程服务
var request = event.request.clone();
return fetch(request).then(function (httpRes) {
if (!httpRes || httpRes.status !== 200) {
return httpRes;
}
// 请求成功则将请求缓存起来。
var responseClone = httpRes.clone();
caches.open('my-test-cache-v1').then(function (cache) {
cache.put(event.request, responseClone);
});
return httpRes;
});
})
);
});

更新 Service Worker

当安装发生的时候,前一个版本依然在响应请求,新的版本正在后台安装,我们调用了一个新的缓存 v2,所以前一个 v1 版本的缓存不会被扰乱。

当没有页面在使用当前的版本的时候,这个新的 Service Worker 就会激活并开始响应请求。

删除旧缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.addEventListener('activate', function (event) {
var cacheWhitelist = ['v3'];
// 删除旧缓存
event.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(keyList.map(function (key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});

Service Worker 只是 Service Worker

Service Worker 只是一个常驻在浏览器中的 JS 线程,它本身做不了什么。它能做什么,全看跟哪些 API 搭配使用。

  • 跟 Fetch 搭配,可以从浏览器层面拦截请求,做数据 mock
  • 跟 Fetch 和 CacheStorage 搭配,可以做离线应用
  • 跟 Push 和 Notification 搭配,可以做像 Native APP 那样的消息推送

假如把这些技术融合在一起,再加上 Manifest 等,就差不多成了 PWA 了。

接收推送消息

https://fed.renren.com/2017/10/08/service-worker-notification/

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
this.addEventListener('push', function (event) {
console.log(event);
var title = '博客更新啦';
var body = '点开看看吧';
var icon = '/images/icon-192x192.png';
var tag = 'simple-push-demo-notification-tag';
var data = {
url: location.origin
};
event.waitUntil(
this.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: data
})
);
});
this.addEventListener('notificationclick', function (event) {
console.log('[Service Worker] Notification click Received.');
let notification = event.notification;
console.log(notification.data);
notification.close();
event.waitUntil(
clients.openWindow(notification.data.url)
);
});

参考资料:

浏览器缓存、CacheStorage、Web Worker 与 Service Worker

HTTP 缓存

大公司里怎样开发和部署前端代码?