現在很流行基於Github page和markdown的靜態blog,非常適合技術的思維和習慣,針對不同的語言都有一些優秀的靜態blog系統出現,如Jekyll/Ruby,Pelican/Python,Hexo/NodeJs,由於靜態內容的特性非常適合做快取來加速頁面的訪問,就利用Service worker來實現加速,結果是除了PageSpeed,CDN這些常見的伺服器和網路加速之外,通過客戶端實現了更好的訪問體驗。
加速/離線訪問只需三步
- 首頁新增註冊程式碼
1 2 3 4 5 |
<script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } </script> |
- 複製程式碼
將https://alphayang.github.io/sw.js儲存到你的網站根目錄下
- 修改不快取域名列表及離線狀態頁面
在你的sw.js中修改
1 2 3 4 5 6 |
const ignoreFetch = [ /https?:\/\/cdn.bootcss.com\//, /https?:\/\/static.duoshuo.com\//, /https?:\/\/www.google-analytics.com\//, /https?:\/\/dn-lbstatics.qbox.me\//, ]; |
開啟Chrome Dev Tools->Source
,看看自己的blog都引用了哪些第三方資源,逐個加到忽略列表裡。
在根目錄下新增offline.html,在沒有網路且快取中也沒有時使用,效果如下:
在根目錄下新增offline.svg,在無網路時圖片資源請求返回該檔案。
加速效果
首頁加速後,網路請求從16降為1,載入時間從2.296s降為0.654s,得到了瞬間載入的結果。
檢視測試結果
加速/離線原理探索
什麼是 Service worker
如上圖,Service worker 是一種由Javascript編寫的瀏覽器端代理指令碼,位於你的瀏覽器和伺服器之間。當一個頁面註冊了一個 Service worker,它就可以註冊一系列事件處理器來響應如網路請求和訊息推送這些事件。Service worker 可以被用來管理快取,當響應一個網路請求時可以配置為返回快取還是從網路獲取。由於Service worker 是基於事件的,所以它只在處理這些事件的時候被調入記憶體,不用擔心常駐記憶體佔用資源導致系統變慢。
Service worker生命週期
Service worker 為網頁新增一個類似於APP的生命週期,它只會響應系統事件,就算瀏覽器關閉時作業系統也可以喚醒Service worker,這點非常重要,讓web app與native app的能力變得類似了。
Service worker在Register時會觸發Install事件,在Install時可以用來預先獲取和快取應用所需的資源並設定每個檔案的快取策略。
一旦Service worker處於activated狀態,就可以完全控制應用的資源,對網路請求進行檢查,修改網路請求,從網路上獲取並返回內容或是返回由已安裝的Service worker預告獲取並快取好的資源,甚至還可以生成內容並返回給網路語法。
所有的這些都使用者都是透明的,事實上,一個設計優秀的Service worker就像一個智慧快取系統,加強了網路和快取功能,選擇最優方式來響應網路請求,讓應用更加穩定的執行,就算沒有網路也沒關係,因為你可以完全控制網路響應。
Service worker的控制從第二次頁面訪問開始
在首次載入頁面時,所有資源都是從網路載的,Service worker 在首次載入時不會獲取控制網路響應,它只會在後續訪問頁面時起作用。
頁面首次載入時完成install,並進入idle狀態。
頁面第二次載入時,進入activated狀態,準備處理所有的事件,同時 瀏覽器會向伺服器傳送一個非同步 請求來檢查Service worker本身是否有新的版本,構成了Service worker的更新機制。
當Service worker處理完所有的事件後,進入idle狀態,最終進入terminated狀態資源被釋放,當有新的事件發生時再度被呼叫。
特點
- 瀏覽器
Google Chrome,Firefox,Opera以及國內的各種雙核瀏覽器都支援,但是 safari 不支援,那麼在不支援的瀏覽器裡Service worker不工作。
- https
網站必須啟用https來保證使用Service worker頁面的安全性,開發時localhost預設認為是安全的。
- non-block
Service worker 中的 Javascript 程式碼必須是非阻塞的,因為 localStorage 是阻塞性,所以不應該在 Service Worker 程式碼中使用 localStorage。
- 單獨的執行環境
Service worker執行在自己的全域性環境中,通常也執行在自己單獨的執行緒中。
- 沒有繫結到特定頁面
service work能控制它所載入的整個範圍內的資源。
- 不能操作DOM
跟DOM所處的環境是相互隔離的。
- 沒有瀏覽頁面時也可以執行
接收系統事件,後臺執行
- 事件驅動,需要時執行,不需要時就終止
按需執行,只在需要時載入到記憶體
- 可升級
執行時會非同步獲取最新的版本
實現加速/離線
Cache
網頁快取有很多,如HTTP快取,localStorage,sessionStorage和cacheStorage都可以靈活搭配進行快取,但操作太繁瑣,直接使用更高階Service worker –本文的主人公。
新增Service worker入口
在web app的首頁新增以下程式碼
1 2 3 4 5 |
<script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } </script> |
如果瀏覽器支援serviceWorker就註冊它,不支援還是正常瀏覽,沒有Service worker所提供的增強功能。
Service worker控制範圍:
簡單情況下,將sw.js
放在網站的根目錄下,這樣Service worker可以控制網站所有的頁面,,同理,如果把sw.js
放在/my-app/sw.js
那麼它只能控制my-app
目錄下的頁面。
把sw.js
放在/js/
目錄呢?更好的目錄結構和範圍控制呢?
在註冊時指定js位置並設定範圍。
1 2 3 4 5 6 7 |
navigator.serviceWorker.register('/js/sw.js', {scope: '/sw-test/'}).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實現
監聽三個事件:
1 2 3 |
self.addEventListener('install', onInstall); self.addEventListener('fetch', onFetch); self.addEventListener("activate", onActivate); |
install
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
////////// // Install ////////// function onInstall(event) { log('install event in progress.'); event.waitUntil(updateStaticCache()); } function updateStaticCache() { return caches .open(cacheKey('offline')) .then((cache) => { return cache.addAll(offlineResources); }) .then(() => { log('installation complete!'); }); } |
install時將所有符合快取策略的資源進行快取。
fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//////// // Fetch //////// function onFetch(event) { const request = event.request; if (shouldAlwaysFetch(request)) { event.respondWith(networkedOrOffline(request)); return; } if (shouldFetchAndCache(request)) { event.respondWith(networkedOrCached(request)); return; } event.respondWith(cachedOrNetworked(request)); } onFetch做為瀏覽器網路請求的代理,根據需要返回網路或快取內容,如果獲取了網路內容,返回網路請求時同時進行快取操作。 |
activate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/////////// // Activate /////////// function onActivate(event) { log('activate event in progress.'); event.waitUntil(removeOldCache()); } function removeOldCache() { return caches .keys() .then((keys) => { return Promise.all( // We return a promise that settles when all outdated caches are deleted. keys .filter((key) => { return !key.startsWith(version); // Filter by keys that don't start with the latest version prefix. }) .map((key) => { return caches.delete(key); // Return a promise that's fulfilled when each outdated cache is deleted. }) ); }) .then(() => { log('removeOldCache completed.'); }); } |
在activate時根據version值來刪除過期的快取。
管理 Service worker
特定網站
- Google Chrome
Developer Tools
->Application
->Service Workers
,
在這裡還有三個非常有用的核取方塊:
- Offline
模擬斷網狀態
- Update on reload
載入時更新 - Bypass for network
總是使用網路內容
- Firefox
只有在Settings裡有一個可以在HTTP環境中使用Service worker的選項,適應於除錯,沒有單獨網站下的Service worker管理。
- Opera及其它雙核瀏覽器同Google Chrome
如果看到多個相同範圍內的多個Service worker,說明Service woker更新後,而原有Service worker還沒有被terminated。
瀏覽器全域性
看看你的瀏覽器裡都有哪些Service worker已經存在了
- Google Chrome
在位址列裡輸入:
1 |
chrome://serviceworker-internals/ |
可以看到已經有24個Service worker了,在這裡可以手動Start讓它工作,也可以Unregister解除安裝掉。
- Firefox
有兩種方式進入Service worker管理介面來手動Start或unregister。
- 選單欄,Tool->Web Developer->Service workers
- 位址列中輸入
1 |
about:debugging#workers |
- Opera及其它雙核瀏覽器同Google Chrome
更多
TODO:
- Service workers的更新需要手動編輯version,每次釋出新文章時需要編輯。
- 使用AMP讓頁面渲染速度達到最高。
Ref links
Chrome service worker status page
Firefox service worker status page