Justin's Words

Service workers

常用 web 静态资源的缓存

  • 代理服务器缓存
  • CDN 缓存
  • 浏览器端缓存

浏览器缓存

  • Cache-Control: max-age=3600(s) | no-cache | no-store
  • Pragma: no-cache | Pragma
  • Expires: Fri, 11 Aug 2017, 17:00:00 GMT
  • Last-Modified: Fri, 11 Aug 2017, 17:00:00 GMT
  • If-Modified-Since: Fri, 11 Aug 2017, 17:00:00 GMT
  • Etag: “v2.6”
  • If-None-Match: “v2.6”

no-cache 是允许缓存的,但是使用缓存之前必须和服务器进行新鲜度对比,no-store 才是禁止缓存。

AppCache

对app内存缓存的方案,具体表现为当请求某个文件时不是从网络获取该文件,而是从本地获取。

index.appcache

1
2
3
4
5
6
7
8
9
CACHE MANIFEST
index.html
index.css
index.js
logo.jpg
NETWORK:
*
FALLBACK:
/404.html

index.html

1
2
3
<html manifest="index.appcache">
...
</html>

这样的话,每次都会请求 index.appcache,如果请求的文件存在在 index.appcache 里面,就无需向服务器请求。

弊端

  • 除非改变 index.appcache,否则 index.appcache 里面的文件永远只会来自应用缓存
  • 不在 appcache 缓存列表里面的文件不会被缓存,再次请求也不会去请求 😶

Service workers

本质上充当浏览器和网络之间的代理服务器。它们旨在能够创建有效的离线体验,拦截网络请求并基于网络是否可用判断资源是否需要更新来采取适当的动作。他们还允许访问推送通知和后台同步API。

Service worker 运行在 worker 线程,和驱动应用的 JavaScript 线程是分开的,两者相互不受影响,所以 Service worker 是不能访问 DOM 的。

完全的使用 Promise 异步设计,不允许使用同步 API。必须在 HTTPS 下使用,但是调试的时候在 localhost 下面可以使用 HTTP。

X5 内核浏览器是支持 service worker 的。

Service workers 生命周期

Service workers 监听事件

入口

这里指定的 sw.js 是在 worker 线程工作,所以它不能访问 DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function registerSW() {
if ('serviceWorker' in navigator) {
// 指定 service worker 执行文件和其控制的文件的子目录
return navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(reg => {
if (reg.installing) {
log('Service worker status', 'installing');
} else if (reg.waiting) {
log('Service worker status', 'waiting');
} else if (reg.active) {
log('Service worker status', 'active');
}
}).catch(error => {
console.error(`Registration failed with ${error}`);
});
}

return false;
}

安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 需要缓存的文件列表,可以通过 Legos 生成或者 Webpack 获取
const assets = [
"/bg-header.24e53c7254a141935d68580e59ac3e2a.jpg",
"/i-logo.fe4eaaab959dae955171e85dd6451f01.png",
"/sprite-index.155b7aa999c0b00c8b3041e88dab5e3a.png",
"/bundle.9eb1d16891f485d70cfd.js",
"/style.9eb1d16891f485d70cfd.css"
];

// 版本号
const CACHE_NAME = (new Date()).toISOString();

// 执行安装,将需要缓存的文件存入 CacheStorage
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
log('Install', assets);
return cache.addAll(assets);
}).then(self.skipWaiting)
);
});

请求拦截

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
// 这里会监听浏览器的 HTTP 请求,并做一层拦截
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
// 如果这个请求已经缓存过了,则直接使用 Cache 里面的缓存
log('Use cache', response.url);
return response;
}

// 如果没有请求过,前往服务器请求
return fetch(event.request).then(response => {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}

log('Fetch', event.request);

// 将新的请求回复也缓存下来
let responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
log('Add fetch', event.request);
});
}).catch(error => {
log('Fetch failed', error);
});
})
);
});

缓存更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 如果请求回来的 sw.js 有改变,则触发 activate 事件,
* 然后触发 install 事件重新拉取文件进行缓存
*/
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(
keys => Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => {
// 把旧的缓存全部删掉,以免占用空间
caches.delete(key);
log('Delete cache', key);
})
)
).then(self.skipWaiting)
);
});

调试

一些新的 API

Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Request extends Object, Body {
readonly cache: RequestCache;
readonly credentials: RequestCredentials;
readonly destination: RequestDestination;
readonly headers: Headers;
readonly integrity: string;
readonly keepalive: boolean;
readonly method: string;
readonly mode: RequestMode;
readonly redirect: RequestRedirect;
readonly referrer: string;
readonly referrerPolicy: ReferrerPolicy;
readonly type: RequestType;
readonly url: string;
clone(): Request;
}

Resopnse

1
2
3
4
5
6
7
8
9
10
interface Response extends Object, Body {
readonly body: ReadableStream | null;
readonly headers: Headers;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly type: ResponseType;
readonly url: string;
clone(): Response;
}

Cache 是一个专为为 Request 和 Response 对象提供缓存机制的接口,它可以把 Request 和 Response 对象缓存起来,并且提供一些很方便的 API 供调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Cache {
// 单个添加
add(request: RequestInfo): Promise<void>;
// 批量添加
addAll(requests: RequestInfo[]): Promise<void>;
// 单个删除
delete(request: RequestInfo, options?: CacheQueryOptions): Promise<boolean>;
// 获取存储的所有对象的 key
keys(request?: RequestInfo, options?: CacheQueryOptions): any;
// 查看是否存有该对象
match(request: RequestInfo, options?: CacheQueryOptions): Promise<Response>;
// 批量查看是否存有这些对象
matchAll(request?: RequestInfo, options?: CacheQueryOptions): any;
// 添加对象
put(request: RequestInfo, response: Response): Promise<void>;
}

参考

https://x5.tencent.com/tbs/guide/serviceworker.html

https://alistapart.com/article/application-cache-is-a-douchebag

http://harttle.com/2017/04/10/service-worker-update.html

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers