作者: 魏聪 日期: 2020-10-20
我们知道,JavaScript 是一门单线程运行的语言,由于其单线程的特性,在我们执行代码的时候,只能按照顺序执行,所以容易造成阻塞,虽然人们极尽智慧,发明了事件循环这种改善代码执行顺序的方式,实现了某种程度上的异步,但还是没有改变其单线程的命运,而在 HTML5 之后,W3C 定义了一种可以在 JS 中创建多线程的接口:Worker,它允许主线程去创建一个 Worker 线程,当我们将主线程的一些任务分配给 Worker 线程,在主线程运行的同时,Worker 线程也在背后运行,待 Worker 线程运行结束之后,在把运行产生的结果返回给主线程。并且主线程的一些操作不会阻断 Worker 线程的运行,Worker 线程也不会干扰主线程的运行,这样当我们把一些计算量大的任务交给 Worker 线程时,而不是放在主线程执行时,整个过程就变得更加丝滑了。
Web worker 主要分为 Dedicated Worker(专用 worker)、Share Worker(共用 worker)和 Service Worker,我们大部分使用的是专用 Worker。
单一页面使用
1、兼容性
if (window.Worker) {
// pass
}
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 异常。
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();
});
4、注意事项
const buffer = new ArrayBuffer(10000);
worker.postMessage(buffer, [buffer]);
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');
6、应用场景
它可以使我们在多个页面(包括 iframes 和 workers)中使用同一个 worker,但必须保证这些都是同源的(相同的协议,主机和端口号),这样在我们多页面操作和共享同一份数据的时候非常有用。实际上共享 worker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程,当所有的页面都关闭之后该线程也会随之被结束。
1、兼容性
if (window.SharedWorker) {
// pass
}
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();
};
4、调试
打开 chrome://inspect/#workers 页面,点击 inspect,在这里面我们就可以调试 share-worker 了。
service worker 是 H5 的一个 API,可以用来做持久的离线缓存,它类似一个代理服务器,可以拦截和处理网络请求,它也是一个 web worker,但是是一个升级版,在这个 API 出来之前,我们可能使用 AppCache 来进行离线缓存,虽然 AppCache 使用上比 Service Worker 更加简单,但是它是有许多前置条件的,假定你遵循了一系列规则。而 service worker 则可以轻松覆盖原有 App Cache 的功能,并提供扩展支持,完美支持原生应用离线缓存方案,使你的应用更加丝滑。
1、兼容性
if ("serviceWorker" in navigator) {
// pass
}
2、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",
});
4、注意事项
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);
});
7、应用场景
8、调试和查看
chrome://inspect/#service-workers
service worker 信息
缓存的信息
service worker 调试设置
chrome://serviceworker-internals/?devtools