1. 先說說PWA盯蝴?
PWA = progressive web app = 漸進增強式web應(yīng)用 = 谷歌也想分app市場的一杯羹搞出的東西 = 用js寫本地app
由于是谷歌想搞點東西鳖敷,所以支持pwa的主要還是安卓系統(tǒng)和谷歌瀏覽器减牺。
單純依賴網(wǎng)頁的網(wǎng)站存在一些問題:
手機桌面入口不夠便捷,想要進入一個頁面必須要記住它的url或者加入書簽
沒網(wǎng)絡(luò)就沒響應(yīng),不具備離線能力
不像APP一樣能進行消息推送
基于以上幾個網(wǎng)頁應(yīng)用存在的問題,pwa橫空出世,其核心就是想要脫離瀏覽器入口環(huán)境成為可以離線運行的app妄呕,實現(xiàn)native app可以做到的事情。
那么說回來嗽测,pwa想要實現(xiàn)離線運行绪励,就必須要緩存能力與操控請求和響應(yīng)的權(quán)利,擁有安全高效的底層功能唠粥。
PWA的核心技術(shù)是ServiceWorker:
ServiceWorker給前端開發(fā)者開放了內(nèi)核大量的底層能力疏魏,比如,它給前端提供了細(xì)粒度操作請求緩存的底層原語厅贪,等同于給前端開放了操作HTTP Cache級別緩存的能力蠢护,與Fetch API結(jié)合,讓前端具備了完全操控請求养涮,響應(yīng)葵硕,緩存的能力眉抬,這點對于pwa的實現(xiàn)至關(guān)重要。因此serviceWorker是pwa的核心懈凹。
實際應(yīng)用中蜀变,為了兼容安卓與ios,serviceWorker的應(yīng)用最廣泛的還不在pwa上面介评,而是在網(wǎng)站的優(yōu)化方面库北。
ServiceWorker能在網(wǎng)頁優(yōu)化中干些啥?
Service Worker 是 Chrome 團隊提出和力推的一個 WEB API们陆,用于給 web 應(yīng)用提供高級的可持續(xù)的后臺處理能力寒瓦。
Service Worker 最主要的特點是:在頁面中注冊并安裝成功后,運行于瀏覽器后臺坪仇,不受頁面刷新的影響杂腰,可以監(jiān)聽和截攔作用域范圍內(nèi)所有頁面的 HTTP 請求。
基于 Service Worker API 的特性椅文,結(jié)合 Fetch API喂很、Cache API、Push API皆刺、postMessage API 和 Notification API少辣,可以在基于瀏覽器的 web 應(yīng)用中實現(xiàn)如離線緩存、消息推送羡蛾、靜默更新等 native 應(yīng)用常見的功能漓帅,以給 web 應(yīng)用提供更好更豐富的使用體驗。
特點
網(wǎng)站必須使用 HTTPS林说。除了使用本地開發(fā)環(huán)境調(diào)試時(如域名使用?localhost)??
運行于瀏覽器后臺煎殷,不受頁面刷新的影響屯伞,可以控制打開的作用域范圍下所有的頁面請求
單獨的作用域范圍腿箩,單獨的運行環(huán)境和執(zhí)行線程,可監(jiān)聽和攔截注冊Scope下的所有請求和響應(yīng)
不能操作頁面 DOM劣摇。但可以通過事件機制來處理
有獨立的與html無關(guān)的生命周期
生命周期
參考 Service Worker 的生命周期珠移,使用 Service Worker 大概需要如下幾個過程。
install -> installed -> actvating -> Active -> Activated -> Redundant
收到事件SW線程要啟動末融,也意味著事件處理完成钧惧,SW線程是需要關(guān)閉的。SW有獨立的GlobalScope勾习,獨立的Isolate浓瞪,獨立的JS運行環(huán)境,SW線程的資源消耗是非常大的巧婶,事件驅(qū)動是減少SW線程資源消耗的一種有效的方式乾颁,這就是SW被設(shè)計成事件驅(qū)動的原因涂乌。
生命周期包含兩部分,一部分是腳本英岭,一部分是線程湾盒。
SW腳本的狀態(tài)是存儲在數(shù)據(jù)庫的,打開頁面時诅妹,會先從數(shù)據(jù)庫中讀取當(dāng)前頁面activated狀態(tài)的SW腳本罚勾,然后再派發(fā)Fetch事件去啟動SW線程。SW要控制頁面吭狡,腳本是activated狀態(tài)尖殃,線程是running狀態(tài),兩者缺一不可划煮,而這兩者的生命周期都與頁面文檔無關(guān)分衫。這就是SW文檔無關(guān)生命周期的內(nèi)在涵義。
sw腳本(sw.js) =>? ?腳本狀態(tài)存儲數(shù)據(jù)庫LevelDB開啟(activated)? =>? 派發(fā)fetch事件啟動線程? =>?
sw線程? ?=>? ?事件驅(qū)動(running)=>? sw所控制的頁面
SW腳本激活之后會存儲相關(guān)信息到LevelDB數(shù)據(jù)庫般此,再次訪問頁面時蚪战,可以直接從注冊數(shù)據(jù)庫里讀取信息,然后派發(fā)Fetch事件去啟動SW線程铐懊,SW線程啟動完成之后邀桑,所有的Fetch請求都會觸發(fā)fetch事件,前端可以監(jiān)聽fetch事件科乎,按照各種策略去獲取資源壁畸。
攔截注冊Scope下所有的請求和響應(yīng)
Scope內(nèi)的頁面,所有的請求都會經(jīng)過SW茅茂,由內(nèi)部對象負(fù)責(zé)處理捏萍。內(nèi)部對象會檢查這些資源是否在SW緩存,如果在SW緩存空闲,就會創(chuàng)建讀取緩存的任務(wù)令杈,直接從SW緩存讀取碴倾;如果不在SW緩存逗噩,就會創(chuàng)建寫入緩存的任務(wù),繼續(xù)走到網(wǎng)絡(luò)流程跌榔,并將結(jié)果寫入SW緩存异雁。如果請求不受SW控制,會直接進入網(wǎng)絡(luò)請求僧须,走正常請求的流程纲刀。
具有可靠的能力
web前端服務(wù)常被認(rèn)為是不夠可靠(Reliable)的,網(wǎng)絡(luò)會斷担平,用戶停留時間無法掌控示绊,沒有本地緩存機制〗嬲酰現(xiàn)在有了sw了,這些問題都可以解決了耻台。讀不不讀取緩存空免,更不更新scope,都在web前端的掌控中盆耽。
可以精細(xì)的控制每個資源的緩存蹋砚,讓資源更可靠;
可以用push來預(yù)加載摄杂,讓資源減輕即時的網(wǎng)絡(luò)依賴杏慰;
可以使用sync觸發(fā)后臺更新膝迎;
可以使用fetch實現(xiàn)后臺的上傳下載
sw代碼實現(xiàn)
注冊
在網(wǎng)站頁面上注冊實現(xiàn) Service Worker 功能邏輯的腳本煤禽。例如注冊?/sw/sw.js?文件键兜,參考代碼:
f('serviceWorker'innavigator) {
????navigator.serviceWorker.register('/sw/sw.js', {scope: '/'})
????????.then(registration => console.log('ServiceWorker 注冊成功!作用域為: ', registration.scope))
????????.catch(err => console.log('ServiceWorker 注冊失敗: ', err));
}
以上代碼的作用就是告訴瀏覽器映挂,你要讀我的sw.js文件哦泽篮,里面有配置喲!
Service Worker 的注冊路徑?jīng)Q定了其?scope?默認(rèn)作用范圍柑船。示例中?sw.js?是在?/sw/?路徑下帽撑,這使得該 Service Worker 默認(rèn)只會收到?/sw/?路徑下的 fetch 事件。如果存放在網(wǎng)站的根路徑下鞍时,則將會收到該網(wǎng)站的所有 fetch 事件亏拉。
如果希望改變它的作用域,可在第二個參數(shù)設(shè)置 scope 范圍逆巍。示例中將其改為了根目錄及塘,即對整個站點生效。
另外應(yīng)意識到這一點:Service Worker 沒有頁面作用域的概念锐极,作用域范圍內(nèi)的所有頁面請求都會被當(dāng)前激活的 Service Worker 所監(jiān)控笙僚。
sw.js
const CACHE_NAME = "lzwme_cache_v1.0.0";? ??// 用于標(biāo)注創(chuàng)建的緩存,也可以根據(jù)它來建立版本規(guī)范
//?列舉要默認(rèn)緩存的靜態(tài)資源溪烤,一般用于離線使用
const urlsToCache = [ '/offline.html',? '/offline.png' ];
// self 為當(dāng)前 scope 內(nèi)的上下文
self.addEventListener('install', event => {
????// event.waitUtil 用于在安裝成功之前執(zhí)行一些預(yù)裝邏輯,?但是建議只做一些輕量級和非常重要資源的緩存味咳,減少安裝失敗的概率
????// 安裝成功后 ServiceWorker 狀態(tài)會從 installing 變?yōu)?installed
????event.waitUntil (
????????caches.open(CACHE_NAME).then(cache => {? ??// 使用 cache API 打開指定的 cache 文件
????????????console.log(cache);? ? ? // 添加要緩存的資源列表
????????????return cache.addAll(urlsToCache);
????????})
????);
});
sw.js?內(nèi)監(jiān)聽了?install?事件庇勃。當(dāng)?sw.js?被安裝時會觸發(fā)?install?事件檬嘀,監(jiān)聽該事件可執(zhí)行安裝時要做的事情。示例中是緩存用于離線時使用的靜態(tài)資源责嚷,這也是最常見的行為鸳兽。
需要注意的是,只有?urlsToCache?中的文件全部安裝成功罕拂,Service Worker 才會認(rèn)為安裝完成揍异。否則會認(rèn)為安裝失敗全陨,安裝失敗則進入?redundant?(廢棄)狀態(tài)。所以這里應(yīng)當(dāng)盡量少地緩存資源(一般為離線時需要但聯(lián)網(wǎng)時不會訪問到的內(nèi)容)衷掷,以提升成功率辱姨。
安裝成功后,即進入等待(waiting)或激活(active)狀態(tài)戚嗅。在激活狀態(tài)可通過監(jiān)聽各種事件雨涛,實現(xiàn)更為復(fù)雜的邏輯需求。具體參見后文事件處理部分懦胞。
更新
如果?sw.js?文件的內(nèi)容有改動替久,當(dāng)訪問網(wǎng)站頁面時瀏覽器獲取了新的文件,它會認(rèn)為有更新躏尉,于是會安裝新的文件并觸發(fā)?install?事件蚯根。但是此時已經(jīng)處于激活狀態(tài)的舊的 Service Worker 還在運行,新的 Service Worker 完成安裝后會進入 waiting 狀態(tài)胀糜。直到所有已打開的頁面都關(guān)閉颅拦,舊的 Service Worker 自動停止,新的 Service Worker 才會在接下來打開的頁面里生效教藻。
如果希望在有了新版本時矩距,所有的頁面都得到及時更新怎么辦呢?
可以在?install?事件中執(zhí)行?skipWaiting?方法跳過 waiting 狀態(tài)怖竭,然后會直接進入?activate?階段锥债。接著在?activate?事件發(fā)生時,通過執(zhí)行?clients.claim?方法痊臭,更新所有客戶端上的 Service Worker哮肚。
// 安裝階段跳過等待,直接進入 active
self.addEventListener('install', function(event) {
????event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', event => event.waitUntil(
????Promise.all([
????????// 更新客戶端
????????clients.claim(),
????????// 清理舊版本
????????caches.keys().then(cacheList => Promise.all(
????????????cacheList.map(cacheName => {
????????????????if(cacheName !== CACHE_NAME) {
????????????????????caches.delete(cacheName);
????????????????}
????????????})
????????))
????])
))
另外要注意一點广匙,sw.js?文件可能會因為瀏覽器緩存問題允趟,當(dāng)文件有了變化時,瀏覽器里還是舊的文件鸦致。這會導(dǎo)致更新得不到響應(yīng)潮剪。如遇到該問題,可嘗試這么做:在 webserver 上添加對該文件的過濾規(guī)則分唾,不緩存或設(shè)置較短的有效期抗碰。注意,不要想著不同版本使用不同的文件名稱绽乔,這會帶來混亂的問題弧蝇。
參考文章: