引言
網(wǎng)頁緩存分為兩大類,分別是服務(wù)器緩存和客戶端緩存。SSR屬于瀏覽器緩存,service worker屬于瀏覽器緩存。
在第二次加載的時候,一般有HTTP緩存或者客戶端緩存,但是在弱網(wǎng)的情況下,HTTP緩存往往是不夠用的。很多app會考慮改變網(wǎng)絡(luò)框架的情況,優(yōu)先加載本地資源,再去檢查資源是否被更新,以此提高靜態(tài)資源加載效率。PWA中的service-worker可以被看作類似的一個代理,它改變了整個緩存結(jié)構(gòu)。目前,ios11.4已經(jīng)支持ServiceWorker和Web App Manifest,支持PWA添加到桌面。
Service Worker的未來是在用戶允許的基礎(chǔ)上,提供接近native app的功能,例如:
- web push
- background sync
web push
服務(wù)器可以定期給網(wǎng)頁推送消息,區(qū)別于其他的傳統(tǒng)網(wǎng)頁。用戶在打開瀏覽器時,不需要進入特定的網(wǎng)站,就能收到該網(wǎng)站推送而來的消息,例如:新評論,新動態(tài)等等。
background sync
background sync可延遲發(fā)送用戶行為,直到用戶網(wǎng)絡(luò)連接穩(wěn)定。這樣有助于保證用戶想要發(fā)送的數(shù)據(jù)就是實際發(fā)送的數(shù)據(jù)。
這些功能則會涵蓋在workbox的功能內(nèi)部。
歷史背景
有很多文章把pwa技術(shù)和小程序技術(shù)放在一起比較。谷歌瀏覽器至于pwa,微信至于小程序,都是給網(wǎng)頁應(yīng)用提供了離線緩存靜態(tài)資源文件的功能,動靜分離,native的接口,這些都是給網(wǎng)頁應(yīng)用提供更優(yōu)質(zhì)的加載性能。但是小程序并沒有BOM和DOM,意味著它對瀏覽器有著更深入的改造,它并非純正意義上的網(wǎng)頁應(yīng)用,是對所有Web開發(fā)資源的一種限制。
相反,pwa則不一樣。
兼容性情況

考慮到service worker是一個新的接口本身,肯定會存在兼容性問題。PWA的意思在于Progressive,也就是支持pwa的頁面則使用SW的緩存機制,而不支持的頁面使用原來的HTTP緩存機制。由于pwa是谷歌的“親兒子”,所以它在新版本安卓的各大瀏覽器都有非常好的支持。詳情我們可以參考lavas的兼容性報告
重點的重點當(dāng)然是微信瀏覽器對pwa的支持情況,我們可以看到除了推送信息和支付接口之外基本已經(jīng)實現(xiàn)支持(支付接口的支持應(yīng)該是出于安全的考慮,以及和weixin-js-sdk重疊的原因,X5瀏覽器支持它只是時間的問題)。如今我們更關(guān)心的是關(guān)于SW-cache這一部分,換句話說,我們可以放心在安卓微信上使用SW-cache的技術(shù)。

ios(蘋果)的支持
《震驚!蘋果向開發(fā)者低頭?!!開始支持Service Worker》一文中講述了蘋果的開發(fā)工程師開始完成研發(fā),并且在2017年底safari桌面技術(shù)預(yù)覽版上已經(jīng)實現(xiàn)了service worker的相關(guān)api,從In development的狀態(tài)轉(zhuǎn)移到Supported In Preview,這意味著service worker極有可能在IOS12得到支持(詳情https://webkit.org/status),這也就意味著pwa的時代很快就會到來。

Sw-precache和Workbox
sw-precache是什么?
workbox又是什么?
web前端的各位同學(xué)可能或多或少聽過pwa,聽過service worker(后面簡稱為SW),也知道對應(yīng)的生命周期。知道了這些api后,你還是不知道如何將pwa技術(shù)投入生產(chǎn)。它不僅僅是個玩具,它是一個“神器”,是用來拉近native和web app之間的差距。當(dāng)我們做spa項目越做越大的時候,JS bundle會越來越大,單頁面不能承載那么多的邏輯,我們可能會選擇多個單頁面(也就是多頁面)。每次加載都會存在空白加載的情景,雖然性能優(yōu)化上,我們能把這個時間減少到很少很少,但是沒法完全把它“干掉”。pwa的service-worker技術(shù)很好地彌補這片“空白”?!癮pp-shell”也就是web app中的應(yīng)用殼將會緩存在瀏覽器端,讓它的加載速度更加快速。而可變的內(nèi)容則是異步加載。
對比
我們知道vue-cli打造出來的pwa模版,使用的是sw-precache,而workbox是它的取代品。它們各自有一個webpack版的插件,分別是sw-precache-webpack-plugin和workbox-webpack-plugin。
結(jié)合Vue筆記八:多頁面打包框架的多頁面打包框架,我添加上precache的功能(以后計劃替換成為workbox),實現(xiàn)多頁面的service worker框架,github的地址是https://github.com/brandonxiang/mpa-pwa
我寫了一個關(guān)于workbox在vue-webpack框架的腳手架,github的地址是https://github.com/brandonxiang/example-vue-workbox,大家可以參考一下。
它們之間的區(qū)別如下,可以說非常相似:
| 中文說明 | workbox | 中文說明 | sw-precache |
|---|---|---|---|
| 緩存的目錄 | globDirectory | 緩存前綴 | stripPrefix |
| 緩存的靜態(tài)文件類型 | globPatterns | 緩存的靜態(tài)文件類型 | staticFileGlobs |
| sw文件路徑 | swDest | sw文件名 | filename |
| 讓sw立即接管網(wǎng)頁 | clientsClaim | (相同) | clientsClaim |
| 激活的等待 | skipWaiting | (相同) | skipWaiting |
| 動態(tài)請求 | runtimeCaching | (相同) | runtimeCaching |
sw-precache的主要開發(fā)者 jeffposnick 也是workbox的主要開發(fā)者,這說明了它們之間的關(guān)系,sw-precache是為了滿足service worker的cache API中的靜態(tài)資源文件的注冊作用。而workbox是為了滿足所有pwa的資源內(nèi)容,可以看作一個“平臺”。

Workbox是 GoogleChrome 團隊推出的一套 Web App 靜態(tài)資源本地存儲的解決方案,該解決方案包含一些 Js 庫和構(gòu)建工具,在 Chrome Submit 2017 上首次隆重面世,它已經(jīng)支持很多方面的內(nèi)容,當(dāng)然,還有很多內(nèi)容有待開發(fā)。而在它背后則是 Service Worker 和 Cache API 等技術(shù)和標(biāo)準(zhǔn)在驅(qū)動。在此之前,GoogleChrome 團隊較早時間推出過 sw-precache 和 sw-toolbox 庫,但是在 GoogleChrome 工程師們看來,Workbox 才是真正能方便統(tǒng)一的處理離線能力的更完美的方案,并停止了對 sw-precache 和 sw-toolbox 的維護,所以,將項目的的 SW 的打包控制插件升級到 WorkBox 是非常重要。該文主要提出以vue官方的pwa模版為基礎(chǔ),sw-precache升級成為workbox。整個升級的過程參考了lavas。
pwa的框架配置升級侵入性較少,基本上只需要改框架內(nèi)容,不需要修改代碼的內(nèi)容,詳情參考mpa-pwa。在實戰(zhàn)應(yīng)用中,往往不直接訪問service worker的生命周期,基于webpack插件去控制緩存。
緩存機制
Service Worker的出現(xiàn)很大程度,改變了web app的格局,HTTP cache和SW cache有著天壤之別。這樣的HTTP緩存機制沒法彌補網(wǎng)頁跳轉(zhuǎn)帶來的白屏間隙,SW cache由于優(yōu)先緩存靜態(tài)資源以及接口的機制,大大減少了網(wǎng)絡(luò)狀況差(甚至斷網(wǎng))帶來的白屏現(xiàn)象。優(yōu)先更新本地的同時,service worker會和后端進行一次通信,這次通信會告知靜態(tài)資源是否被更改,在下次刷新的時候更改內(nèi)容。
動態(tài)接口方面則會采用 runtimeCaching 進行交互,這部分也會進行動態(tài)內(nèi)容的緩存,sw-toolbox的代碼將會被引入你的sw.js中,它會利用正則表達式匹配到你請求的接口,進行接口緩存,當(dāng)該接口出現(xiàn)內(nèi)容變化時,SW會和后端進行一次通訊保證下一次加載的數(shù)據(jù)是最新數(shù)據(jù),這樣的更新機制分為5個類型。
- networkFirst
- cacheFirst
- fastest
- cacheOnly
- networkOnly
networkFirst是顯示完成后,SW優(yōu)先和后端通訊,看接口是否更新,下一次刷新則是采用最新數(shù)據(jù)內(nèi)容。cacheFirst則是優(yōu)先考慮緩存,如果緩存沒有命中,才會去請求接口拿新數(shù)據(jù),這個選型適合那種不經(jīng)常更改的內(nèi)容或者有別的更新機制。fastest則是兩個同時進行,哪個快執(zhí)行哪個。cacheOnly和networkOnly比較不常用。
項目中引入插件
在已有的項目的webpack.prod.conf.js中引入兩個webpack插件,其中,workbox-webpack-plugin是workbox的官方插件,處理項目中靜態(tài)文件的緩存及更新。只有在打包至測試環(huán)境和生產(chǎn)環(huán)境使用上service worker,但是在開發(fā)環(huán)境,無緩存和熱更新的調(diào)試會大大提高我們開發(fā)效率。
- workbox-webpack-plugin (workbox的官方插件)
- sw-register-webpack-plugin (sw的更新插件,確保更新緩存)
{
plugins: [
new workboxPlugin(config.sw.workbox),
new SwRegisterWebpackPlugin(config.sw.register),
]
}
config.sw.workbox指的是對應(yīng)的配置參數(shù)。它會配置在config文件夾的sw.js中,用于控制workbox。
const path = require('path');
const dist = './dist';
module.exports = {
workbox: {
globDirectory: dist,
globPatterns: ['**/*.{html,js,css}'],
swDest: path.join(dist, 'module/service-worker.js'),
clientsClaim: true,
skipWaiting: true,
},
register: {
filePath: path.resolve(__dirname, '../src/module/sw-register.js'),
prefix: '..',
output: 'module/sw-register.js',
excludes: [
/activitytemplate\.html/,
/addMember\.html/,
/detail\.html/,
/ecommand\.html/,
/infoDetail\.html/,
/insuredetail\.html/,
/invite\.html/,
/onlineBooking\.html/,
/productDetail\.html/,
/weappClientDetail\.html/,
],
}
};
參數(shù)說明
workbox-webpack-plugin參數(shù)
- globDirectory 緩存的目錄
- globPatterns 緩存的靜態(tài)文件類型, 可以是html,js,css等
- swDest sw生成后路徑
- clientsClaim sw立即接管網(wǎng)頁
- skipWaiting 新舊sw更新等待
sw-register-webpack-plugin參數(shù)
- filePath 文件路徑
- prefix 文件前綴,解決cdn路徑問題
- output sw-register輸出文件
- excludes 排除某些不需要sw的頁面
自定義更新sw-register.js
sw-register-webpack-plugin是百度處理更新 service worker 的一個方案,參考lavas。它會在html行內(nèi)注入sw-register.js,并加入時間戳,保證每次都能獲取到最新的sw文件,保障其他靜態(tài)文件更新。
<script>
window.onload = function () {
var script = document.createElement('script');
var firstScript = document.getElementsByTagName('script')[0];
script.type = 'text/javascript';
script.async = true;
script.src = '../module/sw-register.js?v=' + Date.now();
firstScript.parentNode.insertBefore(script, firstScript);
};
</script>
在配置文件中,自定義的sw-register.js會寫在module文件夾中。它的作用主要是sw線程和主線程通訊,主要通過postMessage,sw的線程鉤子是沒法在界面中顯示的??赡墚?dāng)靜態(tài)文件更新的時候,界面需要有所變化,或是提示,或是強制更新。都可以用自定義sw-register.js來完成。
navigator.serviceWorker && navigator.serviceWorker.register('./service-worker.js').then(() => {
navigator.serviceWorker.addEventListener('message', e => {
// service-worker.js 如果更新成功會 postMessage 給頁面,內(nèi)容為 'sw.update'
if (e.data === 'sw.update') {
let dom = document.createElement('div');
let themeColor = document.querySelector('meta[name=theme-color]');
themeColor && (themeColor.content = '#000');
dom.innerHTML = `
<style>
.app-refresh{background:#000;height:0;line-height:52px;overflow:hidden;position:fixed;top:0;left:0;right:0;z-index:10001;padding:0 18px;transition:all .3s ease;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;}
.app-refresh-wrap{display:flex;color:#fff;font-size:15px;}
.app-refresh-wrap label{flex:1;}
.app-refresh-show{height:52px;}
</style>
<div class="app-refresh" id="app-refresh">
<div class="app-refresh-wrap" onclick="location.reload()">
<label>已更新最新版本</label>
<span>點擊刷新</span>
</div>
</div>
`;
document.body.appendChild(dom);
setTimeout(() => document.getElementById('app-refresh').className += ' app-refresh-show', 16);
}
});
});