[譯] 理解 Service Workers

理解 Service Workers

什麼是 Service Workers?他們可以作什麼,怎樣使你的 web app 表現得更好?本文旨在回答這些問題,以及如何使用 Ember.js 框架來實現他們。css

目錄

背景

在互聯網早期時代,幾乎沒人會考慮用戶處於離線狀態時該如何呈現一個 web 頁面,只會考慮在線狀態。html

Connected!
Connected!

鏈接上了!這幫傢伙在這裏!永遠別想離開。前端

可是,隨着移動互聯網的到來以及網絡在世界其餘地區的普及,良莠不齊的網絡質量在用戶使用的現代網絡中已經愈來愈廣泛。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事件

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 以及後端實現。這個要點能夠單獨抽出一篇文章詳細講解,可是其基本要點以下圖所示:

Push API Diagram
Push API Diagram

這是一個稍微複雜的過程,超出了本文的範圍。但若是你想了解更多,能夠參考 introduction to push notifications 這篇文章 。

使用Ember.js實現

用 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 目錄下能夠找到的全部資源,文件,觸發調用其 installfetch 事件處理函數。

有幾個插件 several add-ons 也使用 ember-service-worker 該插件的結構,容許你自定義和微調對應的 Service Worker 的表現和緩存策略。

構建基於Ember和Service-Workers的App

首先,你須要下載 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 正在招聘前端工程師!咱們幫助世界各地的科學家節省資金,使得他們更有效率的在實驗室研究。點擊這裏申請吧。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索