Service Worker 初体验
In 未分类 on 2016年01月06日 by view: 14,384
8

在 2014 年,W3C 公布了 service worker 的草案,service worker 提供了很多新的能力,使得 web app 拥有与 native app 相同的离线体验、消息推送体验。
service worker 是一段脚本,与 web worker 一样,也是在后台运行。作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与 web 交互行为。native app 可以做到离线使用、消息推送、后台自动更新,service worker 的出现是正是为了使得 web app 也可以具有类似的能力。

service worker 可以:

  1. 后台消息传递
  2. 网络代理,转发请求,伪造响应

  3. 离线缓存

  4. 消息推送

  5.  ... ...

本文以资源缓存为例,说明一下 service worker 是如何工作的。

生命周期

先来看一下一个 service worker 的运行周期


上图是 service worker 生命周期,出处 http://www.html5rocks.com/en/tutorials/service-worker/introduction/

图中可以看到,一个 service worker 要经历以下过程:

    1.   安装

    2.   激活,激活成功之后,打开 chrome://inspect/#service-workers 可以查看到当前运行的 service worker

        

    3. 监听 fetch 和 message 事件,下面两种事件会进行简要描述

    4. 销毁,是否销毁由浏览器决定,如果一个 service worker 长期不使用或者机器内存有限,则可能会销毁这个 worker

fetch 事件

在页面发起 http 请求时,service worker 可以通过 fetch 事件拦截请求,并且给出自己的响应。
w3c 提供了一个新的 fetch api,用于取代 XMLHttpRequest,与 XMLHttpRequest 最大不同有两点:

     1. fetch() 方法返回的是 Promise 对象,通过 then 方法进行连续调用,减少嵌套。ES6 的 Promise 在成为标准之后,会越来越方便开发人员。

      2.  提供了 Request、Response 对象,如果做过后端开发,对 Request、Response 应该比较熟悉。前端要发起请求可以通过 url 发起,也可以使用 Request 对象发起,而且 Request 可以复用。但是 Response 用在哪里呢?在 service worker 出现之前,前端确实不会自己给自己发消息,但是有了 service worker,就可以在拦截请求之后根据需要发回自己的响应,对页面而言,这个普通的请求结果并没有区别,这是 Response 的一处应用。

下面是在 http://www.sitepoint.com/introduction-to-the-fetch-api/中,作者利用 fetch api 通过 fliker 的公开 api 获取图片的例子,注释中详细解释了每一步的作用:

fetch api 与 XMLHttpRequest 相比,更加简洁,并且提供的功能更全面,资源获取方式比 ajax 更优雅。兼容性方面:chrome 42 开始支持,对于旧浏览器,可以通过官方维护的 polyfill 支持。

message 事件

页面和 serviceWorker 之间可以通过 posetMessage() 方法发送消息,发送的消息可以通过 message 事件接收到。

这是一个双向的过程,页面可以发消息给 service worker,service worker 也可以发送消息给页面,由于这个特性,可以将 service worker 作为中间纽带,使得一个域名或者子域名下的多个页面可以自由通信。

这里是一个小的页面之间通信 demohttps://nzv3tos3n.qnssl.com/message/msg-demo.html

利用 service workder 缓存文件

下面介绍一个利用 service worker 缓存离线文件的例子
准备 index.js,用于注册 service-worker

在上述代码中,注册了 service-worker.js 作为当前路径下的 service worker。由于 service worker 的权限很高,所有的代码都需要是安全可靠的,所以只有 https 站点才可以使用 service worker,当然 localhost 是一个特例。
注册完毕,现在开始写 service-worker.js 代码。
根据前面的生命周期图,在一个新的 service worker 被注册以后,首先会触发 install 事件,在 service-workder.js 中,可以通过监听 install 事件进行一些初始化工作,或者什么也不做。
因为我们是要缓存离线文件,所以可以在 install 事件中开始缓存,但是只是将文件加到 caches 缓存中,真正想让浏览器使用缓存文件需要在 fetch 事件中拦截

首先定义了需要缓存的文件数组 cacheFile,然后在 install 事件中,缓存这些文件。
evt 是一个 InstallEvent 对象, 继承自 ExtendableEvent,其中的 waitUntil() 方法接收一个 promise 对象,直到这个 promise 对象成功 resolve 之后,才会继续运行 service-worker.js。
caches 是一个 CacheStorage 对象,使用 open() 方法打开一个缓存,缓存通过名称进行区分。
获得 cache 实例之后,调用 addAll() 方法缓存文件。

这样就将文件添加到 caches 缓存中了,想让浏览器使用缓存,还需要拦截 fetch 事件

通过监听 fetch 事件,service worker 可以返回自己的响应。

首先检缓存中是否已经缓存了这个请求,如果有,就直接返回响应,就减少了一次网络请求。否则由 service workder 发起请求,这时的 service workder 起到了一个中间代理的作用。

service worker 请求的过程通过 fetch api 完成,得到 response 对象以后进行过滤,查看是否是图片文件,如果不是,就直接返回请求,不会缓存。

如果是图片,要先复制一份 response,原因是 request 或者 response 对象属于 stream,只能使用一次,之后一份存入缓存,另一份发送给页面。
这就是 service worker 的强大之处:拦截请求,伪造响应。fetch api 在这里也起到了很大的作用。

 

service worker 的更新很简单,只要 service-worker.js 的文件内容有更新,就会使用新的脚本。但是有一点要注意:旧缓存文件的清除、新文件的缓存要在 activate 事件中进行,因为可能旧的页面还在使用之前的缓存文件,清除之后会失去作用。

 

在初次使用 service worker 的过程中,也遇到了一些问题,下面是其中两个

问题 1. 运行时间

service worker 并不是一直在后台运行的。在页面关闭后,浏览器可以继续保持 service worker 运行,也可以关闭 service worker,这取决与浏览器自己的行为。所以不要定义一些全局变量,例如下面的代码 (来自 https://jakearchibald.com/2014/service-worker-first-draft/):

返回的结果可能是没有规律的:1,2,1,2,1,1,2....,原因是 hitCounter 并没有一直存在,如果浏览器关闭了它,下次启动的时候 hitCounter 就赋值为 0 了
这样的事情导致调试代码困难,当你更新一个 service worker 以后,只有在打开新页面以后才可能使用新的 service worker,在调试过程中经常等上一两分钟才会使用新的,比较抓狂。

问题 2. 权限太大

当 service worker 监听 fetch 事件以后,对应的请求都会经过 service worker。通过 chrome 的 network 工具,可以看到此类请求会标注:from service worker。如果 service worker 中出现了问题,会导致所有请求失败,包括普通的 html 文件。所以 service worker 的代码质量、容错性一定要很好才能保证 web app 正常运行。

 

参考文章:

 1. http://www.html5rocks.com/en/tutorials/service-worker/introduction/

 2. http://www.sitepoint.com/introduction-to-the-fetch-api/

 3. https://developer.mozilla.org/en-US/docs/Web/API/InstallEvent

 4. https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent

 5. https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage

 

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2016/01/9274/

  1. 达达达 2017 年 10 月 9 日

    if (!response && response.status !== 200 && !response.headers.get(‘Content-type’).match(/image/)) {
    return response;
    }

    这句话没有问题?

  2. 刘欢欢HapLeo 2017 年 1 月 12 日

    不错。在看 lodash 的文档,发现他们用到了 Service Worker。。

  3. 来利强 2016 年 10 月 18 日

    求解答可以缓存网络请求吗?

  4. honghe 2016 年 4 月 19 日

    “ 如果是图片,要先复制一份 response,原因是 request 或者 response 对象属于 stream,只能使用一次,之后一份存入缓存,另一份发送给页面。”
    stream 只能使用一次,这个能否再详述技术原理、原因,谢谢!

    • 曾田生 2018 年 3 月 25 日

      我也注意到了这一句,应该是为了释放内存吧,使用 response 对象同时也吧 response 的内存给释放掉,
      不然实际线上环境那么多的网络请求,每次都把 response 数据缓存起来,那服务器或浏览器内存不得崩掉。
      当然纯属个人见解,还需要查阅相关文档加以说明

  5. 巧映露 2016 年 1 月 25 日

    此贴有意思~

  6. 周含梅 2016 年 1 月 23 日

    看看吧,大家都会支持你

  7. kazaff 2016 年 1 月 17 日

    学到了

发表评论到 honghe