张鑫旭-鑫空间-鑫生活
it's my whole life!备份内容浏览
Service Worker那些事
姜天意
丰富的离线体验,定期的后台同步,推送通知,这些通常需要一个本地应用程序才能实现的功能现在已经来到了 web 端。以上这些特性背后所依赖的技术都会通过 service worker 提供。
Service Worker是什么?
Service Worker 是一个脚本,浏览器独立于当前网页,将其在后台运行,为实现一些不依赖页面或者用户交互的特性打开了一扇大门。在未来这些特性将包括推送消息,背景后台同步, geofencing(地理围栏定位),但它将推出的第一个首要特性,就是拦截和处理网络请求的能力,包括以编程方式来管理被缓存的响应。
这个 API 会让人兴奋的原因是,它允许你提供离线体验,而且是开发人员完全可控的离线体验。
在 service worker 之前,另一个叫做 APP Cache 的 api 也可以提供离线体验。APP Cache 的的主要问题是坑比较多,而且其被设计为只适合于单页 web 应用程序,对于传统的多页网站则不适合。service worker 的设计规避了这些痛点。
关于 service worker 的一些注意点:
- service worker 是一个JavaScript worker ,所以它不能直接访问 DOM 。相反, service worker 可以通过postMessage 接口与跟其相关的页面进行通信,发送消息,从而让这些页面在有需要的时候去操纵 DOM 。
- Service worker 是一个可编程的网络代理,允许你去控制如何处理页面的网络请求。
- Service worker 在不使用时将被终止,并会在需要的时候重新启动,因此你不能把onfetch 和 onmessage事件来作为全局依赖处理程序。如果你需要持久话一些信息并在重新启动Service worker后使用他,可以使用 IndexedDBAPI ,service worker 支持。
- Service worker 广泛使用了 promise ,所以如果不熟悉 promise ,那么你应该停止阅读这篇文章,看看 Jake Archibald 这篇文章。
Service Worker生命周期
Service worker 有一个独立于你的 web 页面的生命周期。
如果你需要在网站上安装 serice worker ,你需要通过页面中的 JavaScript 注册他,注册后浏览器会在后台安装 service worker。
在安装步骤中,通常需要缓存一些静态资源。如果所有的静态资源缓存成功,那么 service worker 就会安装成功。如果有任意文件不能下载和缓存,安装步骤将失败,service worker 不会激活(即不会安装)。如果发生这种情况,不要担心,会重试一次。这意味着如果 service woker 安装后,你要知晓你有一些静态资源已经被缓存了。
当我们安装后,会开始激活步骤。service worker 升级的这个过程也是一个很好的机会来处理老的缓存。
激活步骤后,service worker 将控制所有的页面,纳入它的范围,不过第一次在页面注册 service worker 时不会控制页面,直到它再次加载。一旦 service worker 在生效,它会处于两种状态之一里:要么 service worker 终止来节省内存,要么当页面发起网络请求后,它将处理请求获取和消息事件。
下面是一个 service worker 第一次安装时的生命周期。
开始之前
从这里获取 cache api 的polyfill coonsta/cache-polyfill · GitHub。这个 polyfill 支持 CacheStorage.match,Cache.add 和 Cache.addAll,而现在 Chrome M40 所实现的 Cache API 还没有支持这些方法。
把 dist/serviceworker-cache-polyfill.js 这个文件放到你的网站里,之后在 service worker 中,通过 importScripts 来加载。被 service worker 加载的脚本文件会被自动缓存住。
需要HTTPS
在开发阶段,你可以通过 localhost 来使用 service worker ,但是一旦部署到线上后,需要你的服务器开启 HTTPS 。
你可以通过 service worker 来劫持连接,伪造和过滤响应,这非常恐怖。即使你可以约束自己用在正途上,其他人可不一定这么想。所以为了避免这个风险,你只能在开启了 HTTPS 的网页上注册 service workers ,这样我们才可以防止加载 service worker 的时候不会被人篡改。
Github Pages 正好是 HTTPS 的,所以这是实验 service worker 的好地方。
如果你想要让你的服务器开启HTTPS,你需要为你的服务器获取一个TLS证书。不同的服务器安装方法不同,请阅读你服务器的帮助文档,并通过Mozilla’s SSL config generator了解最佳实践。
使用Service Worker
现在我们使用了 polyfill 并开启了 HTTPS,让我们看看究竟怎么使用 service worker 。
如何注册和安装service worker
要安装 service worker ,你需要在你的页面上注册。下面的代码会告诉浏览器你的 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/)。
现在你可以到 chrome://inspect/#service-workers 这里,检查 service worker 是否对你的网站启用了。
现在 service worker 第一版被实现后,你可以在 chrome://serviceworker-internals 中查看,这是非常有用的,通过它可以至关的看到 service worker 的生命周期,不过不要恐慌,这个功能将会被移到 chrome://inspect/#service-workers中。
你会发现这个功能可以非常方便地在一个模拟的窗口中测试你的 service worker ,你关闭后重新打开窗口后,老的 service worker 不会影响到你的新窗口。当窗口关闭后,任何创建于其中的注册跟缓存都将消失。
Service Worker的安装步骤
在你所控制的页面上完成 service worker 的注册步骤之后,让我们把注意力转到 service worker 的脚本中,我们要在其中完成它的安装。
下面简单的例子中,你需要为 install 事件定义一个 callback ,并确定哪些文件需要缓存。
// The files we want to cache
var urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
// Set the callback for the install step
self.addEventListener('install', function(event) {
// Perform install steps
});
在 install 的 callback 中,我们需要执行以下步骤:
- 开启一个缓存
- 缓存我们的文件
确定所有的资源是否要被缓存
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); }) ); });
上面的代码中,我们通过 caches.open 打开指定被 cache 的文件名,然后调用 cache.addAll ,将文件数组传入。这个过程是通过一连串 promise (caches.open 和 cache.addAll)完成的。event.waitUntil 会拿到一个 promise ,并使用其来获取安装耗费的时间以及是否安装成功。
如果所有的文件都缓存成功,service worker 就安装成功了。如果任何一个文件下载失败,那么安装步骤就会失败。这个方式依赖于你自己指定的资源,但这意味着,你需要非常仔细地确定哪些文件需要被缓存。指定了太多文件的话,会增加失败率。
上面只是一个简单的例子,你可以在 install 事件中执行其他操作,甚至忽略 install 事件。
怎样缓存和返回请求
你已经安装了 service worker ,现在应该考虑返回你缓存的请求?。
当 service worker 安装成功,并且用户浏览了另一个页面或刷新当前页面后,service worker 开始接收 fetch 事件。下面是一个例子:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
上面的代码中,event.respondWith 里我们定义 了fetch 事件,我们传入了一个 promise,由 caches.match 产生。caches.match 会对 request 进行查找,看这个请求,是否命中了之前 service worker 设定过的缓存。
若有一个命中的 response(请求结果),我们会把被缓存的值返回,否则,我们返回真实从网络请求 fetch 后的结果。上面的栗子很简单,会使用所有 install 步骤时被缓存的资源。
如果我们想在缓存中添加新的请求缓存,可以通过处理fetch请求的response,将其添加到缓存中即可,例如:
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;
}
);
})
);
});
代码里我们做了以下事情:
- 添加一个 callback 到 fetch 请求的 .then 方法中。
一旦我们获得一个 response,我们进行如下的检查:
- 确保 response 有效
- 检查 response 的状态是200
- 确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
如果检查通过会clone 这个请求。这么做的原因是如果 response 是一个 Stream,那么它的 body 只能被消费一次。所以为了让浏览器跟缓存都使用这个body,我们必须克隆这个 body,一份到浏览器,一份到缓存中缓存。
如何更新一个 Service Worker
你的 service worker 总会有要更新的时候。在那时,你需要按照以下步骤来更新:
更新你 service worker 的 JavaScript 文件
1.当用户浏览你的网站时,浏览器尝试在后台重新下载 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);
}
})
);
})
);
});
容错和坑
这部分内容是比较新鲜的,这是目前 service worker 存在的一个问题列表。希望这一节之后会被删掉,但现在,这些内容还要注意以下。
如果安装失败了,没有一个很好的方式来知晓
如果一个 worker 被注册了,但chrome://inspect/#service-workers或chrome://serviceworker-internals 里面没有,那么很可能因为抛异常安装失败,或者是一个 recject 掉的 promise 被传递给 event.waitUtil。
要解决这类问题,请去 chrome://serviceworker-internals 里面检查。打开开发者工具来调试,在 install 事件前面添加 debugger; 语句,这样通过“当出现未捕获的错误暂停”的功能你可以很方便的定位问题。
fetch api()目前仅支持Service Workers中使用
fetch未来会支持在页面中使用,但目前 Chrome 的实现只支持 service worker 来使用。cache API 也即将支持在页面上被使用,但目前为止, cache 还只能在 service worker 中用。
fetch()的默认值
当你使用 fetch ,默认请求不会带上 cookies 等身份信息,若需要带上身份信息,请这样调用:
fetch(url, {
credentials: 'include'
})
这样设计是有原因的,这样的设计也比 XHR 请求在同源下默认发送身份信息,但跨域时不带的设计要好。fetch的行为更像CORS请求,如,它默认不发送 cookies,除非你使用。
Non-CORS默认情况下会失败
默认情况下,从第三方 URL 跨域获取资源将会失败,除非对方支持 CORS。你可以通过在 Request 中添加 non-CORS 配置来避免失败。代价是如果这么做,会返回一个“未知”的 response ,你无法获取这个请求是成功还是失败。
cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
console.log('All resources have been fetched and cached.');
});
fetch()不遵循30x重定向
非常无语的是,fetch() 中不会被触发重定向,这是 Chrome 的 bug https://code.google.com/p/chromium/issues/detail?id=402389;
处理响应式图片
img 的 srcset 属性或 标签,在运行时会从浏览器或者网络中获取最适合的尺寸的图片。
在 service worker 中,你想要在 install 步骤中缓存一个图片资源,有以下几种选择:
- install 时缓存所有会被 元素或者 srcset 请求的图片资源。
- install 时缓存 low-res 版本的图片
- install时缓存 high-res 版本的图片
建议你选择方案2或3,因为如果把所有的图片都下载下来会造成内存浪费。
假如你在 install 时缓存了 low-res 版本,同时想在页面加载完成的时候尝试从网络上下载 high-res 的版本,同时如果 high-res 版本下载失败,使用 low-res 版本。这个想法 ok 也应该做,但是有一个问题:
如果我们有如下两种图片类型:
Screen DensityWidtHHeight1x4004002x800800HTML 代码如下:
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />
如果我们在 2x 的环境下,浏览器会下载 image-2x.png 。在离线情况下,你可以之前缓存的 image-src.png 来替代。尽管如此,由于现在的环境是 2x ,浏览器会把本来应该是400X400尺寸的图片显示成200X200,要避免这个问题,你需要在图片上设置一个修正的宽高。
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x"
style="width:400px; height: 400px;" />
<picture> 标签情况更复杂一些,难度取决于你是如何创建和使用的,但是可以通过与 srcset 类似的思路去解决。
URL Hash的Bug
在chrome M40 版本中有一个 bug,页面在改变 hash 时,service worker 会停止工作。你可以在这里找到更多信息: https://code.google.com/p/chromium/issues/detail?id=433708
了解更多
这里有一些相关文档:Service Worker Resources
获得帮助
如果遇到问题,请把你的问题po到Stackoverflow上,并加上’service-worker’标签,以便于我们跟进来尽可能帮助到你。
PS: 备份内容仅显示纯文字。