提起 「worker」 的話,你能想起什麼來呢 --javascript
是「我們工人有力量」?php
仍是「伐伐伐...伐木工」?css
固然,稍有經驗的開發者可能已經從標題猜出,今天真正要說的是 -- JavaScript 中的 worker 們:html
在 HTML5 規範中提出了工做線程(Web Worker
)的概念,容許開發人員編寫可以脫離主線程、長時間運行而不被用戶所中斷的後臺程序,去執行事務或者邏輯,並同時保證頁面對用戶的及時響應。java
Web Worker
又分爲 Dedicated Worker
和 SharedWorker
。node
隨後 ServiceWorker
也加入進來,用於更好的控制緩存和處理請求,讓離線應用成爲可能。ios
先來複習一下基礎知識:laravel
傳統頁面中(HTML5 以前)的 JavaScript 的運行都是以單線程的方式工做的,雖然有多種方式實現了對多線程的模擬(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),可是在本質上程序的運行仍然是由 JavaScript 引擎以單線程調度的方式進行的。git
爲了不多線程 UI 操做的衝突(如線程1要求瀏覽器刪除DOM節點,線程2卻但願修改這個節點的某些樣式風格),JS 將處理用戶交互、定時執行、操做DOM樹/CSS樣式樹等,都放在了 JS 引擎的一個線程中執行。es6
從 2008 年 W3C 制定出第一個 HTML5 草案開始,HTML5 承載了愈來愈多嶄新的特性和功能。它不但強化了 Web 系統或網頁的表現性能,並且還增長了對本地數據庫等 Web 應用功能的支持。
隨之而來的,還有上面提到的幾種 worker
,首先解決的就是多線程的問題。
那麼,來看看解決線程問題的東西爲何叫 worker
,這來源於一種設計模式:
Master-Worker模式是經常使用的並行設計模式。其核心思想是:系統有兩個進程協同工做:Master進程和Worker進程。Master進程負責接收和分配任務,Worker進程負責處理子任務。當各個Worker進程將子任務處理完後,將結果返回給Master進程,由Master進行概括和彙總,從而獲得系統結果
Node 的內置模塊 cluster,能夠經過一個主進程管理若干子進程的方式來實現集羣的功能
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) { //主進程
console.log(`Master ${process.pid} is running`);
//分配子任務
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else { //子進程
//應用邏輯根本不須要知道本身是在集羣仍是單邊
//每一個HTTP server都能監聽到同一個端口
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
複製代碼
運行 node server.js
後,輸出:
Master 3596 is running
Worker 4324 started
Worker 4520 started
Worker 6056 started
Worker 5644 started
複製代碼
在 HTML5 中,
Web Worker
的出現使得在 Web 頁面中進行多線程編程成爲可能
HTML5 中的多線程是這樣一種機制:它容許在 Web 程序中併發執行多個 JavaScript 腳本,每一個腳本執行流都稱爲一個線程,彼此間上下文互相獨立,而且由瀏覽器中的 JavaScript 引擎負責管理
HTML5 規範列出了 Web Worker
的三大主要特徵:
HTML5 中的 Web Worker
能夠分爲兩種不一樣線程類型,一個是專用線程 Dedicated Worker
,一個是共享線程 Shared Worker
。
專用線程是指標準 worker,一個專用 worker 僅僅能被生成它的腳本所使用
也就是說,所謂的專用線程(dedicated worker)並無一個顯示的DedicatedWorker
構造函數,其實指的就是普通的Worker
構造函數。
🌰 在解釋概念前,先來看一個唄兒簡單的小栗子:
//myWorker.js
self.onmessage = function(event) {
var info = event.data;
self.postMessage(info + " from worker!");
};
複製代碼
//主頁面
<input type="text" name="wkInput1" />
<button id="btn1">test!</button>
<script> if (window.Worker) { const myWorker = new Worker("myWorker.js"); myWorker.onmessage = function (event) { alert(event.data); }; const btn = document.querySelector('#btn1'); btn.addEventListener('click', e=>{ const ipt = document.querySelector('[name=wkInput1]'); const info = "hello " + ipt.value; myWorker.postMessage(info); }); } </script>
複製代碼
很顯然,運行的效果無非是點擊按鈕後彈出包含文本框內容的字符串。
例子很簡單,但攜帶的關鍵信息還算豐富,那麼結合規範中的一些定義來看一看上面的代碼:
首先是專用 worker 在運行的過程當中,會隱式的使用一個MessagePort
對象,其接口定義以下:
interface MessagePort {
void postMessage(message, optional transfer = []);
attribute onmessage;
void start();
void close();
attribute onmessageerror;
};
複製代碼
咱們把最重要的兩個成員放在了前面,一個是postMessage()
方法,另外一個是onmessage
屬性。
postMessage()
方法用來發送數據:第一個參數除了能夠發送字符串,還能夠發送 JS 對象(有的瀏覽器須要JSON.stringify()
);可選的第二個參數可用來發送 ArrayBuffer 對象數據(一種二進制數組,配合XHR、File API、Canvas等讀取字節流數據用)onmessage
屬性應被指定一個事件處理函數,用於接收傳遞過來的消息;也能夠選擇使用 addEventListener 方法,其實現方式和做用和 onmessage 相同而後來看看簡化後的 Worker
的定義:
interface AbstractWorker {
attribute onerror;
};
複製代碼
interface Worker {
Constructor(scriptURL, optional workerOptions);
void terminate();
void postMessage(message, optional transfer = []);
attribute onmessage;
attribute onmessageerror;
};
Worker implements AbstractWorker;
複製代碼
AbstractWorker
接口,也就是說有一個onerror
回調用來管理錯誤;myWorker.onerror = function(event){
console.log(event.message);
console.log(event.filename);
console.log(event.lineno);
}
複製代碼
MessagePort
接口,能夠 postMessage/onmessage
;terminate()
方法去終止該線程經過workerOptions 中的選項能夠支持 es6 模塊化等,這裏不展開論述
至此,已經能夠理解「主頁面」中的各類定義和調用行爲了;而"myWorker.js"中的self
又是怎樣的呢,繼續來看看相關定義:
interface WorkerGlobalScope {
readonly attribute self; //WorkerGlobalScope
readonly attribute location;
readonly attribute navigator;
void importScripts(urls);
attribute onerror;
attribute onlanguagechange;
attribute onoffline;
attribute ononline;
attribute onrejectionhandled;
attribute onunhandledrejection;
};
複製代碼
interface DedicatedWorkerGlobalScope {
readonly attribute name;
void postMessage(
message,
optional transfer = []
);
void close();
attribute onmessage;
attribute onmessageerror;
};
複製代碼
專用 worker 實現了以上兩個接口,可知:
worker
中的全局對象就是其自己WorkerGlobalScope
的 self
只讀屬性來得到這個對象自己的引用MessagePort
接口方法。看起來很簡單,兩邊均可以 postMessage/onmessage
,就能夠愉快的通訊了。
除了上述這些,其餘的一些要點包括:
在現代瀏覽器和移動端上,能夠說專用 worker 已經被支持的不錯了:
共享線程指的是一個能夠被多個頁面經過多個鏈接所使用的 worker
🌰 仍是先看一個栗子:
//wk.js
var arr = [];
self.onconnect = function(e) {
var port = e.ports[0];
port.postMessage('hello from worker!');
port.onmessage = function(evt) {
var val = evt.data;
if (!~arr.indexOf(val)) {
arr.push(val);
}
port.postMessage(arr.toString());
}
}
複製代碼
<!DOCTYPE html>
<html><body>
page 1
<script> if (window.SharedWorker) { var wk = new SharedWorker('wk.js'); wk.port.onmessage = function(e) { console.log(e.data); } wk.port.postMessage(1); } //輸出 //hello from worker! //1 </script>
</body></html>
複製代碼
<!DOCTYPE html>
<html><body>
page 2
<script> if (window.SharedWorker) { var wk = new SharedWorker('wk.js'); wk.port.onmessage = function(e) { console.log(e.data); } wk.port.postMessage(2); } //輸出 //hello from worker! //1,2 </script>
</body></html>
複製代碼
運行效果也不難理解,引用共享 worker 的兩個同域的頁面,共享了其中的 arr 數組。也就是說,專用 worker 一旦被某個頁面引用,該頁面就擁有了一個獨立的子線程上下文;與之不一樣的是,某個共享 worker 腳本文件若是被若干頁面(要求是同源的)引用,則這些頁面會共享該 worker 的上下文,擁有共同影響的變量等。
interface SharedWorker {
Constructor(
scriptURL,
optional (DOMString or WorkerOptions) options
);
readonly attribute port;
};
SharedWorker implements AbstractWorker;
複製代碼
另外一個很是大的區別在於,前面也提到過,與一個專用 worker 通訊,對MessagePort
的實現是隱式進行的(直接在 worker 上進行postMessage/onmessage
);而共享 worker 必須經過端口(MessagePort
類型的worker.port
)對象進行。
此外的幾個注意點:
var wk = new SharedWorker('wk.js', 'foo');
//or
var wk = new SharedWorker('wk.js', {name: 'foo'});
複製代碼
interface SharedWorkerGlobalScope {
readonly attribute name;
void close();
attribute onconnect;
};
複製代碼
self.name
得到self.onconnect = function(e) {
var port = e.ports[0];
port.postMessage('hello from worker!');
//...
}
複製代碼
var wk = new SharedWorker('wk.js');
wk.port.onmessage = function(e) {
console.log(e.data);
}
//-->
wk.port.addEventListener('message', function(e) {
console.log(e.data);
});
wk.port.start();
複製代碼
addEventListener
代替onmessge
,則須要額外調用 start()
方法才能創建鏈接移動端尚不支持、IE11/Edge也沒戲;測試時 Mac 端的 chrome/firefox 也是情況頻頻沒法成功,最後在 chrome@win10 以及 opera@mac 才能夠
Service Worker 基於 Web Worker 的事件驅動,提供了用來管理安裝、版本、升級的一整套系統。
專用 worker 或共享 worker 專一於解決 「耗時的 JS 執行影響 UI 響應」 的問題, -- 一是後臺運行 JS,不影響主線程;二是使用postMessage()/onmessage
消息機制實現了並行。
而 service worker 則是爲解決 「由於依賴並容易丟失網絡鏈接,從而形成 Web App 的用戶體驗不如 Native App」 的問題而提供的一系列技術集合;它比 web worker 獨立得更完全,能夠在頁面沒有打開的時候就運行。
而且相比於已經被廢棄的 Application Cache 緩存技術:
<html manifest="appcache.manifest">
...
</html>
複製代碼
CACHE MANIFEST
# appcache.manifest text file, version: 0.517
NETWORK:
#CACHE:
assets/loading.gif
assets/wei_shop_bk1.jpg
assets/wei_shop_bk2.jpg
assets/wei_ios/icons.png
assets/wei_ios/icon_addr.png
assets/wei_ios/icon_tel.png
NETWORK:
scripts/wei_webapp.js
styles/meishi_wei.css
複製代碼
service worker 擁有更精細、更完整的控制;做爲一個頁面與服務器之間代理中間層,service worker 能夠捕獲它所負責的頁面的請求,並返回相應資源,這使離線 web 應用成爲了可能。
🌰 一如既往的先看一個直觀的小栗子:
<!--http://localhost:8000/service.html-->
<h1>hello service!</h1>
<img src="deer.png" />
<script> if (navigator.serviceWorker) { window.onload = function() { navigator.serviceWorker.register( 'myService.js', {scope: '/'} ).then(registration=>{ console.log('SW register OK with scope: ', registration.scope); registration.onmessage = function(e) { console.log(e.data) } }).catch(err=>{ console.log('SW register failed: ', err); }); } // SW register OK with scope: http://localhost:8000/ } </script>
複製代碼
//myService.js
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/styles/main.css',
'/script/main.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname == '/deer.png') {
event.respondWith(
fetch('/horse.jpg').catch(ex=>console.log(ex))
);
} else {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
}
});
複製代碼
結合這個簡單的示例,來梳理一下其中反映出的信息:
和其餘兩種 worker 不一樣的是,service worker 中的各項技術普遍地利用了 Promise
Promises 是一種很是適用於異步操做的機制,一個操做依賴於另外一個操做的成功執行。這也成爲了 service worker 的通用工做機制
Response 的構造函數容許建立一個自定義的響應對象:
new Response('<p>Hello from service worker!</p>', {
headers: { 'Content-Type': 'text/html' }
})
複製代碼
但更常見的是:經過其餘的 API 操做返回了一個 Response 對象,例如一個 service worker 的 event.respondWith
,或者一個簡單的 fetch()
在 service worker 中使用 Response 對象時,一般還要經過 response.clone()
來取得一個克隆使用;這樣作的緣由是,一個 response 是一個流,只用被消費一次,而咱們想讓瀏覽器、緩存等屢次操做這個響應,就須要 clone 出不一樣的對象來;對於 Request 請求對象的使用也是相似的道理
在 service worker 中沒法使用傳統的 XMLHttpRequest
,只能使用 fetch
;然後者的優點正在於,可使用 Request
和 Response
對象
每次網絡請求,都會觸發對應的 service worker 中的 fetch
事件
在咱們的例子中,頁面上有一個指向 deer.png
的圖片元素,最後卻由 fetch
事件回調攔截並返回了 /horse.jpg
,實現了混淆是非的自定義資源指向
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname == '/deer.png') {
event.respondWith(
fetch('/horse.jpg').catch(ex=>console.log(ex))
);
}
});
複製代碼
在 service worker 規範中包含了原生的緩存能力,用以替代已被廢棄的 Application Cache 標準。
Cache
API 提供了一個網絡請求的持久層,並可使用 match 操做查詢這些請求。
在 service worker 中最主要用到 Cache 的地方,仍是在上面提到的 fetch
事件回調中。
經過使用本地緩存中的資源,不但能省去對網絡的昂貴訪問,更有了在 離線、掉線、網絡不佳 等狀況下維持應用可用的能力。
相關的定義以下:
interface Cache {
match(request, optional cacheQueryOptions);
matchAll(optional request, optional cacheQueryOptions);
add(request);
addAll(requests);
put(request, response);
delete(request, optional cacheQueryOptions);
keys(optional request, optional cacheQueryOptions);
};
複製代碼
同時 service worker 也能夠用 self.caches
來取得緩存:
interface WindowOrWorkerGlobalScope {
readonly attribute caches; //CacheStorage
};
interface CacheStorage {
match(request, optional options); //Promise
has(cacheName); //Promise
open(cacheName); //Promise
delete(cacheName); //Promise
keys(); //Promise
};
複製代碼
反映在例子中就是(版本的部分會在稍後提到):
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
複製代碼
將 server worker 的生命週期設計成這樣,其目的在於:
重要的好比:
install
事件:使用register()
註冊時會觸發activate
事件:register()
註冊時也會觸發activate事件具體到各個事件的回調中,event 參數對應的類型以下:
事件名稱 | 接口 |
---|---|
install | ExtendableEvent |
activate | ExtendableEvent |
fetch | FetchEvent |
message | ExtendableMessageEvent |
messageerror | MessageEvent |
其中有表明性的兩個事件的定義以下:
interface ExtendableEvent {
void waitUntil(promiseFunc);
};
interface FetchEvent {
readonly attribute request;
readonly attribute clientId;
readonly attribute reservedClientId;
readonly attribute targetClientId;
void respondWith(promiseFunc);
};
複製代碼
因此,才能夠在例子中調用 event.waitUntil()
和 event.respondWith()
:
self.addEventListener('install', function(event) {
event.waitUntil( //用一個 promise 檢查安裝是否成功
//...
);
});
self.addEventListener('fetch', function(event) {
event.respondWith( // 返回符合指望的 Response 對象
//...
);
複製代碼
不一樣於其餘兩種 worker 的是,service worker 再也不用 new 來實例化,而是直接經過 navigator.serviceWorker
取得
navigator.serviceWorker
實際上實現了 ServiceWorkerContainer
接口:
interface ServiceWorkerContainer {
readonly attribute controller;
readonly attribute ready; //promise
register(scriptURL, optional registrationOptions);
getRegistration(optional clientURL = "");
getRegistrations();
void startMessages();
attribute oncontrollerchange;
attribute onmessage; // event.source is a worker
attribute onmessageerror;
};
複製代碼
好比咱們在例子中的主頁面所作的:
navigator.serviceWorker.register(
'myService.js',
{scope: '/'}
).then().catch()
複製代碼
scope 參數是選填的,能夠被用來指定想讓 service worker 控制的內容的子目錄;service worker 能控制的最大權限層級就是其所在的目錄
運行 register()
方法成功的話,會在 navigator.serviceWorker
的 Promise 的 then 回調中獲得一個 ServiceWorkerRegistration
類型的對象;
正如例子中所示,主頁面中就能夠用這個實例化後的 'registration' 對象調用 onmessage
了
interface ServiceWorkerRegistration {
readonly attribute installing;
readonly attribute waiting;
readonly attribute active;
readonly attribute scope;
readonly attribute updateViaCache;
update(); //in promise
unregister(); //in promise
attribute onupdatefound;
};
複製代碼
同時若是 register()
成功,service worker 就在 ServiceWorkerGlobalScope
環境中運行;
也就是說,myService.js
中引用的 self
就是這個類型了,能夠調用 self.skipWaiting()
等方法;
這是一個特殊類型的 worker 上下文運行環境,與主運行線程相獨立,同時也沒有訪問 DOM 等能力
interface ServiceWorkerGlobalScope {
readonly attribute clients;
readonly attribute registration;
skipWaiting();
attribute oninstall;
attribute onactivate;
attribute onfetch;
attribute onmessage; // event.source is a client
attribute onmessageerror;
};
複製代碼
和 shared worker 相似,須要當心 service worker 腳本里的全局變量: 每一個頁面不會有本身獨有的worker
在 service worker 註冊以後,install 事件會被觸發
在 install 回調中,通常執行如下任務:
出如今 activate 回調中的一個常見任務是緩存管理。在這個步驟進行緩存管理,而不是在以前的安裝階段進行,緣由在於:若是在 install 步驟中清除了任何舊緩存,則繼續控制全部當前頁面的任何舊 service worker 將忽然沒法從緩存中提供文件
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);
}
})
);
})
);
});
複製代碼
出於安全考慮,目前只能在 HTTPS 環境下才能使用 service worker;不符合則會拋出錯誤
DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).
在測試時,是能夠用 http://localhost
進行的
後臺同步(Background Sync)是基於 service worker 構建的另外一個功能。容許用戶一次性或按間隔時間請求後臺數據同步。
navigator.serviceWorker.register('sw.js');
//...
navigator.serviceWorker.ready.then(registration=>{
registration.sync.register('update-leaderboard').then(function() {
// registration succeeded
}, function() {
// registration failed
});
});
複製代碼
//sw.js
self.addEventListener('sync', function(event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function(cache) {
return cache.add('/leaderboard.json');
})
);
}
});
複製代碼
Push API 是基於 service worker 構建的另外一個功能。該 API 容許喚醒 service worker 以響應來自操做系統消息傳遞服務的消息。
正是基於 service worker,chrome 在網絡不可用時會顯示小恐龍冒險的離線遊戲,按下空格鍵,就能夠開始了~
因爲一些相關的 google 服務沒法用,iOS 上對其的支持也有限並在試驗階段,因此尚不具有大規模應用的條件;
但做爲漸進式網絡應用技術 PWA 中的最重要的組成部分,國內不少廠商已經在嘗試推動相關的支持,將來值得期待:
Master-Worker
是經常使用的並行設計模式,用worker
表示線程相關的概念就來源於此web worker
的出現使得在 Web 頁面中進行多線程編程成爲可能