<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文搞懂 Web Worker(原理到實(shí)踐)

          共 12979字,需瀏覽 26分鐘

           ·

          2021-01-05 10:39

          作者:poetry

          原文地址:https://mp.weixin.qq.com/s/XF7qOhbBtYlwADCiyxbT-w

          Web Worker 作為瀏覽器多線程技術(shù),在頁面內(nèi)容不斷豐富,功能日趨復(fù)雜的當(dāng)下,成為緩解頁面卡頓,提升應(yīng)用性能的可選方案。

          發(fā)展歷史

          簡介

          曾經(jīng)的瀏覽器對于 JS 的處理模式是單線程模式,頁面更新要先 串行 做 2 件事情。

          隨著 Web Worker 的發(fā)布,2 件事情可以 并行 完成。

          img

          可以直觀地聯(lián)想:并行可能會 提升執(zhí)行效率;運(yùn)行任務(wù)拆分能 減少頁面卡頓

          技術(shù)規(guī)范

          Web Worker 屬于 HTML 規(guī)范,規(guī)范文檔見 ?Web Workers Working Draft ,2009 年就提出了草案。

          目前,除了 ie10 以下,主流瀏覽器都已經(jīng)得到了兼容。

          DediactedWorker 和 SharedWorker

          Web Worker 規(guī)范中包括:DedicatedWorker 和 SharedWorker 。

          img

          如上圖所示,DedicatedWorker 簡稱 Worker,其線程只能與一個(gè)頁面渲染進(jìn)程(Render Process)進(jìn)行綁定和通信,不能多 Tab 共享。DedicatedWorker 是 最早實(shí)現(xiàn)并廣泛支持的 Web Worker 能力。

          而 SharedWorker 可以多個(gè)瀏覽器 Tab 中訪問到同一個(gè) Worker 實(shí)例,實(shí)現(xiàn)多 Tab 共享數(shù)據(jù),共享 websocket 等,但是 safari 放棄了 SharedWorker 支持,因?yàn)?webkit 引擎的技術(shù)原因。如下圖所示,只在 safari 5~6 中短暫支持過。

          因此,社區(qū)中主要針對兼容性更好的 DedicatedWorker 進(jìn)行拓展,文章后續(xù)也主要以 DedicatedWorker 進(jìn)行講解。

          主線程和多線程

          用戶使用瀏覽器一般會打開多個(gè)頁面(Tab),現(xiàn)代瀏覽器使用單獨(dú)的進(jìn)程渲染每個(gè)頁面,以提升頁面性能和穩(wěn)定性,并進(jìn)行操作系統(tǒng)級別的內(nèi)存隔離。

          img

          主線程(Main Thread)

          頁面中,內(nèi)容渲染和用戶交互主要由 Render Process 中的主線程進(jìn)行管理,主線程渲染頁面每一幀(Frame)。

          如下圖所示,會包含 5 個(gè)步驟:JavaScript -> Style -> Layout -> Paint -> Composite,如果 JS 的執(zhí)行修改了 DOM,還會暫停 JS,插入并執(zhí)行 Style 和 Layout。

          img

          我們熟知的 JS 單線程和 Event Loop,是主線程的一部分。JS 單線程的機(jī)制避免了多線程開發(fā)中的復(fù)雜場景(如競態(tài)和死鎖),但單線程的主要困擾是:主線程同步 JS 執(zhí)行耗時(shí)過久時(shí)(瀏覽器理想幀間隔約 16ms),會阻塞用戶交互和頁面渲染

          img

          如上圖所示中,長耗時(shí)任務(wù)執(zhí)行時(shí),頁面無法更新,業(yè)務(wù)發(fā)響應(yīng)用戶的交互事件,如果卡死太久,瀏覽器會拋出卡頓提示。

          多線程

          Web Worker 會創(chuàng)建 操作系統(tǒng)級別的線程

          The Worker interface spawns real OS-level threads. -- MDN

          JS 多線程,是有獨(dú)立于主線程的 JS 運(yùn)行環(huán)境,如下圖所示:Worker 線程有獨(dú)立的內(nèi)存空間,Message Queue,Event Loop,Call Stack 等,線程間通過 postMessage 通信。

          img

          多個(gè)線程可以 并行 運(yùn)行 JS。

          這里的 并行 區(qū)別于單線程中的 并發(fā),單線程中的 并發(fā) 準(zhǔn)確的說叫 Concurrent,如下圖所示,運(yùn)行時(shí) 只有一個(gè)函數(shù)調(diào)用棧,通過 Event Loop 實(shí)現(xiàn)不同 Task 的上下文切換(Context Switch),這些 Task 通過 BOM API 調(diào)起其他線程為主線程工作,但回調(diào)函數(shù)代碼邏輯 仍然由 JS 串行運(yùn)行

          Web Worker 是 JS 多線程運(yùn)行技術(shù),準(zhǔn)確來說是 Parallel,其與 Concurrent,如下圖所示,運(yùn)行時(shí) 有多個(gè)函數(shù)調(diào)用棧,每個(gè)調(diào)用棧可以獨(dú)立運(yùn)行 Task,互不干擾。

          img

          應(yīng)用場景

          討論完主線程和多線程,我們能更好地理解 Worker 多線程的應(yīng)用場景:

          • 可以減少主線程卡頓
          • 可能會帶來性能提升

          減少卡頓

          目前主流顯示器的刷新率為 60Hz,即一幀為 16ms,因此播放動(dòng)畫時(shí)建議小于 16ms,用戶操作響應(yīng)建議小于 100ms,頁面打開到開始呈現(xiàn)內(nèi)容建議小于 1000ms。-- 根據(jù) Chrome 團(tuán)隊(duì)提出的用戶感知性能模型 RAIL。

          邏輯異步化

          減少主線程卡頓的主要方法是邏輯異步化,比如播放動(dòng)畫,將同步任務(wù)拆分為多個(gè)小于 16ms 的子任務(wù),然后在頁面的每一幀前通過 requestAnimationFrame 按計(jì)劃執(zhí)行一個(gè)子任務(wù),直到全部子任務(wù)執(zhí)行完畢。

          img

          拆分同步邏輯的異步方案對大部分場景有效果,但并非一勞永逸,有以下幾個(gè)問題:

          • 并非所有 JS 邏輯都可拆分:比如數(shù)組排序,樹的遞歸查找,圖像處理算法,執(zhí)行中需要維護(hù)當(dāng)前狀態(tài),且調(diào)用上非線性,無法輕易拆分成子任務(wù);

          • 可以拆分的邏輯難以把控顆粒度:如下圖所示,拆分的子任務(wù)在高性能機(jī)器上可以控制在 16ms 以內(nèi),但在性能落后的機(jī)器上就超過了 deadline。16ms 的用戶感知時(shí)間并不會因?yàn)橛脩羰稚蠙C(jī)器的差別而變化,Google 的建議是再拆小到 3~4ms;

            img
          • 拆分的子任務(wù)并不穩(wěn)定:對同步 JS 邏輯的拆分,需要根據(jù)業(yè)務(wù)場景尋找原子邏輯,而原子邏輯會隨著業(yè)務(wù)發(fā)生變化,每次改動(dòng)業(yè)務(wù)都需要去 review 原子邏輯。

          Worker 一步到位

          Worker 的多線程能力,使得同步 JS 任務(wù)拆分一步到位:從宏觀上將整個(gè)同步 JS 任務(wù)異步化。不需要再去苦苦尋找原子邏輯,邏輯異步化的設(shè)計(jì)上也更加簡單和可維護(hù)。

          這給我們帶來更多的想象空間,如下圖所示,在瀏覽器主線程渲染周期內(nèi),將可能阻塞頁面渲染的 JS 任務(wù)遷移到 Worker 線程中,進(jìn)而減少主線程的負(fù)擔(dān),縮短渲染間隔,減少頁面卡頓。

          img

          性能提升

          Worker 多線程并不會直接帶來計(jì)算性能的提升,能否提升與設(shè)備 CPU 核數(shù)和線程策略有關(guān)。

          多線程和 CPU 核數(shù)

          CPU 的單核和多核離前端似乎有點(diǎn)遙遠(yuǎn),但在頁面運(yùn)用多線程技術(shù)時(shí),核數(shù)會影響線程創(chuàng)建策略。

          進(jìn)程是操作系統(tǒng) 資源分配 的基本單位,線程是操作系統(tǒng) 調(diào)度 CPU 的基本單位,操作系統(tǒng)對線程能占用的 CPU 計(jì)算資源有復(fù)雜的分配策略,如下圖所示:

          • 單核多線程通過時(shí)間切片交替執(zhí)行;
          • 多核多線程可在不同核中真正并行。
          img

          Worker 線程策略

          一臺設(shè)備上相同任務(wù)在個(gè)線程中運(yùn)行的時(shí)間是一樣的,如下圖所示:我們將主線程 JS 任務(wù)交給新建的 Worker 線程,任務(wù)在 Worker 線程上運(yùn)行并不會比原本主線程更快,而線程新建消耗和通信開銷使得渲染間隔可能變得更久。

          img

          在單核機(jī)器上,計(jì)算資源是內(nèi)卷的,新建的 Worker 線程并不能為頁面爭取到更多的計(jì)算資源。在多核機(jī)器上,新建的 Worker 線程和主線程都能做運(yùn)算,頁面總計(jì)算資源增多,但對單詞任務(wù)來說,在哪個(gè)線程上運(yùn)行耗時(shí)是一樣的。

          真正帶來性能提升的是 多核多線程并行

          如多個(gè)沒有依賴關(guān)系的同步任務(wù),在單線程上只能串行執(zhí)行,在多核多線程中可以并行執(zhí)行。

          值得注意的是,目前移動(dòng)設(shè)備的核心數(shù)有限,受限于功耗,移動(dòng)設(shè)備 CPU 中的多核通常是大小核,所以在創(chuàng)建多條 Worker 線程時(shí)建議區(qū)分場景和設(shè)備。

          把主線程還給 UI

          Worker 的應(yīng)用場景,本質(zhì)上是把主線程的邏輯剝離,讓主線程專注于 UI 渲染,這種架構(gòu)設(shè)計(jì)并非 Web 獨(dú)創(chuàng)。

          安卓和 IOS 原生開發(fā)中,主線程負(fù)責(zé) UI 工作;前端領(lǐng)域熱門的小程序,實(shí)現(xiàn)原理上就是渲染和邏輯完全分離。

          本該如此。

          Worker API

          通信 API

          img

          如上圖所示,Worker 的通信十分簡單,具體可以參考 Web Worker 使用教程。

          雙向通信代碼十分簡單,只需要 7 行:

          //?main.js
          const?worker?=?new?Worker('./worker.js')
          worker.postMessage('hello')
          worker.onmessage?=?event?=>?{
          ??console.log(event.data)?//?'world'
          }

          //?worker.js
          self.onmessage?=?event?=>?{
          ??console.log(event.data)?//?'hello'
          ??postMessage('world')
          }

          postMessage 會在接收線程創(chuàng)建一個(gè) MessageEvent,傳遞的數(shù)據(jù)添加到 event.data,再觸發(fā)該事件;MessageEvent 的回調(diào)函數(shù)進(jìn)入 Message Queue,成為 待執(zhí)行的宏任務(wù)。因此 postMessage 順序發(fā)送 的消息,在接收線程中會 順序執(zhí)行回調(diào)函數(shù)。而且我們無需擔(dān)心實(shí)例化 Worker 過程中 postMessage 的信息丟失問題,對此 Worker 內(nèi)部機(jī)制已經(jīng)處理。

          Worker 事件驅(qū)動(dòng)的通信 API 雖然簡潔,但大多數(shù)場景下通信需要等待響應(yīng),并且多次同類型通信要匹配到各自的響應(yīng),所以業(yè)務(wù)使用一般會封裝成 Promise。

          運(yùn)行環(huán)境

          在 Worker 線程中運(yùn)行 JS,會創(chuàng)建 獨(dú)立于主線程的 JS 運(yùn)行環(huán)境,稱之為 DedicatedWorkerGlobalScope,開發(fā)者需要關(guān)注 Worker 環(huán)境和主線程環(huán)境的異同,以及 Worker 在不同瀏覽器中的差異。

          Worker 環(huán)境和主線程環(huán)境的異同

          Worker 是無 UI 線程,無法調(diào)用 UI 相關(guān)的 DOM/BOM API,具體可參考 MDN 的 functions and classes available to workers。

          img

          上圖展示了 Worker 線程和主線程的異同,它們的共同點(diǎn)包括:

          • 包含完整的 JS 運(yùn)行時(shí),支持 ES 規(guī)范定義的語言語法和內(nèi)置對象;
          • 支持 XMLHTTPRequest,能獨(dú)立發(fā)送網(wǎng)絡(luò)請求和后端進(jìn)行交互;
          • 包含只讀的 Location,指向 Worker 線程執(zhí)行的 script url,可通過 url 傳遞參數(shù)給 Worker 環(huán)境;
          • 包含只讀的 Navigator,用于獲取瀏覽器信息;
          • 支持 setTimeout / setInterval 計(jì)時(shí)器,可用于實(shí)現(xiàn)異步邏輯;
          • 支持 WebSocket 進(jìn)行網(wǎng)絡(luò) I / O,支持 IndexDB 進(jìn)行文件 I / O。

          從共同點(diǎn)上說,Worker 線程非常強(qiáng)大,除了利用線程獨(dú)立執(zhí)行重度邏輯以外,其網(wǎng)絡(luò) I / O 和文件 I / O 能力給業(yè)務(wù)和技術(shù)方案帶來很大的想象空間。

          img

          另一方面,Worker 線程運(yùn)行環(huán)境和主線程的差異點(diǎn)有:

          • Worker 線程沒有 DOM API,無法新建和操作 DOM,也無法訪問到主線程的 DOM Element;
          • Worker 線程和主線程內(nèi)存獨(dú)立,Worker 線程無法訪問頁面上的全局變量(window,document等)和 JS 函數(shù);
          • Worker 線程不能調(diào)用 alert()confirm() 等 UI 相關(guān)的 BOM API;
          • Worker 線程被主線程控制,主線程可以新建和銷毀 Worker;
          • Worker 線程可以通過 self.close 自行銷毀。

          從差一點(diǎn)上看,Worker 線程無法操作 UI,并受主線程控制。

          Worker 在不同瀏覽器中的差異

          各家瀏覽器實(shí)現(xiàn) Worker 規(guī)范有差異,對比主線程,部分 API 功能不完備,如:

          • IE10 發(fā)送的 ajax 請求沒有 referer,請求可能會被后端拒絕響應(yīng);
          • Edge18 上字符編碼 / Buffer 的實(shí)現(xiàn)有問題。

          好家伙,都是你濃眉大眼的微軟系瀏覽器,解決這些問題得通過 polyfil。

          另一方面,一些新增的 HTML 規(guī)范 API 只在較新的瀏覽器中得到實(shí)現(xiàn),Worker 運(yùn)行環(huán)境甚至主線程上沒有,使用 Worker 時(shí)需要進(jìn)行判斷和兼容。

          多線程同構(gòu)代碼

          Worker 線程不支持 DOM,這一點(diǎn)和 node.js 非常像,我們在使用 node.js 做前后端 ssr 時(shí),經(jīng)常會遇到調(diào)用 BOM / DOM API 的錯(cuò)誤。

          在開發(fā) Worker 前端項(xiàng)目,或遷移已有業(yè)務(wù)代碼到 Worker 時(shí),可以通過構(gòu)建變量區(qū)分代碼邏輯,或運(yùn)行時(shí)動(dòng)態(tài)判斷所在線程,實(shí)現(xiàn)同構(gòu)代碼在不同線程環(huán)境下運(yùn)行。

          通信速度

          Worker 多線程雖然實(shí)現(xiàn)了 JS 的并行運(yùn)行,但是也帶來了額外的 通信開銷。如下圖所示,從線程 A 調(diào)用 postMessage 發(fā)送數(shù)據(jù)到線程 B,onmessage 接收到數(shù)據(jù)有時(shí)間差,這段時(shí)間差成為 通信消耗

          img

          在線程計(jì)算能力的前提下,要通過多線程提升更多的性能,需要盡量 減少通信消耗

          而且主線程 postMessage 會占用主線程同步執(zhí)行,占用時(shí)間與數(shù)據(jù)傳輸方式和數(shù)據(jù)規(guī)模相關(guān)。要避免多線程通信導(dǎo)致的主線程卡頓,需選擇合適的傳輸方式,并控制每個(gè)渲染周期內(nèi)的數(shù)據(jù)傳輸規(guī)模。

          數(shù)據(jù)傳輸方式

          我們先來聊聊主線程和 Worker 線程的數(shù)據(jù)傳輸方式。根據(jù)計(jì)算機(jī)進(jìn)程模型,主線程和 Worker 進(jìn)程同屬一個(gè)進(jìn)程,可以訪問和操作進(jìn)程的內(nèi)存空間,但為了降低多線程并發(fā)的邏輯復(fù)雜度,部分傳輸方式直接隔離了線程間的內(nèi)存,相當(dāng)于默認(rèn)加了鎖。

          通信方式有三種:

          • Structured Clone
          • Transfer Memory
          • Shared Array Buffer
          Structured Clone

          Structured Clone 是 postMessage 默認(rèn)的通信方式,如下圖所示,復(fù)制一份線程 A 的 js object 內(nèi)存給到線程 B,線程 B 能獲取和操作新復(fù)制的內(nèi)存。

          img

          Structured Clone 通過復(fù)制內(nèi)存的方式簡單有效的隔離了不同線程的內(nèi)存,避免沖突;且傳輸?shù)?object 數(shù)據(jù)結(jié)構(gòu)很靈活,但復(fù)制過程中,線程 A 要 同步執(zhí)行 Object Serialization,線程 B 要 同步執(zhí)行 Object Deserialization,如果 object 規(guī)模過大,會占用大量的線程時(shí)間。

          Transfer Memory

          Transfer Memory 意味著轉(zhuǎn)移內(nèi)存,它不需要序列化和反序列化,能大大減少傳輸過程占用的線程時(shí)間。如下圖所示,線程 A 將制定內(nèi)存的所有權(quán)和操作權(quán)轉(zhuǎn)交給線程 B,但轉(zhuǎn)然后線程 A 無法在訪問這塊內(nèi)存。

          img

          Transfer Memory 以失去控制權(quán)來換取高效傳輸,通過內(nèi)存獨(dú)占給多線程并發(fā)加鎖,但只能轉(zhuǎn)讓 ArrayBuffer 等大小規(guī)整的二進(jìn)制數(shù)據(jù),對矩陣數(shù)據(jù)(比如 RGB圖片)比較適用,實(shí)踐上要考慮從 js object 生成二進(jìn)制數(shù)據(jù)的運(yùn)算成本。

          Shared Array Buffers

          Shared Array Buffers 是共享內(nèi)存,線程 A 和線程 B 可以 同時(shí)訪問和操作 同一塊內(nèi)存空間,數(shù)據(jù)都共享了,也就沒什么傳輸?shù)氖铝恕?/p>

          img

          但多個(gè)并行的線程共享內(nèi)存,會產(chǎn)生競爭問題,不像前兩種傳輸方式默認(rèn)加鎖,Shared Array Buffers 把難題拋給了開發(fā)者,開發(fā)者可以用 Atomics 來維護(hù)這塊共享的內(nèi)存。作為較新的傳輸方式,瀏覽器兼容性可想而知,目前只有 Chrome 68+ 支持。

          傳輸方式小結(jié)
          • 全瀏覽器兼容的 Structured Clone 是較好的選擇,但要考慮數(shù)據(jù)傳輸?shù)囊?guī)模,下文我們會詳細(xì)展開;
          • Transfer Memory 兼容性也不錯(cuò)(IE11+),但數(shù)據(jù)獨(dú)占和數(shù)據(jù)類型的限制,使得它是特定場景的最優(yōu)解,而不是常規(guī)解;
          • Shared Array Buffers 當(dāng)下糟糕的兼容性和線程鎖的開發(fā)成本,建議先暗中觀察。

          數(shù)據(jù)傳輸規(guī)模

          Structured Clone 的序列化和反序列化執(zhí)行耗時(shí) 主要受數(shù)據(jù)對象復(fù)雜度影響,這很好理解,因?yàn)樾蛄谢头葱蛄谢辽僖阅撤N方式遍歷對象。數(shù)據(jù)對象的復(fù)雜度本身難易度量,可以用序列化后的數(shù)據(jù)規(guī)模作為參考。

          2015 年的 How fast are web workers 在中等性能手機(jī)上進(jìn)行了測試: postMessage 發(fā)送數(shù)組的通信速率為 80KB/ms,相當(dāng)于理想渲染周期(16ms)內(nèi)發(fā)送 1300KB。

          2019 年 Surma 對 postMessage 的數(shù)據(jù)傳輸能力進(jìn)行了更深入研究,具體見 Is postMessage slow。高性能機(jī)器(macbook) 上的測試結(jié)果如下圖所示:

          img

          其中:

          • 測試數(shù)據(jù)為嵌套層數(shù) 1 到 6 層(payload depth,圖中縱坐標(biāo)),每層節(jié)點(diǎn)的子節(jié)點(diǎn) 1 到 6 個(gè)(payload breadth,圖中橫坐標(biāo))的對象,數(shù)據(jù)規(guī)模從 10B 到 10MB
          • 在 MacBook 上,10MB 的數(shù)據(jù)傳遞耗時(shí) 47ms,16ms 內(nèi)可以傳遞 1MB 級別的數(shù)據(jù)

          **低性能機(jī)器(nokia2)**上的測試結(jié)果如下:

          img

          其中:

          • 在 nokia2 上傳輸 10MB 數(shù)據(jù)的耗時(shí)是 638ms,16ms 內(nèi)可以傳遞 10KB 級別的數(shù)據(jù)
          • 高性能機(jī)器和低性能機(jī)器有超過 10 倍的傳輸效率差距

          綜上,不管用戶的機(jī)器性能如何,用戶對流暢的感受是一致的:前端同學(xué)的老朋友 60ms 和 100ms。

          Surma 兼顧低性能機(jī)型上 postMessage 容易造成主線程卡頓,提出的數(shù)據(jù)傳輸規(guī)模的建議是:

          • 如果 JS 代碼里面不包括動(dòng)畫渲染(100ms),數(shù)據(jù)傳輸規(guī)模應(yīng)該保持在 100KB 一下;
          • 如果 JS 代碼里面包括動(dòng)畫渲染(16ms),數(shù)據(jù)傳輸規(guī)模應(yīng)該保持在 10KB 一下。

          兼容性

          兼容性是前端最關(guān)鍵的一點(diǎn),畢竟我們無法控制用戶使用哪一款瀏覽器的那一個(gè)版本。對 Web Worker 更是如此,因?yàn)?Worker 的多線程能力要么業(yè)務(wù)場景根本用不上,要么就是重度依賴。

          從 caniuse 上面可以看到,Web Worker 的兼容性做的還是挺不錯(cuò)的。

          img

          使用 Web Worker 并非一錘子買賣,我們不止關(guān)注瀏覽器 Worker 能力有無,還需要關(guān)注它是否完備可用,因此可以用以下幾個(gè)指標(biāo)來進(jìn)行評測:

          • 是否有 Worker 能力:通過瀏覽器是否有 window.Worker 來判斷
          • 能否實(shí)例化 Worker:通過監(jiān)控 new Worker() 是否報(bào)錯(cuò)來判斷;
          • 能否跨線程通信:通過測試雙向通信來驗(yàn)證,并設(shè)置超時(shí);
          • 首次通信耗時(shí):頁面開始加載 Worker 腳本到首次通訊完成的耗時(shí),該指標(biāo)與 JS 資源加載時(shí)長,同步邏輯執(zhí)行耗時(shí)相關(guān)。

          Service Worker

          模仿 Web Worker,利用現(xiàn)代瀏覽器支持多線程運(yùn)行的機(jī)制,實(shí)現(xiàn)了一個(gè)獨(dú)立于主線程的子線程。

          作用

          • 離線緩存
          • 消息推送
          • 后臺數(shù)據(jù)同步
          • 響應(yīng)來自其他源的資源請求
          • 集中接收計(jì)算成本高的數(shù)據(jù)更新,比如地理位置和陀螺儀信息,這樣多個(gè)頁面就可以利用同一組數(shù)據(jù)
          • 在客戶端運(yùn)行 CoffeeScript,Less,CJS / AMD 等模塊編譯和依賴管理(用于開發(fā)目的)
          • 后臺服務(wù)鉤子
          • 自定義模版用于特定 URL 模式
          • 性能增強(qiáng),比如預(yù)取用戶可能需要的資源,比如相冊中后面數(shù)張照片

          局限性

          • https:Service Worker 必須運(yùn)行在 HTTPS 協(xié)議上,但在本地環(huán)境中 http://localhost 或者 http://127.0.0.1 也可以

          • 瀏覽器的兼容性

            img

            我們可以看到 IE 完全不兼容,早期的 IOS 也不兼容。

          調(diào)試

          以 Google Chrome 為例:

          1. chrome://serviceworker-internals

            img
          2. 開發(fā)者模式的 Application

            img
            img

          生命周期

          Service Worker 生命周期的反應(yīng):

          1. installing
          2. installed
          3. activating
          4. activated

          其中,installed 用來緩存文件,activated 用來更新緩存

          img

          用法

          1. html 中

            ????if?('serviceWorker'?in?navigator)?{
            ???????//?開始注冊service?workers
            ???????navigator.serviceWorker.register('./sw-demo-cache.js',?{
            ???????????scope:?'./'
            ???????}).then(function?(registration)?{
            ???????????console.log('注冊成功');
            ???????????var?serviceWorker;
            ???????????if?(registration.installing)?{
            ???????????????serviceWorker?=?registration.installing;
            ???????????????console.log('安裝installing');
            ???????????}?else?if?(registration.waiting)?{
            ???????????????serviceWorker?=?registration.waiting;
            ???????????????console.log('等待waiting');
            ???????????}?else?if?(registration.active)?{
            ???????????????serviceWorker?=?registration.active;
            ???????????????console.log('激活active');
            ???????????}
            ???????????console.log('=>serviceWorker:',?serviceWorker);
            ???????????if?(serviceWorker)?{
            ???????????????console.log(serviceWorker.state);
            ???????????????serviceWorker.addEventListener('statechange',?function?(e)?{
            ???????????????????console.log(' 狀態(tài)變化為',?e.target.state);
            ???????????????});
            ????????????????//?創(chuàng)建信道
            ???????????????var?channel?=?new?MessageChannel();
            ???????????????//?port1留給自己
            ???????????????channel.port1.onmessage?=?e?=>?{
            ???????????????????console.log('main?thread?receive?message...');
            ???????????????????console.log(e);
            ???????????????}
            ???????????????console.log('給對方',?window.RES_MAP);
            ???????????????//?port2給對方
            ???????????????serviceWorker.postMessage(window.RES_MAP,?[channel.port1]);
            ???????????????serviceWorker.addEventListener('statechange',?function?(e)?{
            ???????????????????//?logState(e.target.state);
            ???????????????});
            ???????????}
            ???????}).catch(function?(error)?{
            ???????????console.log('注冊沒有成功');
            ???????});
            ???}?else?{
            ???????console.log('不支持');
            ???}
          2. 引進(jìn) sw-demo-cache.js

            //?sw
            self.addEventListener('message',?ev?=>?{
            ??console.log('sw?receive?message..');
            ??console.log(ev);
            ??fileMap?=?ev.data.RES_MAP;
            ??var?arr1?=?[].slice.call(fileMap);?//?['a',?'b',?'c']
            ??//?取main?thread傳來的port2
            ??ev.ports[0].postMessage('Hi,?hello?too');
            });

            //?var?fs?=?require('fs');
            //?console.log(fs);
            //?緩存
            self.addEventListener('install',?function(event)?{
            ??event.waitUntil(
            ????caches.open(VERSION).then(function(cache)?{
            ??????return?cache.addAll([
            ????????'./index.html',
            ??????]);
            ????})
            ??);
            });

            //?緩存更新
            self.addEventListener('activate',?function(event)?{
            ??console.log('two?now?ready?to?handle?fetches!');
            ??event.waitUntil(
            ????caches.keys().then(function(cacheNames)?{
            ??????return?Promise.all(
            ????????cacheNames.map(function(cacheName)?{
            ??????????console.log('cacheName:',?cacheName);
            ??????????//?如果當(dāng)前版本和緩存版本不一致
            ??????????if?(cacheName?!==?VERSION)?{
            ????????????return?caches.delete(cacheName);
            ??????????}
            ????????})
            ??????);
            ????})
            ??);
            });

            //?捕獲請求并返回緩存數(shù)據(jù)
            self.addEventListener('fetch',?function?(event)?{
            ??try{
            ????event.respondWith(
            ????????caches.match(event.request).then(function(res){
            ????????????if(res){
            ????????????????return?res;
            ????????????}
            ????????????requestBackend(event);
            ????????})
            ????)
            ??}?catch?{
            ????console.log(event);
            ??}
            });

            function?requestBackend(event){
            ??var?url?=?event.request.clone();
            ??return?fetch(url).then(function(res){
            ??????//if?not?a?valid?response?send?the?error
            ??????if(!res?||?res.status?!==?200?||?res.type?!==?'basic'){
            ??????????return?res;
            ??????}
            ??????var?response?=?res.clone();
            ??????console.log('VERSION:',?VERSION);
            ??????caches.open(VERSION).then(function(cache){
            ??????????cache.put(event.request,?response);
            ??????});

            ??????return?res;
            ??})
            }
          3. webpack 中獲取文件目錄,引入第三個(gè)模塊 glob,遞歸獲取打包后的文件目錄

            exports.resMap?=?function?()?{
            ????var?entryFiles?=?glob.sync(PAGE_PATH?+?'/*/*.js')
            ????var?map?=?{}
            ????entryFiles.forEach((filePath)?=>?{
            ????????var?filename?=?filePath.substring(filePath.lastIndexOf('\/')?+?1,?filePath.lastIndexOf('.'))
            ????????map[filename]?=?filePath;
            ????})
            ????var?entryFiles2?=?glob.sync(PAGE_PATH2?+?'/*')
            ????var?map2?=?{}
            ????findPath(entryFiles2,?map2);
            ????console.log('map2',?map2);
            ????return?map2;
            };


            function?findPath(entryFiles2,?map2)?{
            ????entryFiles2.forEach(filePath?=>?{
            ????????var?filename?=?filePath.substring(filePath.lastIndexOf('/')?+?1,?filePath.lastIndexOf('.'));
            ????????if?(filePath.indexOf('.')?<=?0)?{
            ????????????let?pathRes?=?path.resolve(__dirname,?filePath);
            ????????????let?files?=?glob.sync(pathRes?+?'/*');
            ????????????findPath(files,?map2);
            ????????????map2[filename]?=?filePath;
            ????????}
            ????????map2[filename]?=?filePath;
            ????});
            }
          4. 導(dǎo)出目錄:通過 webpack 的 DefinePlugin 插件,導(dǎo)出上一步獲取的目錄

          5. Web 和 Service Worker 的通信:通過 postMessage 實(shí)現(xiàn) Web 和 Service Worker 間的通信

          //?創(chuàng)建信道
          var?channel?=?new?MessageChannel();
          ?//?port1留給自己
          channel.port1.onmessage?=?e?=>?{
          ?console.log('main?thread?receive?message...');
          ??console.log(e);
          }
          console.log('給對方',?window.RES_MAP);
          //?port2給對方
          serviceWorker.postMessage(window.RES_MAP,?[channel.port1]);
          serviceWorker.addEventListener('statechange',?function?(e)?{
          ?//?logState(e.target.state);
          });
          //?sw
          self.addEventListener('message',?ev?=>?{
          ??console.log('sw?receive?message..');
          ??console.log(ev);
          ??fileMap?=?ev.data.RES_MAP;
          ??var?arr1?=?[].slice.call(fileMap);?//?['a',?'b',?'c']
          ??//?取main?thread傳來的port2
          ??ev.ports[0].postMessage('Hi,?hello?too');
          });


          ??愛心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的點(diǎn)贊在看是我創(chuàng)作的動(dòng)力。

          2.關(guān)注公眾號程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端?開發(fā)者,會討論?前端 Node 知識,互相學(xué)習(xí)」!

          3.也可添加微信【ikoala520】,一起成長。

          “在看轉(zhuǎn)發(fā)”是最大的支持

          瀏覽 84
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美黄片网站 | 九九性爱 | 日韩成人AV在线播放 | 人人撸人人看 | 日韩无码性爱 |