构建 Web 应用之 Service Worker 初探

这次体验一种新的博客风格,我们长话短说,针针见“血”。

构建 Web 应用之 Service Worker 初探_第1张图片

备马

在深入 Service Worker 之前,我们需要快速回顾如下基础。

诞生之初,JavaScript 是单线程的。

进程有私有的虚拟地址空间、代码、数据和其它系统资源,进程申请创建和使用的系统资源会随其终止而销毁。线程运行在进程之中,系统创建进程之后就开始启动执行进程的主线程,并随主线程的退出而终止。

JavaScript 作为浏览器脚本语言,为方便准确无误的操作 DOM,诞生之初便采用了单线程的方式。举个例子,若多线程同时分别删除和修改同一个 DOM,我们很难预知其执行结果。

但单线程中,必须通过异步和回调来优化耗时操作。

我们在网页上提交一个表单,并不希望在提交后页面卡顿,一直等待服务端返回的提交结果。这时我们需要能在单线程中发送异步请求,点击提交表单后可以先在页面进行其他操作。

Ajax 让我们可以向后端发送异步请求,同时不影响用户在界面中继续操作。当 Ajax 接收到服务端的响应之后,便通过回调函数执行之后的操作。一个典型的异步 Ajax 实战场景如下:

// 生成可发送同步/异步请求的 XMLHttpRequest 对象实例
var oReq = new XMLHttpRequest();
// open 方法初始化请求方法、地址,第三个参数 true 声明进行异步请求
oReq.open("GET", "http://www.jianshu.com/", true);
// 请求的整个过程中有五种状态,且同一时刻只能存在一种状态:
// 1. 未打开
// 2. 未发送
// 3. 已获取响应体
// 4. 正在下载响应体
// 5. 请求完成
// 当请求状态发生改变时,触发 onreadystatechange 会被调用
oReq.onreadystatechange = function (oEvent) {
  // 如果已经开始下载响应体了
  if (oReq.readyState === 4) {
    // 如果响应体成功下载,并且服务端返回 200 状态码
    if (oReq.status === 200) {
      // 打印响应信息
      console.log(oReq.responseText);
    } else {
      console.log("Error", oReq.statusText);
    }
  }
};
// send 方法发送请求,由于此请求是异步的,该方法立刻返回
oReq.send(null);

当我们的多个请求需要依赖于上一个请求的服务端响应时,回调函数中 Ajax 的层级逐步提高,可维护性极度下降,这就是回调地狱。

I Promise U that I`ll Marry U!!!

Promise 由 ES6 标准原生支持。正如题名,Promise 作出诺言,也要因此承担成功(fulfilled)或失败(rejected)的结果,以便解决回调地狱问题:

// 生成一个 Promise 实例,传入有特定的两个参数的匿名函数
// Promise 初始状态是 pending
// resolve 被调用时,将 Promise 状态改为成功(fulfilled)
// reject 被调用时,将 Promise 状态改为失败(rejected)
// 该匿名函数抛出错误时,Promise 状态为失败(rejected)
var a = new Promise(function(resolve, reject) {
  // setTimeout() 模拟异步请求,成功后执行 resolve() 方法
  setTimeout(function() {
      resolve('1')
  }, 2000)
})

a.then(function(val){
    // then() 有两个函数作为参数,onfulfilled 和 onrejected
    // 当 Promise 状态为 fulfilled 时,调用 then 的 onfulfilled 方法
    // 当 Promise 状态为 rejected 时,调用 then 的 onrejected 方法
    console.log(val)
    // then() 方法返回 Promise 对象实例,所以可被链式调用
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve('2')
      }, 2000)
    })
  })
  .then(function(val) {
    // 链式调用的第二个环节,处理上一个环节返回的 Promise 对象
    console.log(val)
  })

Promise 对象的生命周期如下图。

构建 Web 应用之 Service Worker 初探_第2张图片

除了异步编程,我们还可以有 Web Worker。

通过异步编程,我们的页面可以边响应用户的下一步操作边等待服务端的回应,不再拥有阻塞感,但 JavaScript 的单线程问题并没有得到相应的解决。通过 HTML 5 标准支持的 Web Worker,我们可以为 JavaScript 创建运行在后台的额外线程,并被多个页面共享。

在一个简单的 Web Worker 实例中,main.js 和 task.js 的源码如下。

// main.js
// 实例化 Worker 对象,其实质为新创建的工作线程在主线程的引用
var worker = new Worker("task.js")
// postMessage 方法与新创建的工作线程通信
worker.postMessage({
        id:1,
        msg:'Hello World'
});
// 当 Worker 线程返回数据时,onmessage 回调函数执行
worker.onmessage = function(message) {
    var data = message.data;
    console.log(JSON.stringify(data))
    // terminate 方法终止 worker 线程的运行
    worker.terminate()
};
// 当 Worker 线程出错时,onerror 回调函数执行
// error 参数中封装了错误对象的文件名、出错行号和具体错误信息
worker.onerror = function(error) {
    console.log(error.filename, error.lineno, error.message)
}
// task.js
onmessage = function(message) {
    var data = message.data
    data.msg = 'Hi from task.js'
    postMessage(data)
}

在 Chrome 浏览器里,以上代码必须运行在 Web 容器如 Apache 中。同时,WebKit 内核加载并执行 Worker 线程的流程如下图所示。

构建 Web 应用之 Service Worker 初探_第3张图片

上述知识点的详尽博客尽请期待,您可以先查阅其它资料进行补充。

冲锋

Service Worker 基于 Web Worker 事件驱动。

Service Worker 同样可以在浏览器后台挂起新线程,来缓解 JavaScript 的单线程问题。并且,我们可以用 Service Worker 拦截网络请求进行本地缓存或请求转发,相当于充当服务端与浏览器、浏览器与 Web 应用程序之间的代理服务器。

Service Worker 带来了速度,极大的提高了用户体验。

  • Service Worker 可有效加快重复访问网络应用的速度。
  • 拥有拦截请求、消息推送、静默更新、地理围栏等服务。
  • 可以在客户端通过 indexedDB API 保存持久化信息。

Service Worker 大量使用 Promise 对象。

因为通常 Service Worker 会等待响应后继续,并根据响应返回一个成功或者失败的操作。Promise 非常适合这种场景。

零、Service Worker 的生命周期。

所谓生命周期,包括 Service Worker 的注册、安装、激活、控制和销毁时的全部过程。我们需要对 Service Worker 的生命周期有所了解。

  • 先决条件:
    • 浏览器支持:Service Worker。
    • 在 localhost 域或 HTTPS 域下运行:介于我们能够通过使用 Service Worker 劫持连接、编撰以及过滤响应来进行权限较高的操作。
构建 Web 应用之 Service Worker 初探_第4张图片
  • 注册:注册过程独立于网页,先在页面执行注册,之后在浏览器后台启动安装步骤。
  • 安装:通常需要缓存某些静态资源。当所有文件已成功缓存,则安装完毕。如果任何文件下载失败或缓存失败,则安装失败,无法激活。
  • 激活:管理就缓存的绝佳机会。激活后它将会对作用域页面实时控制,不过首次注册该服务工作线程的页面需要再次加载才会受其控制。
  • 控制时:处于两种状态之一:
    • ①、终止以节省内存;
    • ②、监听获取 fetch 和消息 message 事件。
  • 销毁:由浏览器决定,因此尽量不要留存全局变量。
构建 Web 应用之 Service Worker 初探_第5张图片
初始安装时的简化声明周期

一、注册 Service Worker。

当浏览器对 Service Worker 提供原生支持时,我们便可以在页面加载后注册指定的 JavaScript 文件,并运行在后台线程之中,以下代码是这一过程的实例。




  ServiceWorker


  

Hello World!

这里通过 php 内置命令监听项目目录,便能看到 Service Worker 注册成功。同时,在 Chrome 浏览器里,可以访问 chrome://inspect/#service-workerschrome://serviceworker-internals/ 来检查 Service Worker 是否已经启用。

构建 Web 应用之 Service Worker 初探_第6张图片

二、安装 Service Worker。

安装阶段,我们可以执行任何任务。这里我们逐步打开缓存、缓存文件和确认所有需要的资产是否缓存。ServiceWorker.js 中的实例安装代码如下:

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);
      })
  );
});

这要求我们在与项目根目录下建立 main.jsmain.css 空文件。我们可以在 Chrome 开发者工具里的“Application”菜单的“Cache Storage”中看到相应的缓存。并且在图中的“Service Workers”选项卡中看到正在运行的 Service Workers。

且从上面的代码可以看到,通过 Service Worker 对象加载的文件拥有全局变量 caches 等,并且 self 关键字指向这个对象本身。cache 使我们可以存储网络响应发来的资源,并且根据它们的请求来生成 key。这个 API 和浏览器的标准的缓存工作原理很相似,且会持久存在,直到我们释放主动空间——我们拥有全部的控制权。

构建 Web 应用之 Service Worker 初探_第7张图片

三、激活 Service Worker。

当 Service Worker 安装成功后,便被激活,这时可实时控制作用域中的所有网站,进行缓存文件等操作。不过首次使用 Service Worker 的页面需要再次加载才会受其控制。

四、控制 Service Worker

以下列举几个常见的 Service Worker 应用场景。

1. 文件缓存

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // 以下方法检视请求,并从服务工作线程所创建的任何缓存中查找缓存的结果。
    caches.match(event.request)
      .then(function(response) {
        console.log(event.request)
        console.log(caches)
        // 如果发现匹配的响应,则返回缓存的值
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

通过上述文件缓存过程,我们可以告诉 Service Worker 如何使用这些缓存文件,并通过 fetch 事件来捕获。fetch 事件只会在浏览器准备请求 Service Worker 控制的资源时才会被触发。这些资源包括了指定的 scope 内的文档,和这些文档内引用的其他任何资源。

构建 Web 应用之 Service Worker 初探_第8张图片

2. 多页面传递消息

我们可以打开多个 https://nzv3tos3n.qnssl.com/message/msg-demo.html 测试页面来进行测试,效果如下。

构建 Web 应用之 Service Worker 初探_第9张图片

其中,index.js 源码为:

(function () {
    if (navigator.serviceWorker) {
        // 获取页面 DOM 元素
        var msgIpt = document.getElementById('ipt'),
            showArea = document.getElementById('show'),
            sendBtn = document.getElementById('sendBtn');

        navigator.serviceWorker.register('service-worker3.js');

        navigator.serviceWorker.addEventListener('message', function (event) {
            // 接受数据,并填充在 DOM 中
            showArea.innerHTML = showArea.innerHTML + ('
  • ' + event.data.message + '
  • '); }); sendBtn.addEventListener('click', function () { // 绑定点击事件,点击后发送数据 navigator.serviceWorker.controller.postMessage(msgIpt.value); msgIpt.value = ''; }); } })();

    3. 更新 Service Worker

    每次用户导航至使用 Service Worker 的站点时,浏览器会尝试在后台重新下载该脚本文件。这时新的 Service Worker 将会在后台安装,并在第二次访问时获取控制权,为了不与新的 Service Worker 缓存的文件冲突,我们可以使用类似 caches.open('v2') 语句来创建新的缓存目录。

    this.addEventListener('install', function(event) {
      event.waitUntil(
        // 创建新的缓存目录,并指定
        caches.open('v2').then(function(cache) {
          return cache.addAll([
            '/sw-test/',
            '/sw-test/index.html',
            …
          ]);
        });
      );
    });
    

    当新的 Service Worker 激活,记得删除 v1 缓存目录,代码如下。

    this.addEventListener('activate', function(event) {
      // 声明缓存白名单,该名单内的缓存目录不会被生成
      var cacheWhitelist = ['v2'];
      event.waitUntil(
        // 传给 waitUntil() 的 promise 会阻塞其他的事件,直到它完成
        // 确保清理操作会在第一次 fetch 事件之前完成
        caches.keys().then(function(keyList) {
          return Promise.all(keyList.map(function(key) {
            if (cacheWhitelist.indexOf(key) === -1) {
              return caches.delete(key);
            }
          }));
        })
      );
    });
    

    4. 预缓存

    Service Worker 也可以在后台主动发送请求,优化用户体验,图片来源于《饿了么的 PWA 升级实践》。

    5. Service Worker 支持的所有事件

    构建 Web 应用之 Service Worker 初探_第10张图片

    五、销毁 Service Worker

    浏览器决定是否销毁 Service Worker。在无痕浏览中,当页面关闭时相应的 Service Worker 会被销毁,因此尽量不要在代码中留存全局变量。可以访问 chrome://inspect/#service-workers和 chrome://serviceworker-internals/ 来检查 Service Worker 是否已经停用。

    小结

    困扰 Web 用户多年的难题——丢失网络连接,从 APPCache 到 Service Worker,解决办法一直在完善。Service Worker 开启的服务工作线程,对如何步入 Web 应用开发之旅,提供了很棒的切入角度。

    那么,如何从本文开始,更好的学习 Service Worker?结合更多其它技术博客与 Service Worker 的 API 文档会更好。本文图片素材、写作思路多取源于此。

    接口列表
    Cache CacheStorage
    Client Clients
    ExtendableEvent FetchEvent
    InstallEvent Navigator.serviceWorker
    NotificationEvent PeriodicSyncEvent
    PeriodicSyncManager PeriodicSyncRegistration
    ServiceWorker ServiceWorkerContainer
    ServiceWorkerGlobalScope ServiceWorkerRegistration
    SyncEvent SyncManager
    SyncRegistration WindowClient
    • Hello,我是韩亦乐,现任本科软工男一枚。软件工程专业的一路学习中,我有很多感悟,也享受持续分享的过程。如果想了解更多或能及时收到我的最新文章,欢迎订阅我的个人微信号:韩亦乐。我的个人主页中,有我的订阅号二维码和 Github 主页地址;我的知乎主页 中也会坚持产出,欢迎关注。
    • 本文内部编号经由我的 Github 相关仓库统一管理;本文可能发布在多个平台但仅在上述仓库中长期维护;本文同时采用【知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议】进行许可。

    你可能感兴趣的:(构建 Web 应用之 Service Worker 初探)