[TOC]
# Service Workers
豐富的離線體驗、定期的后臺同步以及推送通知等通常需要將面向本機應用的功能將引入到網絡應用中。service worker提供了所有這些功能所依賴的技術基礎。
<br>
## 什么是service worker
service worker是瀏覽器在后臺獨立于網頁運行的腳本,它打開了通向不需要網頁或用戶交互的功能的大門。現在,它們已包括如推送通知和后臺同步等功能。將來,service worker將會支持如定期同步或地理圍欄等其他功能。本教程討論的核心功能是攔截和處理網絡請求,包括通過程序來管理緩存中的響應。
這個 API 之所以令人興奮,是因為它可以支持離線體驗,讓開發者能夠全面控制這一體驗。
在service worker出現前,存在能夠在網絡上為用戶提供離線體驗的另一個 API,稱為 AppCache。App Cache 的主要問題是,它具有相當多的缺陷,并且,雖然它對單頁網絡應用支持較好,但對多頁網站來說則不盡人意。service worker則能很好地避免這些常見的難點。
service worker相關注意事項:
* 它是一種 [JavaScript 工作線程](https://www.html5rocks.com/en/tutorials/workers/basics/),無法直接訪問 DOM。 service worker通過響應 [`postMessage`](https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage) 接口發送的消息來與其控制的頁面通信,頁面可在必要時對 DOM 執行操作。
* service worker是一種可編程網絡代理,讓您能夠控制頁面所發送網絡請求的處理方式。
* 它在不用時會被中止,并在下次有需要時重啟,因此,您不能依賴于service worker的 `onfetch` 和 `onmessage` 處理程序中的全局狀態。如果存在您需要持續保存并在重啟后加以重用的信息,service worker可以訪問 [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)。
* service worker廣泛地利用了 `promise`,因此如果您不熟悉 `promise`,則應停下閱讀此內容,看一看 Promise 簡介。
<br>
## service worker生命周期
service worker的生命周期完全獨立于網頁。
要為網站安裝service worker,您需要先在頁面的 JavaScript 中注冊。 注冊service worker將會導致瀏覽器在后臺啟動服務工作線程安裝步驟。
在安裝過程中,您通常需要緩存某些靜態資源。如果所有文件均已成功緩存,那么service worker就安裝完畢。如果任何文件下載失敗或緩存失敗,那么安裝步驟將會失敗,service worker就無法激活(也就是說,不會安裝)。 如果發生這種情況,不必擔心,它下次會再試一次。 但這意味著,如果安裝完成,您可以知道您已在緩存中獲得那些靜態資源。
安裝之后,接下來就是激活步驟,這是管理舊緩存的絕佳機會,我們將在service worker的更新部分對此詳加介紹。
激活之后,service worker將會對其作用域內的所有頁面實施控制,不過,首次注冊該service worker的頁面需要再次加載才會受其控制。service worker實施控制后,它將處于以下兩種狀態之一:service worker終止以節省內存,或處理獲取和消息事件,從頁面發出網絡請求或消息后將會出現后一種狀態。
以下是service worker初始安裝時的簡化生命周期。

<br>
## 先決條件
### 瀏覽器支持
可用的瀏覽器日益增多。service worker受 Firefox 和 Opera 支持。 Microsoft Edge 現在[表示公開支持](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/serviceworker/)。甚至 Safari 也暗示未來會進行相關開發。 您可以在 Jake Archibald 的 [is Serviceworker ready](https://jakearchibald.github.io/isserviceworkerready/) 網站上查看所有瀏覽器的支持情況。
### 您需要 HTTPS
在開發過程中,可以通過 localhost 使用service worker,但如果要在網站上部署服務工作線程,需要在服務器上設置 HTTPS。
使用service worker,您可以劫持連接、編撰以及過濾響應。 這是一個很強大的工具。您可能會善意地使用這些功能,但中間人可會將其用于不良目的。 為避免這種情況,可僅在通過 HTTPS 提供的頁面上注冊service worker,如此我們便知道瀏覽器接收的service worker在整個網絡傳輸過程中都沒有被篡改。
Github 頁面 通過 HTTPS 提供,因此這些頁面是托管演示的絕佳位置。
如果想要向服務器添加 HTTPS,您需要獲得 TLS 證書并在服務器上進行設置。 具體因您的設置而異,因此請查看服務器的文檔,并務必查閱 [Mozilla SSL 配置生成器](https://mozilla.github.io/server-side-tls/ssl-config-generator/),了解最佳做法。
<br>
## 注冊service worker
要安裝service worker,您需要通過在頁面中對其進行**注冊**來啟動安裝。 這將告訴瀏覽器service worker JavaScript 文件的位置。
~~~
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
~~~
此代碼用于檢查 Service Worker API 是否可用,如果可用,則在頁面加載后注冊位于 /sw.js 的服務工作線程。。
每次頁面加載無誤時,即可調用 **register()** 瀏覽器將會判斷 service worker 是否已注冊并做出相應的處理。
有一個需要特別說明的是service worker文件的路徑,你一定注意到了在這個例子中,service worker文件被放在這個域的根目錄下,這意味著service worker和網站同源。換句話說,這個service work將會收到這個域下的所有fetch事件。如果我將service worker文件注冊為/example/sw.js,那么,service worker只能收到/example/路徑下的fetch事件(例如: /example/page1/, /example/page2/)。
現在,您可以通過轉至 chrome://inspect/#service-workers 并尋找您的網站來檢查 service worker 是否已啟用。

首次執行 service worker 時,您還可以通過 chrome://serviceworker-internals 來查看服務工作線程詳情。 如果只是想了解 service worker 的生命周期,這仍很有用,但是日后其很有可能被 chrome://inspect/#service-workers 完全取代。
您會發現,它還可用于測試隱身窗口中的 service worker ,您可以關閉 service worker 并重新打開,因為之前的 service worker 不會影響新窗口。從隱身窗口創建的任何注冊和緩存在該窗口關閉后均將被清除。
<br>
## Service Worker的安裝步驟
在頁面上完成注冊步驟之后,讓我們把注意力轉到service worker的腳本里來,在這里面,我們要完成它的安裝步驟。
在最基本的例子中,你需要為install事件定義一個callback,并決定哪些文件你想要緩存。
~~~
self.addEventListener('install', function(event) {
// Perform install steps
});
~~~
在我們的install callback中,我們需要執行以下步驟:
1. 開啟一個緩存
2. 緩存我們的文件
3. 決定是否所有的資源是否要被緩存
~~~
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);
})
);
});
~~~
上面的代碼中,我們通過 `caches.open` 打開我們指定的cache文件名,然后我們調用 `cache.addAll` 并傳入我們的文件數組。這是通過一連串 promise( `caches.open` 和 `cache.addAll` )完成的。`event.waitUntil` 拿到一個 promise 并使用它來獲得安裝耗費的時間以及是否安裝成功。
如果所有的文件都被緩存成功了,那么 service worker 就安裝成功了。如果任何一個文件下載失敗,那么安裝步驟就會失敗。這個方式允許你依賴于你自己指定的所有資源,但是這意味著你需要非常謹慎地決定哪些文件需要在安裝步驟中被緩存。指定了太多的文件的話,就會增加安裝失敗率。
上面只是一個簡單的例子,你可以在install事件中執行其他操作或者甚至忽略install事件。
<br>
## 緩存和返回Request
你已經安裝了service worker,你現在可以返回你緩存的請求了。
當service worker被安裝成功并且用戶瀏覽了另一個頁面或者刷新了當前的頁面,service worker將開始接收到 fetch 事件。下面是一個例子:
~~~
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
~~~
上面的代碼里我們定義了 fetch 事件,在 `event.respondWith()` 里,我們傳入了一個由 `caches.match` 產生的 `promise.caches.match` 查找 reques t中被 service worker 緩存命中的 response。
如果我們有一個命中的 response,我們返回被緩存的值,否則我們返回一個實時從網絡請求fetch的結果。這是一個非常簡單的例子,使用所有在 install 步驟下被緩存的資源。
如果我們想要增量地緩存新的請求,我們可以通過處理fetch請求的response并且添加它們到緩存中來實現,例如:
~~~
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 緩存命中 - 返回響應
if (response) {
return response;
}
// 重要:克隆請求。
? // 請求是一個流,只能被使用一次。
? // 由于我們通過緩存消耗了一次,而瀏覽器獲取用了一次,我們需要克隆響應。
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// 檢查是否收到有效的 response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 重要提示:克隆響應。
// 響應是一個流,因為我們希望瀏覽器使用響應以及消耗響應的緩存,我們需要克隆它以便我們有兩個流
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
~~~
代碼里我們所做事情包括:
1. 添加一個callback到fetch請求的 .then 方法中
1.1. 一旦我們獲得了一個response,我們進行如下的檢查:
1.2. 確保response是有效的
1.3. 檢查response的狀態是否是200
2. 保證response的類型是**basic**,這表示請求本身是同源的,非同源(即跨域)的請求也不能被緩存。
3. 如果我們通過了檢查,clone這個請求。這么做的原因是如果response是一個Stream,那么它的body只能被讀取一次,所以我們得將它克隆出來,一份發給瀏覽器,一份發給緩存。
<br>
## 更新一個Service Worker
你的service worker總有需要更新的那一天。當那一天到來的時候,你需要按照如下步驟來更新:
1. 更新你的service worker的JavaScript文件。當用戶瀏覽你的網站,瀏覽器嘗試在后臺下載service worker的腳本文件。只要服務器上的文件和本地文件有一個字節不同,它們就被判定為需要更新。
2. 更新后的service worker將開始運作,install event被重新觸發。
3. 在這個時間節點上,當前頁面生效的依然是老版本的service worker,新的servicer worker將進入”waiting”狀態。
4. 當前頁面被關閉之后,老的service worker進程被殺死,新的servicer worker正式生效。
5. 一旦新的service worker生效,它的activate事件被觸發。
代碼更新后,通常需要在activate的callback中執行一個管理cache的操作。因為你會需要清除掉之前舊的數據。我們在activate而不是install的時候執行這個操作是因為如果我們在install的時候立馬執行它,那么依然在運行的舊版本的數據就壞了。
之前我們只使用了一個緩存,叫做my-site-cache-v1,其實我們也可以使用多個緩存的,例如一個給頁面使用,一個給blog的內容提交使用。這意味著,在install步驟里,我們可以創建兩個緩存,pages-cache-v1和blog-posts-cache-v1,在activite步驟里,我們可以刪除舊的my-site-cache-v1。
下面的代碼能夠循環所有的緩存,刪除掉所有不在白名單中的緩存。
~~~
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);
}
})
);
})
);
});
~~~
<br>
## 處理邊界和填坑
這一節內容比較新,有很多待定細節。希望這一節很快就不需要講了,但是現在,這些內容還是應該被提一下。
### 如果安裝失敗了,沒有很優雅的方式獲得通知
如果一個worker被注冊了,但是沒有出現在 chrome://inspect/#service-workers 或 chrome://serviceworker-internals ,那么很可能因為異常而安裝失敗了,或者是產生了一個被拒絕的的 promise 給 `event.waitUtil`。
要解決這類問題,首先到 chrome://serviceworker-internals檢查。打開開發者工具窗口準備調試,然后在你的install event代碼中添加debugger;語句。這樣,通過斷點調試你更容易找到問題。
### fetch()的默認參數
**默認情況下沒有憑據**
當你使用fetch,缺省地,請求不會帶上cookies等憑據,要想帶上的話,需要:
~~~
fetch(url, {
credentials: 'include'
})
~~~
這樣設計是有理由的,它比XHR的在同源下默認發送憑據,但跨域時丟棄憑據的規則要來得好。fetch的行為更像其他的CORS請求,例如 `<img crossorigin>` ,它默認不發送 cookies,除非你指定了`<img crossorigin="use-credentials">.`。
### Non-CORS默認不支持
默認情況下,從第三方URL跨域得到一個資源將會失敗,除非對方支持了CORS。你可以添加一個non-CORS選項到Request去避免失敗。代價是這么做會返回一個“不透明”的response,意味著你不能得知這個請求究竟是成功了還是失敗了。
~~~
cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
console.log('All resources have been fetched and cached.');
});
~~~
### 處理響應式圖片
`img` 的 `srcset` 屬性或者`<picture>`標簽會根據情況從瀏覽器或者網絡上選擇最合適尺寸的圖片。
在service worker中,你想要在install步驟緩存一個圖片,你有以下幾種選擇:
* 安裝所有的`<picture>`元素或者將被請求的srcset屬性。
* 安裝單一的low-res版本圖片
* 安裝單一的high-res版本圖片
比較好的方案是2或3,因為如果把所有的圖片都給下載下來存著有點浪費內存。
假設你將low-res版本在install的時候緩存了,然后在頁面加載的時候你想要嘗試從網絡上下載high-res的版本,但是如果high-res版本下載失敗的話,就依然用low-res版本。這個想法很好也值得去做,但是有一個問題:
| Screen Density | Width | Height |
|---|---|---|
| 1x | 400 | 400 |
| 2x | 800 | 800 |
在srcset圖像中,我們有一些像這樣的標記:
~~~
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />
~~~
如果我們在一個2x的顯示模式下,瀏覽器會下載image-2x.png,如果我們離線,你可以讀取之前緩存并返回image-src.png替代,如果之前它已經被緩存過。盡管如此,由于現在的模式是2x,瀏覽器會把400X400的圖片顯示成200X200,要避免這個問題就要在圖片的樣式上設置寬高。
~~~
<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x"
style="width:400px; height: 400px;" />
~~~
`<picture>`標簽情況更復雜一些,難度取決于你是如何創建和使用的,但是可以通過與srcset類似的思路去解決。
## 參考資料
[Service Worker入門](http://web.jobbole.com/82247/)
[Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼