前言:看到一篇讲解 service worker 的文章,基础讲的还不错,所以转了以后作为自己的参考
Service Worker 是什么
service worker 是独立于当前页面的一段运行在浏览器后台进程里的脚本。它的特性将包括推送消息, 背景后台同步,geofencing(地理围栏定位),拦截和处理网络请求。
这个 API 会让人兴奋的原因是,它可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。
在 service worker 之前,另一个叫做 APP Cache 的 api 也可以提供离线体验。APP Cache 的的主要问题是坑比较多,而且其被设计为只适合于单页 web 应用程序,对于传统的多页网站则不适合。service worker 的设计规避了这些痛点。
关于 service worker 的一些注意点:
- service worker 是一个 JavaScript worker , 所以它不能直接访问 DOM。但 service worker 可以通过 postMessage 接口与跟其相关的页面进行通信, 发送消息, 从而让这些页面在有需要的时候去操纵 DOM。
- Service worker 是一个可编程的网络代理,允许你去控制如何处理页面的网络请求,可以处理 fetch 请求。
- Service worker 在不使用时将被终止,并会在需要的时候重新启动,因此你不能把 onfetch 和 onmessage 事件来作为全局依赖处理程序。如果你需要持久话一些信息并在重新启动 Service worker 后使用他,可以使用 IndexedDBAPI,service worker 支持。
- Service Worker 的缓存机制是依赖 Cache API 实现的 Service worker 广泛使用了 promise。
- Service worker 依赖 HTML5 fetch API
- Service Workers 要求必须在 HTTPS 下才能运行
Service Worker 生命周期
- 注册 service worker,在网页上生效
- 安装成功,激活 或者 安装失败(下次加载会尝试重新安装)
- 激活后,在 sw 的作用域下作用所有的页面,首次控制 sw 不会生效,下次加载页面才会生效。
- sw 作用页面后,处理 fetch(网络请求)和 message(页面消息)事件 或者 被终止(节省内存)。
需要提前掌握的 API
- Cache API 基本使用
- (1)检测 api 是否存在
if('caches' in window) {// Has support!}
- (2)caches.open,创建缓存总对象。如下创建名为 test-cache 的缓存。
caches.open('test-cache').then(function(cache) {// Cache is created and accessible});
- (3)cache.add 和 cache.addAll,添加缓存内容。其中 cache.add 只添加一个,cache.addAll 可以添加多个。
caches.open('test-cache').then(function(cache) {cache.addAll(['/', '/images/logo.png'])
.then(function() {
// Cached!
// or use cache.add
cache.add('/page/1'); // "/page/1" URL will be fetched and cached!
});
});
- (4)cache.keys(),查看已经缓存的数据
caches.open('test-cache').then(function(cache) {cache.keys().then(function(cachedRequests) {console.log(cachedRequests); // [Request, Request]
});
});
- (5)cache.match 和 cache.matchAll,匹配缓存文件路径
caches.open('test-cache').then(function(cache) {cache.match('/page/1').then(function(matchedResponse) {console.log(matchedResponse);
});
});
(6)cache.delete,删除缓存。
caches.open('test-cache').then(function(cache) {cache.delete('/page/1');
});
- Fetch API 基本使用
// url (required), options (optional)
fetch('https://davidwalsh.name/some/url', {method: 'get'}).then(function(response) {}).catch(function(err) {// Error :(});
其中 options 对象包含以下属性:
- method – GET, POST, PUT, DELETE, HEAD
- url – 请求的链接
- headers – 请求的 header 对象
- referrer – 请求的 referrer 对象
- mode – cors, no-cors, same-origin
- credentials – 设置请求可不可以携带 cookie
- redirect – follow, error, manual
- integrity – 子资源完整值
- cache – 缓存模式 (default, reload, no-cache)
可以在 fetch 中传入 Request 对象实例:
var request = new Request('https://davidwalsh.name/users.json', {
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({'Content-Type': 'text/plain'})
});
// Now use it!
fetch(request).then(function() {/* handle response */});
可以自定义返回的 Response 对象实例,其中的 options 有:
- type – basic, cors
- url
- useFinalURL – 上面的 url 参数是不是最终的 URL
- status – 状态码 (ex: 200, 404, etc.)
- ok – 是否成功响应 (范围在 200-299)
- statusText – 状态码 (ex: OK)
- headers – 响应的 headers 对象
另外 Response 的实例还具备以下方法:
- clone() – 创建 Response 对象的克隆。
- error() – 返回与网络错误关联的新 Response 对象。
- redirect() – 使用不同的 URL 创建新响应。
- arrayBuffer() – 返回使用 ArrayBuffer 解析的 promise。
- blob() – 返回使用 Blob 解析的 promise。
- formData() – 返回使用 FormData 对象解析的 promise。
- json() – 返回使用 JSON 对象解析的 promise。
- text() – 返回使用 USVString(文本)解析的 promise。
// Create your own response for service worker testing
// new Response(BODY, OPTIONS)
var response = new Response('.....', {
ok: false,
status: 404,
url: '/'
});
// The fetch's `then` gets a Response instance back
fetch('https://davidwalsh.name/')
.then(function(responseObj) {console.log('status:', responseObj.status);
});
Service Worker 的使用
- 兼容低版本,注入 Cache API 的一个 polyfill,Service Worker 需要依赖 Cache API:
self.importScripts('./serviceworker-cache-polyfill.js');
- 注册 service worker:
if ('serviceWorker' in navigator) {navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope:', registration.scope);
}).catch(function(err) {
// registration failed :(console.log('ServiceWorker registration failed:', err);
});
}
上面的代码检查 service worker API 是否可用,如果可用,/sw.js 这个文件将会作为 service worker 被注册。
如果这个 service worker 已经被注册过,浏览器会自动忽略上面的代码。
有一个特别要注意是 service worker 文件的路径。你一定注意到,在这个例子中,service worker 文件被放在这个域的根目录下,这意味着 service worker 是跟网站同源的。换句话说,这个 service worker 将会获取到这个域下的所有 fetch 事件。如果 service worker 文件注册到 /example/sw.js,那么 service worker 只能收到 /example/ 路径下的 fetch 事件(比如:/example/page1/, /example/page2/)。
- 安装 service worker:
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(caches.open(CACHE_NAME)
.then(function(cache) {console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
上面代码声明了需要缓存的内容,如果所有的文件都缓存成功,service worker 就安装成功了。如果任何一个文件下载失败,那么安装步骤就会失败。这个方式依赖于你自己指定的资源,但这意味着,你需要非常仔细地确定哪些文件需要被缓存。指定了太多文件的话,会增加失败率。
- 对缓存跟返回请求的处理
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {return response;}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {return response;}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have 2 stream.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
如果我们想在缓存中添加新的请求缓存,可以通过处理 fetch 请求的 response,将其添加到缓存中即可。代码里我们做了以下事情:
添加一个 callback 到 fetch 请求的 .then 方法中。一旦我们获得一个 response,我们进行如下的检查:
1. 确保 response 有效
2. 检查 response 的状态是 200
3. 确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
如果检查通过会 clone 这个请求。这么做的原因是如果 response 是一个 Stream,那么它的 body 只能被消费一次。所以为了让浏览器跟缓存都使用这个 body,我们必须克隆这个 body,一份到浏览器,一份到缓存中缓存。
重新激活
你的 service worker 总会有要更新的时候。在那时,你需要按照以下步骤来更新:
- 更新你 service worker 的 JavaScript 文件 当用户浏览你的网站时,浏览器尝试在后台重新下载 service worker 的脚本文件。经过对比,只要服务器上的文件和本地文件有一个字节不同,这个文件就认为是新的。
- 之后更新后的 service worker 启动并触发 install 事件。此时,当前页面生效的依然是老版本的 service worker,新的 service worker 会进入“waiting”状态。
- 当页面关闭之后,老的 service worker 会被干掉,新的 servicer worker 接管页面 一旦新的 service worker 生效后会触发 activate 事件。通常来讲,需要在 activate 的 callback 中进行 cache 管理,来清理老的 cache。我们在 activate 而不是 install 的时候进行的原因,是如果我们在 install 的时候进行清理,那么老的 service worker 仍然在控制页面,他们依赖的缓存就失效了,因此就会突然被停止。
之前我们使用的缓存可以叫 my-site-cache-v1,我们想把这个拆封到多个缓存,一份给页面使用,一份给博客文章使用。这意味着,install 步骤里,我们要创建两个缓存:pages-cache-v1 和 blog-posts-cache-v1。在 activite 步骤里,我们需要删除旧的 my-site-cache-v1。
下面的代码会遍历所有的缓存,并删除掉不在 cacheWhitelist 数组(我们定义的缓存白名单)中的缓存。
self.addEventListener('activate', function(event) {var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];
event.waitUntil(caches.keys().then(function(cacheNames) {
return Promise.all(cacheNames.map(function(cacheName) {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);
}
})
);
})
);
});