# Web Worker

作者: 魏聪 日期: 2020-10-20

# 1、前言

我们知道,JavaScript 是一门单线程运行的语言,由于其单线程的特性,在我们执行代码的时候,只能按照顺序执行,所以容易造成阻塞,虽然人们极尽智慧,发明了事件循环这种改善代码执行顺序的方式,实现了某种程度上的异步,但还是没有改变其单线程的命运,而在 HTML5 之后,W3C 定义了一种可以在 JS 中创建多线程的接口:Worker,它允许主线程去创建一个 Worker 线程,当我们将主线程的一些任务分配给 Worker 线程,在主线程运行的同时,Worker 线程也在背后运行,待 Worker 线程运行结束之后,在把运行产生的结果返回给主线程。并且主线程的一些操作不会阻断 Worker 线程的运行,Worker 线程也不会干扰主线程的运行,这样当我们把一些计算量大的任务交给 Worker 线程时,而不是放在主线程执行时,整个过程就变得更加丝滑了。

# 2、介绍

Web worker 主要分为 Dedicated Worker(专用 worker)、Share Worker(共用 worker)和 Service Worker,我们大部分使用的是专用 Worker。

# 2.1 专用 Worker

单一页面使用
1、兼容性

if (window.Worker) {
  // pass
}
1
2
3


2、API 介绍

// jsUrl必须同源且必须是js文件
// options,可选,type、name、credentials
// type:用以指定 Worker 类型的 DOMString 值. 该值可以是 classic 或 module。如果未指定,将使用默认值 classic。
// credentials:用以指定 worker 凭证的 DOMString 值。该值可以是 omit,same-origin 或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证)。
// name:在 DedicatedWorkerGlobalScope 的情况下,用来表示 Worker 的 scope 的一个 DOMString 值。
const myWorker = new Worker(jsUrl, options);
// 实例属性
Worker.onerror
Worker.onmessage
Worker.onmessageerror // 发送的数据无法序列化成字符串时,会触发这个事件。
Worker.postMessage()
Worker.terminate()
----------------------------------------------------------------------------------------------------
// worker.js 内部API this self
self.name
self.onmessage
self.onmessageerror // 发送的数据无法序列化成字符串时,会触发这个事件。
self.close()
self.postMessage()
self.importScripts()

// 错误类型 error
当 document 不被允许启动 worker 的时候,将抛出一个 SecurityError 异常。比如:如果提供的 URL 有语法错误,或者与同源策略相冲突(跨域访问)。
如果 worker 的 MIME 类型不正确,将抛出一个 NetworkError 异常。worker 的 MIME 类型必须是 text/javascript。
如果 URL 无法被解析(格式错误),将抛出一个 SyntaxError 异常。
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

3、基本使用

// main.js
const worker = new Worker('work.js');
worker.postMessage("hello, worker.js");
worker.onmessage = (event) => {
	const data = event.data;
  // pass
  ...
  // 主线程关闭Worker
  worker.terminate();
};
-----------------------------------------------------------------------------------------------------
// worker.js
self.addEventListener("message",(event) => {
  const data = event.data;
  self.postMessage("message","hello, main.js");
  // pass
  ...
  // 子线程关闭Worker
	self.close();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

4、注意事项

  • 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源,而且不能打开本机文件(file://),加载脚本只能来源于网络。
  • Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document、window、parent、alert、confirm 这些对象。但是 Worker 线程可以访问 fetch、XMLHttpRequest、Promise、setTimeout(clearTimeout...)、CustomEvent、Cache、FileReader、WebSocket、indexedDB、navigator、location 对象。
  • Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
  • Worker 在传递值是通过值拷贝的方式,并且他也支持二进制数据的传递,但是通过拷贝方式发送二进制容易造成性能问题,所以 JS 允许主线程直接将二进制数据转移给子线程,一旦转移,主线程就无法使用这些数据了。
const buffer = new ArrayBuffer(10000);
worker.postMessage(buffer, [buffer]);
1
2

5、实例

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}
var pollingWorker = createWorker(function (e) {
  var cache;
  function compare(new, old) { ... };
  setInterval(function () {
    fetch('/api').then(function (res) {
      var data = res.json();
      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});
pollingWorker.onmessage = function () {
  // render data
}
pollingWorker.postMessage('init');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

6、应用场景

  • 预先抓取和/或缓存数据以便稍后使用
  • 突出显示代码语法或其他实时文本格式
  • 拼写检查程序
  • 分析视频或音频数据
  • 背景 I/O 或网络服务轮询
  • 处理较大数组或超大 JSON 响应
  • canvas 中的图片过滤
  • 更新本地网络数据库中的多行内容

# 2.2 共享 worker

它可以使我们在多个页面(包括 iframes 和 workers)中使用同一个 worker,但必须保证这些都是同源的(相同的协议,主机和端口号),这样在我们多页面操作和共享同一份数据的时候非常有用。实际上共享 worker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程,当所有的页面都关闭之后该线程也会随之被结束。
1、兼容性

if (window.SharedWorker) {
  // pass
}
1
2
3


2、API 介绍
api 基本跟专有 worker 差不多的。
3、基本用法

// main.js
const sharedWorker = new SharedWorker("./worker.js");
// 第一种使用方式,addEventListener需要start方式激活
sharedWorker.port.addEventListener("message", (e) => {
  const data = e.data;
  // pass
});
sharedWorker.port.postMessage("hello, worker.js");
sharedWorker.port.start();

// 第二种使用方式,onmessage不需要使用start方法激活
sharedWorker.port.onmessage = () => {
  const data = e.data;
  // pass
};
sharedWorker.postMessage("hello, worker.js");
------------------------------------------------------------------------------------------------------// worker.js
onconnect = (e) => {
  const port = e.ports[0];
  // 第一种方式,onmessage不需要start激活
  port.onmessage = () => {
    port.postMessage("hello, main.js");
  };
  // 第二种方式,addEventListener需要start方式激活
  port.addEventListener("message", () => {
    port.postMessage("hello, main.js");
  });
  port.start();
};
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

4、调试
打开 chrome://inspect/#workers 页面,点击 inspect,在这里面我们就可以调试 share-worker 了。

# 2.3 service worker

service worker 是 H5 的一个 API,可以用来做持久的离线缓存,它类似一个代理服务器,可以拦截和处理网络请求,它也是一个 web worker,但是是一个升级版,在这个 API 出来之前,我们可能使用 AppCache 来进行离线缓存,虽然 AppCache 使用上比 Service Worker 更加简单,但是它是有许多前置条件的,假定你遵循了一系列规则。而 service worker 则可以轻松覆盖原有 App Cache 的功能,并提供扩展支持,完美支持原生应用离线缓存方案,使你的应用更加丝滑。
1、兼容性

if ("serviceWorker" in navigator) {
  // pass
}
1
2
3

2、Service Worker 的流程

  • service worker 注册
  • 执行 worker 脚本
  • 进入 install 阶段(它只发生一次,除非重新更新 worker),缓存给定列表的文件,当所有文件都缓存成功,你就可以从缓存中拿到那些文件,如果其中某个文件缓存失败,则 worker 脚本停止运行(所以不宜缓存太多的静态资源),如果老版本的缓存存在运行,则老版本缓存不受影响。
  • install 成功,进入 activate 阶段,在 active 阶段,它可以更新缓存,使 旧版的缓存失效,新版的缓存生效,如果 install 失败,那么不会进入 activate 阶段,fetch 和 push 等事件也不会有响应
  • 在 service worker 变成 activate 状态之后,service worker 就会控制整个作用域网页,它此时有两种状态:1、worker 终止以节省内存。2、处理获取和消息事件。从页面发出网络请求或消息则触发后一种状态,比如用户访问作用域的其他网页或者刷新页面,它就会触发 fetch 事件,此时请求相当于被代理,它首先会被 service worker 处理,之后看情况返回缓存信息还是继续发送请求。

3、API 介绍

// main.js
// 访问ServiceWorker
const ServiceWorker = navigator.serviceWorker;
// 注册一个ServiceWorker
// jsUrl跟worker一样,需要遵循同源和非file://协议
ServiceWorker.register("jsUrl", { scope: "scopeUrl" });
// 当 SW controlling 变化时被触发,比如新的 SW skippedWaiting 成为一个新的被激活的 SW
ServiceWorker.addEventListener("controllerchange", () => {
  // pass
});
// 接受消息
ServiceWorker.addEventListener("message", (data) => {
  if (data.source == "service-worker") {
    console.log(data.msg);
  }
});
// 获取所有安装的service worker
ServiceWorker.getRegistrations();
// 手动卸载service worker
registration.unregister();
// 获取service worker scope
registration.scope;
--------------------------------------------------------------------------------------------------------------------------------// worker.js
// service worker events
// install进行缓存文件,缓存成功的话进入activate阶段,失败则不能进去activate阶段,下次加载,浏览器会再次尝试安装
this.addEventListener("install", () => {});
// activate主要处理以前的缓存
this.addEventListener("activate", () => {});
// 拦截资源获取
this.addEventListener("fetch", () => {});
// 拦截消息推送
this.addEventListener("push", () => {});
// 获取所有客户端
this.clients;
// 发送消息
client.postMessage({
  msg: "hello, main.js",
  source: "service-worker",
});
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
33
34
35
36
37
38
39

4、注意事项

  • service worker 本质上还是一个 worker,worker 有的特点他都有
  • service worker 在不用时会被中止,在下次作用域内有发生获取和消息事件时重启,所以你不能依赖 onfetch 和 onmessage 来处理程序的全局状态,如果存在需要持续保存并在重启后重新使用的信息,可以通过访问 indexedDB 来进行保存。
  • service worker 在线上出于安全考虑需要给服务器设置 https,而在开发阶段,可以通过 localhost 来使用 service worker
  • 执行 service worker 需要耗费资源的,尤其是在 install 阶段需要下载和缓存资源,这样会占用网络资源,影响首屏展示,应该延迟 service worker 的注册时间,所以一般在 onload 之后进行 register service worker。
  • 默认情况下,在安装 service worker 之后,你需要刷新网页才能看到效果,但是可以通过使用 clients.claim()改变这种行为,可以控制未控制的页面,这种也可能失效,仅当你当前 worker 激活并且调用 claim()在资源加载之前才会生效。
  • 在整个过程中作用域页面始终只由一个 Service worker 控制,或者没有,并且你的网站只有一个 service worker 版本在运行。
  • Service Worker 安装之后会一直存在,除非你手动卸载
  • 调用 register,它的参数 url 发生更改,会触发 service worker 更新

5、升级 service worker 版本
我们一般推荐直接改动 worker.js 脚本代码,而不是改变脚本 url,具体看这里。我们改动 worker.js 代码之后,当用户重新导航到你的站点,浏览器会在后台尝试下载 worker 脚本,如果脚本与当前脚本存在字节差异,那么会视为新的 service worker,新的 worker 将会执行,并且触发 install 事件,但是此时旧的 service worker 仍控制着界面,因此新的 service worker 进入 wating 阶段(这样可以保证每次只运行一个 service worker 版本),直到旧的 worker 控制 0 个客户端(所以你要获取新的更新需要关闭之前当前 service worker 的所有标签),旧的 service worker 将被终止,新的 service worker 取得网站控制权(你可以通过 navigator.serviceWorker.controller 检测客户端是否受控制,客户端指页面、workers、shared workers),此时触发 activate 事件。当然你可以使用 self.skipWaiting()来跨越 wait 阶段,它会将当前活动的 service worker 逐出,install 成功之后就立即激活。
6、基本使用

// main.js
// 注册worker
if ("serviceWorker" in navigator) {
  window.addEventListener("load", function () {
    navigator.serviceWorker.register("/worker.js").then((res) => {
      reg.scope; // 注册作用域
      // 下面是一些监控api
      reg.installing; // the installing worker, or undefined
      reg.waiting; // the waiting worker, or undefined
      reg.active; // the active worker, or undefined
      reg.addEventListener("updatefound", () => {
        // A wild service worker has appeared in reg.installing!
        const newWorker = reg.installing;
        newWorker.state;
        // "installing" - the install event has fired, but not yet complete
        // "installed"  - install complete
        // "activating" - the activate event has fired, but not yet complete
        // "activated"  - fully active
        // "redundant"  - discarded. Either failed install, or it's been
        //                replaced by a newer version
        newWorker.addEventListener("statechange", () => {
          // newWorker.state has changed
        });
      });
    });
  });
}

// worker.js
// 添加缓存列表
self.addEventListener("install", (event) => {
  let CACHE_NAME = "v1";
  let urlsToCache = ["/", "/styles/main.css", "/scripts/bundle.js"];
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache))
  );
});
// 移除旧缓存
self.addEventListener("activate", function (event) {
  const cacheWhitelist = ["v2"];
  event.waitUntil(
    caches.keys().then(function (keyList) {
      return Promise.all(
        keyList.map(function (key) {
          if (cacheWhitelist.indexOf(key) === -1) {
            return caches.delete(key);
          }
        })
      );
    })
  );
});
// 拦截资源获取
self.addEventListener("fetch", function (event) {
  const CACHE_NAME = "v2";
  event.respondWith(
    caches.match(event.request).then(function (response) {
      // 资源存在缓存,直接返回
      if (response) {
        return response;
      }
      // 请求是流,只能被消费一次,需要克隆
      const fetchRequest = event.request.clone();
      return fetch(fetchRequest).then(function (response) {
        // 无效请求不更新或做缓存
        if (!response || response.status !== 200 || response.type !== "basic") {
          return response;
        }
        // 响应是流,只能被消费一次,需要克隆
        const responseToCache = response.clone();
        // 修改或直接添加缓存
        caches.open(CACHE_NAME).then(function (cache) {
          cache.put(event.request, responseToCache);
        });
        return response;
      });
    })
  );
});
// 拦截push
// 详细用法参见:https://serviceworke.rs/push-clients_service-worker_doc.html
self.addEventListener("push", function (event) {
  const analyticsPromise = pushReceivedTracking();
  const pushInfoPromise = fetch("/api/get-more-data")
    .then(function (response) {
      return response.json();
    })
    .then(function (response) {
      const title = response.data.userName + " says...";
      const message = response.data.message;
      self.registration.showNotification(title, {
        body: message,
      });
    });
  const promiseChain = Promise.all([analyticsPromise, pushInfoPromise]);
  event.waitUntil(promiseChain);
});
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

7、应用场景

  • 后台数据同步
  • 响应来自其它源的资源请求
  • 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
  • 在客户端进行 CoffeeScript,LESS,CJS/AMD 等模块编译和依赖管理(用于开发目的)
  • 后台服务钩子
  • 自定义模板用于特定 URL 模式
  • 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片

8、调试和查看
chrome://inspect/#service-workers
service worker 信息

缓存的信息

service worker 调试设置
chrome://serviceworker-internals/?devtools

# 3、参考链接