- 原文地址:Understanding Service Workers
- 原文做者:Adnan Chowdhury
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:zyziyun
- 校對者:undead25、calpa
什麼是 Service Workers?他們可以作什麼,怎樣使你的 web app 表現得更好?本文旨在回答這些問題,以及如何使用 Ember.js 框架來實現他們。css
在互聯網早期時代,幾乎沒人會考慮用戶處於離線狀態時該如何呈現一個 web 頁面,只會考慮在線狀態。html
鏈接上了!這幫傢伙在這裏!永遠別想離開。前端
可是,隨着移動互聯網的到來以及網絡在世界其餘地區的普及,良莠不齊的網絡質量在用戶使用的現代網絡中已經愈來愈廣泛。node
所以,網站在離線狀態時候的表現,以便用戶不受網絡可用性的限制,已變得很是有價值。react
AppCache 最初是做爲 HTML5 規範的一部分引入,用以解決離線 web 應用程序的問題。它包含以 Cache Manifest 配置文件爲中心的HTML和JS的組合,配置文件以聲明式語言來編寫。 android
AppCache 最終被發現是 不實用的和充滿陷阱的。所以它已被廢棄,被 Service Workers 有效的取代。ios
Service workers 提供了一個更具前瞻性的離線應用解決方案,經過更加程序化的語言書寫規則替代 AppCache 的聲明式書寫方式。git
Service Workers 在瀏覽器後臺進程中持續的執行其代碼。它是事件驅動的,這意味着在 Service Worker 的做用域範圍內觸發的事件會驅動其行爲。github
這篇文章剩下的部分將對 Service Worker 的每一個事件階段作個簡要的說明,可是在開始使用 Service Workers 以前,你首先須要在你的 web app 中執行代碼來註冊 Service Worker 。web
下面的代碼說明了怎樣在你的客戶端瀏覽器中註冊你的 Service Worker,這是經過在你的 web app 前端代碼的某一處執行 register
方法調用來實現的:
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('congrats. scope is: ', registration.scope);
})
.catch(error => {
console.log('sorry', error);
});
}複製代碼
這將告訴瀏覽器在哪裏找到你的 Service Worker 的實現,瀏覽器將查找對應的(/sw.js
)文件,並將它保存在你正在訪問的域名下,這個文件將包含全部你本身定義的 Service Worker 事件處理程序。
在 Chrome 開發者工具中查看已註冊的 Service Worker
它也將設置你的 Service Worker 的做用域,這個 /sw.js
文件意味着 Service Worker 的做用範圍是在你 URL(這裏是指http://localhost:3000/
) 的根路徑下。這意味着在你的根路徑下的任何請求,都將經過觸發事件的方式告訴 Service Worker。一個文件路徑爲/js/sw.js
的文件就僅僅能夠捕獲http://localhost:3000/js
該連接下的請求。
另外,你也能夠經過將第二個參數傳入給 register
方法來明確地設置 Service Worker 的做用域範圍:navigator.serviceWorker.register('/sw.js', { scope: '/js' })
。
如今你的 Service Worker 已經被註冊好了,是時候在你的 Service Worker 生命週期中觸發實現對應的事件處理程序了。
當你的 Service Worker 首次註冊的時,或者你的 Service Worker 文件(/sw.js
)在以後的任什麼時候間被更新時(瀏覽器會自動檢測這些更改),install 事件都將被觸發。
對於那些你想在你的 Service Worker 初始化時執行的邏輯,install 事件是很是有用的,它能夠執行一些一次性的操做,貫穿在整個 Service Worker 應用程序的生命週期中。一個常見的例子是在 install 階段加載緩存。
下面是一個在 install 事件處理程序階段向緩存添加數據的例子。
const CACHE_NAME = 'cache-v1';
const urlsToCache = [
'/',
'/js/main.js',
'/css/style.css',
'/img/bob-ross.jpg'
];
self.addEventListener('install', event => {
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
});
});複製代碼
urlsToCache
包含了一組咱們想要添加到緩存的 URL。
caches
是一個全局的 CacheStorage 對象,容許你在瀏覽器中管理你的緩存。咱們將調用 open
方法來檢索具體咱們想要使用的 Cache 對象。
cache.addAll
將收到一組 URL,並向每一個 URL 發起一個請求,而後將響應存儲在其緩存中。它使用請求體做爲每一個緩存值的鍵名。瞭解更多請參閱 addAll。
在 Chrome 開發者工具中查看緩存數據
Fetch 事件是在每次網頁發出請求的時候觸發的,觸發該事件的時候 Service Worker 可以 '攔截' 請求,並決定返回內容 ———— 是返回緩存的數據,仍是返回真實請求響應的數據。
下面的例子說明了緩存優先的策略:與請求匹配的任何緩存數據都將優先被返回,而不須要發送網絡請求。只有當沒有現有的緩存數據時纔會發出網絡請求。
self.addEventListener('fetch', event => {
const { request } = event;
const findResponsePromise = caches.open(CACHE_NAME)
.then(cache => cache.match(request))
.then(response => {
if (response) {
return response;
}
return fetch(request);
});
event.respondWith(findResponsePromise);
});複製代碼
request
屬性包含在 FetchEvent 對象裏,它用於查找匹配請求的緩存。
cache.match
將嘗試找到一個與指定請求匹配的緩存響應。若是沒有找到對應的緩存,則 promise 會 resolve 一個 undefined
值。在這個例子裏,咱們經過判斷這個值來決定是返回這個值,仍是調用 fetch 發起一個網絡請求並返回一個 promise。
event.respondWith
是一個 FetchEvent 對象中的特殊方法,用於將請求的響應發送回瀏覽器。它接收一個對響應(或網絡錯誤)resolve 後的 Promise 對象做爲參數。
Fetch 事件特別重要,由於它可以定義你的緩存策略。也就是說,你能夠決定什麼時候使用緩存數據,什麼時候使用網絡請求來的數據。
Service Worker 的好用之處在於它是一個用於攔截請求的低層 API,並容許你決定爲其提供哪些響應。這容許咱們自由的提供咱們本身的緩存策略或者網絡來源的內容。當你嘗試實現一個最好的 Web App 的時候,有幾種基本的緩存策略可使用。
Mozilla 基金會有一個 handy resource 的文檔,其中有寫幾種不一樣的緩存策略。還有 Jake Archibald 編寫的 The Offline Cookbook 書中有概述幾種類似的緩存策略等等。
在上文的一個例子中,咱們演示了一個基本的緩存優先的策略。如下是我發現的一個適用於我本身項目的示例:緩存和更新策略。這個方法首先讓緩存響應,隨後在後臺發起對應的網絡請求。來自後臺請求的響應用於更新緩存中的數據,以便在下次訪問時提供更新後的響應。
self.addEventListener('fetch', event => {
const { request } = event;
event.respondWith(caches.open(CACHE_NAME)
.then(cache => cache.match(request))
.then(matching => matching || fetch(request)));
event.waitUntil(caches.open(CACHE_NAME)
.then(cache => fetch(request)
.then(response => cache.put(request, response))));
});複製代碼
event.respondWith
用於提供對請求的響應。這裏咱們打開緩存找到匹配的響應,若是它不存在,咱們會走網絡請求。
隨後,咱們將調用 event.waitUntil
方法以容許在 Service Worker 上下文終止以前 resolve 一個異步Promise。這裏會走一個網絡請求,而後緩存其響應。一旦這個異步操做完成,waitUntil
將會 resolve,操做將會終止。
激活事件是一個較少記錄的事件,但當你須要更新 Service Worker 文件,執行清理或者維護以前版本的 Service Worker 的時候,它是很是重要的。
當你更新你的 Service Worker 文件(/sw.js
)的時候,瀏覽器會檢測到這些改變,它們在 Chrome 開發者工具中的展現以下圖所示:
你的新 Service Worker 正在「等待激活」。
當實際網頁關閉並從新打開的時候,瀏覽器將使用新的 Service Worker 替換舊的 Service Worker,而後在 install 事件觸發以後,觸發 activate 事件,若是你須要清理緩存或者對舊版本的 Service Worker 進行維護,激活事件可讓你完美的作到這一點。
Sync 事件容許延遲網絡任務,直到用戶鏈接上網絡,它實現的功能一般被稱爲後臺同步。這對於在離線模式下,確保用戶啓動的任何有網絡依賴的任務,最終都將在網絡再次可用時達到其預期目的,是很是有用的。
下面是一個後臺同步實現的例子。你須要在前端 JavaScript 中註冊一個 sync 事件,並在 Service Worker 中附帶 sync 事件處理程序。
// app.js
navigator.serviceWorker.ready
.then(registration => {
document.getElementById('submit').addEventListener('click', () => {
registration.sync.register('submit').then(() => {
console.log('sync registered!');
});
});
});複製代碼
在這裏,咱們分配一個 click 事件給 button 元素,它將調用 ServiceWorkerRegistration 對象上的 sync.register
方法。
基本上,要確保任何操做均可以當即或最終在網絡可用時到達網絡,都須要被註冊爲 sync 事件。
在 Service Worker 的事件處理程序中,可能的操做像是發送一個評論,或者獲取用戶數據等等。
// sw.js
self.addEventListener('sync', event => {
if (event.tag === 'submit') {
console.log('sync!');
}
});複製代碼
這裏咱們監聽一個 sync 事件,並檢查 SyncEvent 對象上的 tag
屬性屬性是否匹配咱們指定給 click 事件的'submit'
標籤。
若是對應 'submit'
標籤下的多個 sync 事件信息被註冊,sync 事件處理程序將只執行一次。
所以,在這個例子中,若是用戶離線,並點擊了七次按鈕,那麼當網絡恢復時,全部同步的註冊事件將被合而且只觸發一次。
在這種狀況下,若是你想拆分同步事件給每一次點擊,你能夠註冊多個具備惟一標記的同步事件。
若是用戶在線,則同步事件將會當即觸發,並完成你定義的任何任務,而不會延時。
若是用戶離線,則一旦從新得到網絡鏈接,同步事件就會觸發。
若是你像我同樣,想在 Chrome 中嘗試一下,必定要經過禁用 Wi-Fi 或者其餘網絡適配器來斷開互聯網鏈接。而在 Chrome 開發者工具中切換網絡複選框不會觸發 sync 事件。
想了解更多的信息,你能夠閱讀文檔 this explainer document ,還有這篇文檔 introduction to background syncs 。sync 事件如今在大部分瀏覽器當中並無實現(撰寫本文時,只能在 Chrome 中使用),但勢必在未來會發生變化,敬請期待。
通知推送是 Service Workers 經過曝露其 push
以及瀏覽器實現的 Push API 來啓用的功能。
當咱們討論網絡推送通知的時候,實際上會涉及兩種對應的技術:通知和推送信息。
通知是能夠經過 Service Workers 實現的很是簡單的功能:
// app.js
// ask for permission
Notification.requestPermission(permission => {
console.log('permission:', permission);
});
// display notification
function displayNotification() {
if (Notification.permission == 'granted') {
navigator.serviceWorker.getRegistration()
.then(registration => {
registration.showNotification('this is a notification!');
});
}
}複製代碼
// sw.js
self.addEventListener('notificationclick', event => {
// notification click event
});
self.addEventListener('notificationclose', event => {
// notification closed event
});複製代碼
你首先須要向用戶發出許可才能啓用網頁的通知。從那時起,你能夠切換通知,並處理某些事件,例如用戶關閉一個通知的時候。
推送消息涉及利用瀏覽器提供的 Push API 以及後端實現。這個要點能夠單獨抽出一篇文章詳細講解,可是其基本要點以下圖所示:
這是一個稍微複雜的過程,超出了本文的範圍。但若是你想了解更多,能夠參考 introduction to push notifications 這篇文章 。
用 Ember.js 實現 Service Workers 的 APP 是很是容易的,憑藉其腳手架工具 ember-cli 和其插件體系 Ember Add-ons 社區的支持,你能夠以一種即插即拔的方式在你的 Web App 中增長 Service Worker。
這是由 DockYard 的人員提供的一系列插件 ember-service-worker 及其對應文檔 here。
ember-service-worker 創建了一個模塊化的結構,能夠被用於插入其餘 ember-service-worker-* 的插件,例如 ember-service-worker-index 或者 ember-service-worker-asset-cache。這些插件使用不一樣的表現實現對應行爲,以及不一樣的緩存策略組成你的 Service Worker 服務。
ember-service-worker
的約定全部的 ember-service-worker- 插件都遵循相同的模塊結構,它們的核心邏輯存儲在其根目錄的/service-worker
and /service-worker-registration
這兩個文件夾中。
node_modules/ember-service-worker
├── ...
├── package.json
├── service-worker
└── index.js
└── service-worker-registration
└── index.js
/service-worker
該目錄是實現 Service Worker 的主要存儲位置(如文章前面所說的那個 sw.js
就是存儲在這個目錄下)。
/service-worker-registration
該目錄下有你須要在前端代碼中運行的邏輯,像 Service Worker 的註冊流程。
讓咱們看看 ember-service-worker-index 該插件的 /service-worker
目錄下的代碼實現 (code here) ,符合上面所說的內容。
import {
INDEX_HTML_PATH,
VERSION,
INDEX_EXCLUDE_SCOPE
} from 'ember-service-worker-index/service-worker/config';
import { urlMatchesAnyPattern } from 'ember-service-worker/service-worker/url-utils';
import cleanupCaches from 'ember-service-worker/service-worker/cleanup-caches';
const CACHE_KEY_PREFIX = 'esw-index';
const CACHE_NAME = `${CACHE_KEY_PREFIX}-${VERSION}`;
const INDEX_HTML_URL = new URL(INDEX_HTML_PATH, self.location).toString();
self.addEventListener('install', (event) => {
event.waitUntil(
fetch(INDEX_HTML_URL, { credentials: 'include' }).then((response) => {
return caches
.open(CACHE_NAME)
.then((cache) => cache.put(INDEX_HTML_URL, response));
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(cleanupCaches(CACHE_KEY_PREFIX, CACHE_NAME));
});
self.addEventListener('fetch', (event) => {
let request = event.request;
let isGETRequest = request.method === 'GET';
let isHTMLRequest = request.headers.get('accept').indexOf('text/html') !== -1;
let isLocal = new URL(request.url).origin === location.origin;
let scopeExcluded = urlMatchesAnyPattern(request.url, INDEX_EXCLUDE_SCOPE);
if (isGETRequest && isHTMLRequest && isLocal && !scopeExcluded) {
event.respondWith(
caches.match(INDEX_HTML_URL, { cacheName: CACHE_NAME })
);
}
});複製代碼
不去看具體的細節,咱們能夠看到,這個代碼基本實現了咱們以前討論過的三個事件處理程序:install
, activate
and fetch
。
在 install
事件處理程序中,咱們調用 INDEX_HTML_URL
對應的接口,獲取數據,而後調用 cache.put
存儲響應數據。
activate
階段作了一些基本的清理緩存的操做。
在 fetch
事件處理程序中,咱們檢查 request
是否知足幾個條件(是不是 GET
請求,是否請求 HTML,是不是本地資源等等),只有知足一系列的條件,咱們才把對應的數據緩存返回。
注意咱們調用 cache.match
方法 和 INDEX_HTML_URL
地址,來查找值,而不使用 request.url
請求的 url。這意味着不管實際調用的 URL 請求是什麼,咱們始終會根據相同的緩存密鑰作對應的查找操做。
這是由於 Ember 的應用程序將始終使用 index.html
進行頁面渲染。在應用程序的根路徑下的任何 URL 請求都將以 index.html
的緩存版本結尾,Ember 應用程序一般會接管。這就是 ember-service-worker-index 來緩存index.html
的目的。
一樣的,ember-service-worker-asset-cache 該插件將緩存全部在 /assets
目錄下能夠找到的全部資源,文件,觸發調用其 install
和 fetch
事件處理函數。
有幾個插件 several add-ons 也使用 ember-service-worker 該插件的結構,容許你自定義和微調對應的 Service Worker 的表現和緩存策略。
首先,你須要下載 ember-cli,而後在命令行中執行下面的語句操做:
$ ember new new-app
$ cd new-app
$ ember install ember-service-worker
$ ember install ember-service-worker-index
$ ember install ember-service-worker-asset-cache複製代碼
你的應用程序如今由 Service Workers 提供緩存服務,默認狀況下,會將 index.html
文件和 /assets/**/*
該目錄下的內容緩存。
你能夠經過修改 config/environment.js
這個配置文件調整 /assets
文件夾下哪些文件將被緩存。
若是你發現現有的 ember-service-worker 插件沒有解決你的問題,你能夠參照這個文檔 docs at the ember-service-worker website 建立你本身的插件。
我但願你可以對 Service Workers 和其底層架構有一個更深刻理解,以及怎樣利用他們建立用戶體驗更好的Web App。
ember-service-worker
插件讓你能在你的 Ember.js 應用程序中很容易地實現他們。若是你發現須要實現一個本身的 Service Worker 的邏輯,你能夠很容易的建立本身的插件,來實現你須要的行爲所對應的事件處理程序,這是我想在不久的未來解決的問題,敬請關注!
若是你對基於 Ember.js 的全職工做感興趣,Quartzy 正在招聘前端工程師!咱們幫助世界各地的科學家節省資金,使得他們更有效率的在實驗室研究。點擊這裏申請吧。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。