1.介紹
- PWA結(jié)合了最好的web體驗(yàn)和最好的app體驗(yàn),它對第一次使用某個(gè)app的用戶來說是非常有用的,因?yàn)椴恍枰惭b用戶僅在瀏覽器中訪問就可以,并且隨著用戶在PWA上的操作越來越多,交互越來越頻繁,PWA會變得越來越強(qiáng)大,即便在不穩(wěn)定的網(wǎng)絡(luò)下,它也可以快速加載,并且可以向用戶發(fā)送通知,以及在主屏幕創(chuàng)建一個(gè)圖標(biāo),并且可以全屏使用,提供沉浸式的體驗(yàn)。
什么是PWA
- 漸進(jìn)式的,每個(gè)人都可以使用PWA,無論你使用什么瀏覽器,因?yàn)镻WA的最終是想漸進(jìn)式的增強(qiáng)你的用戶體驗(yàn)。
- 多平臺,PWA適用于個(gè)人PC,平板式設(shè)備,智能手機(jī),甚至我們不知道的下一種設(shè)備。
- 獨(dú)立的網(wǎng)絡(luò)連接,增強(qiáng)式的服務(wù)使PWA可以在無線環(huán)境下或網(wǎng)絡(luò)及其不穩(wěn)定的環(huán)境下工作。
- 類本地應(yīng)用,因?yàn)镻WA就是按照本地app來設(shè)計(jì)的,所以你會覺著你在使用一個(gè)本地app。
- 保持最新,serive worker使得應(yīng)用總是保持在最新版本的狀態(tài)。
- 安全,PWA使用https進(jìn)行通信加密,防止了被第三方獲取數(shù)據(jù)以及數(shù)據(jù)被篡改。
- 尋找方式非常簡單,通過W3Cmanifests緩存的數(shù)據(jù)和serive worker的登記,PWA可以非常容易的搜索引擎里找到打開。
- 可復(fù)用性,通過PWA推送的通知,用戶可以再次訪問PWA。
- 可留存性,允許用戶將PWA在桌面上創(chuàng)建圖標(biāo),并且不必到應(yīng)用商店去下載搜索下載應(yīng)用。
- 易分享,通過URL就可以將PWA分享出去,不需要復(fù)雜的安裝。
下面的代碼實(shí)例將會和你一起創(chuàng)一個(gè)PWA,包括PWA創(chuàng)建的設(shè)計(jì)及規(guī)范以及注意事項(xiàng),來確保你的PWA符合應(yīng)用標(biāo)準(zhǔn)。
我們將要做什么
在這個(gè)代碼實(shí)例中,你將學(xué)會使用PWA技術(shù)去建立一個(gè)天氣APP,你將學(xué)到:
- 怎么用"app shell"去設(shè)計(jì)和開發(fā)一個(gè)PWA。
- 怎么讓你的app可以離線工作。
- 如何存儲數(shù)據(jù)以便在離線時(shí)也可以使用。
環(huán)境和要求
- 谷歌瀏覽器版本52或更高
- Web server for chrome或其他的網(wǎng)絡(luò)服務(wù)器。
- 實(shí)例代碼
- 代碼編輯器
- html,css,javascript以及調(diào)試工具的基本知識。
2.開始開發(fā)
下載源碼,你可以下載本項(xiàng)目的代碼通過 項(xiàng)目代碼 。
解壓你下載壓縮文件,將會解壓出來一個(gè)(your-first-pwapp-master)文件夾,這個(gè)文件夾包含了所有這個(gè)項(xiàng)目所需要的資源文件。
而名為step-NN的文件夾包含了這個(gè)項(xiàng)目所需要的步驟,你可以把它當(dāng)做參考。
安裝web server for chrome。
你可以通過chrome應(yīng)用商店安裝web server for chrome(具體方式是不可描述的—譯者語)。
安裝完成后,點(diǎn)擊
在chrome應(yīng)用商店會出現(xiàn)這個(gè)圖標(biāo)
點(diǎn)擊它,你會看到下面的對話框,來配置你的本地web服務(wù)器。
點(diǎn)擊CHOOSE FOLDER按鈕,選擇工作文件夾,就是剛才解壓出來的文件夾,這樣可以讓你在調(diào)試中,直觀地看出url來觀察PWA的運(yùn)行。
在選項(xiàng)中勾上Automatically show index.html,如圖所示。
然后通過Web Server:STARTED來開啟服務(wù)。
現(xiàn)在你就可以看到打開的第一個(gè)畫面,使用瀏覽器訪問工作文件夾就可以(點(diǎn)擊高亮的web server url)
顯然這個(gè)頁面上什么也沒有,它只是這個(gè)app的小骨架,接下來我們將會給這個(gè)app添加UI和各種功能。
3.開發(fā)你的APP Shell
什么是app shell
app的shell包含了構(gòu)建一個(gè)PWA所需要的最基本的html,css,javascript文件,并且是確保app有良好的性能的必要組件之一,它的第一次加載非常的快速,并且第一次加載后就能被緩存下來,這意味著在app shell第一次加載完成后,用戶再打開app后,app shell將從本地緩存中加載,這是非常快速的。
app shell架構(gòu)將app的基礎(chǔ)架構(gòu)和ui分離,所有的基礎(chǔ)架構(gòu)和ui都將在本地緩存,這樣在后續(xù)加載的時(shí)候,PWA只需要檢索必要的數(shù)據(jù),并不需要再次加載所有數(shù)據(jù)。
換句話說,app shell相當(dāng)于那些被存于應(yīng)用商店從來沒有被打開過的app,其中沒有數(shù)據(jù),一旦打開這個(gè)app就會記錄數(shù)據(jù)并且使用。
為何要使用app shell架構(gòu)?
使用app shell架構(gòu),可以使你專注于速度,并且賦予PWA近似于本地app的屬性,熱加載和定期更新,但是不需要應(yīng)用商店。
開發(fā) app shell
第一步是設(shè)計(jì)核心組件
問問自己?
- 什么是需要立刻呈現(xiàn)在屏幕上的
- 這個(gè)app需要什么重要的ui組件
- app shell需要什么js,css以及圖片等
我們將開發(fā)一個(gè)天氣app作為我們的第一個(gè)progress web app,關(guān)鍵的組件包括: - 頭部的標(biāo)題,添加以及刷新按鈕
- 天氣預(yù)報(bào)的卡片式容器
- 卡片式模板
- 添加城市時(shí)的對話框
- 加載時(shí)的動畫效果
當(dāng)你在設(shè)計(jì)更加復(fù)雜的應(yīng)用時(shí),第一次加載時(shí)可以不必加載不需要的資源,例如我們第一次可以不加載添加城市彈出的對話框,只有用戶在發(fā)起點(diǎn)擊時(shí)開始加載。
4.開始實(shí)現(xiàn)你的APP Shell
你的項(xiàng)目可以通過多種項(xiàng)目開始,我們通常使用Web Starter Kit,但是在這個(gè)例子里,為了讓你專注于PWA的開發(fā),我們?yōu)槟闾峁┛闪怂械馁Y源。
創(chuàng)建app shell的html部分
現(xiàn)在我們將添加app shell 架構(gòu)的核心部分,組件包括:
- 頭部的標(biāo)題,添加以及刷新按鈕
- 天氣預(yù)報(bào)的卡片式容器
- 卡片式模板
- 添加城市時(shí)的對話框
- 加載時(shí)的動畫效果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather PWA</title>
<link rel="stylesheet" type="text/css" href="styles/inline.css">
</head>
<body>
<header class="header">
<h1 class="header__title">Weather PWA</h1>
<button id="butRefresh" class="headerButton"></button>
<button id="butAdd" class="headerButton"></button>
</header>
<main class="main">
<div class="card cardTemplate weather-forecast" hidden>
. . .
</div>
</main>
<div class="dialog-container">
. . .
</div>
<div class="loader">
<svg viewBox="0 0 32 32" width="32" height="32">
<circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
</svg>
</div>
<!-- Insert link to app.js here -->
</body>
</html>
這是工作目錄的index.html文件(這只是整體項(xiàng)目的一部分,它已經(jīng)存在了,不需要再賦值。)
注意!默認(rèn)情況下,加載動畫是存在的,確保用戶在加載頁面時(shí)可以立即看到加載器,讓用戶清楚地明白內(nèi)容正在被加載。
為了節(jié)省時(shí)間,我們已經(jīng)創(chuàng)建了樣式表文件供你使用。
開始主要的javascript內(nèi)容
現(xiàn)在主要的ui已經(jīng)成功的構(gòu)建起來了(上文所提到的樣式表文件),看起來應(yīng)該添加一些代碼讓他開始工作了,就像前文所說,哪些文件應(yīng)該在首次運(yùn)行的時(shí)候就應(yīng)該被加載,而哪些可以延時(shí)加載。
在你的工作目錄里,打開,script/app.js文件
- 一個(gè)包含整個(gè)PWA的關(guān)鍵信息的app對象。
- 添加,刷新,取消城市的監(jiān)聽函數(shù)(add/refresh,add/cancel)。
- 添加或者更新天氣預(yù)報(bào)的方法(app.updateForecastCard)。
- 一個(gè)更新所有卡片數(shù)據(jù)的方法(app.getForecast,app.updateForecasts)。
- 一個(gè)從Firebase公開的天氣API上獲取數(shù)據(jù)的方法(app.updateForecasts)。
- 一些作為實(shí)例的假數(shù)據(jù)(fakeForecast)。
測試一下
現(xiàn)在你已經(jīng)完成了完整的html,css和javascript,是時(shí)候去測試這個(gè)app了。
如果你想看看假數(shù)據(jù)是怎么被渲染的,可以在index.html中取消以下代碼的注釋。
<!--<script src="scripts/app.js" async></script>-->
然后從app.js中取消一下代碼的注釋
// app.updateForecastCard(initialWeatherForecast);
然后刷新下你的應(yīng)用,你將會看到下面這個(gè)漂亮的卡片。
當(dāng)你驗(yàn)證其工作正常后,可以app.updateForecastCard的假數(shù)據(jù)清除,我們僅僅是想確保每個(gè)組件都可以正常工作。
5.從第一次快速加載開始
Progressive Web Apps 應(yīng)該快速啟動并且立即可以使用,在當(dāng)前的狀態(tài)下,我們的天氣app啟動的非常快速,但是它還是不能夠使用,因?yàn)闆]有數(shù)據(jù),這時(shí)我們可以創(chuàng)建一個(gè)ajax請求來獲取數(shù)據(jù),但是額外的請求就會使加載時(shí)間變長,我們想一個(gè)辦法,在初次加載的時(shí)候,就給用戶提供真實(shí)的數(shù)據(jù)。
加入天氣預(yù)報(bào)數(shù)據(jù)
在這個(gè)代碼實(shí)例中,我們模擬服務(wù)器直接將數(shù)據(jù)注入javascript,但是在用戶的設(shè)備上運(yùn)行的時(shí)候,最新的天氣預(yù)報(bào)數(shù)據(jù)將根據(jù)用戶的ip來確定位置以便注入。
代碼已經(jīng)包括了我們要注入的數(shù)據(jù),就是我們上一步所使用的方法(initialWeatherForecast)。
如何區(qū)分是不是首次運(yùn)行
但是我們不知道什么時(shí)候展示這些信息,將數(shù)據(jù)版存到本地以供下次使用么?如果用戶下次使用,城市發(fā)生了變更該如何,我們需要的是加載本城市的信息,而不是之前的城市。
用戶的第一選項(xiàng)如已經(jīng)訂閱的城市列表應(yīng)該使用IndexedDB或者其他快速的存儲方式存儲到本地,但是為了簡化代碼實(shí)例,我們使用了localstorage的方法來存儲數(shù)據(jù),但是這在實(shí)際運(yùn)行中并不是理想的環(huán)境,因?yàn)樗亲枞屯綑C(jī)制,在某些設(shè)備上可能會很慢。
下面讓我們來添加存儲用戶訂閱城市的代碼,找到以下注釋
// TODO add saveSelectedCities function here
然后將下列代碼復(fù)制到該注釋下,如下
// TODO add saveSelectedCities function here
app.saveSelectedCities = function() {
var selectedCities = JSON.stringify(app.selectedCities);
localStorage.selectedCities = selectedCities;
};
接下來我們需要添加一些代碼檢查用戶是否已經(jīng)添加了某城市,并且渲染這些城市的數(shù)據(jù),找到以下注釋。
// TODO add startup code here
然后在注釋下添加這些代碼
/************************************************************************
*
* Code required to start the app
*
* NOTE: To simplify this codelab, we've used localStorage.
* localStorage is a synchronous API and has serious performance
* implications. It should not be used in production applications!
* Instead, check out IDB (https://www.npmjs.com/package/idb) or
* SimpleDB (https://gist.github.com/inexorabletash/c8069c042b734519680c)
************************************************************************/
app.selectedCities = localStorage.selectedCities;
if (app.selectedCities) {
app.selectedCities = JSON.parse(app.selectedCities);
app.selectedCities.forEach(function(city) {
app.getForecast(city.key, city.label);
});
} else {
/* The user is using the app for the first time, or the user has not
* saved any cities, so show the user some fake data. A real app in this
* scenario could guess the user's location via IP lookup and then inject
* that data into the page.
*/
app.updateForecastCard(initialWeatherForecast);
app.selectedCities = [
{key: initialWeatherForecast.key, label: initialWeatherForecast.label}
];
app.saveSelectedCities();
}
該代碼用來檢查該城市是否保存在訂閱列表中,如果有,渲染出該城市的卡片,如果沒有則渲染假數(shù)據(jù),并保存到默認(rèn)卡片中。
保存所添加的數(shù)據(jù)
最后,你需要添加 ’添加城市‘ 按鈕,將所要的添加的城市保存到本地。
更新 butAddCity 里的代碼如下
document.getElementById('butAddCity').addEventListener('click', function() {
// Add the newly selected city
var select = document.getElementById('selectCityToAdd');
var selected = select.options[select.selectedIndex];
var key = selected.value;
var label = selected.textContent;
if (!app.selectedCities) {
app.selectedCities = [];
}
app.getForecast(key, label);
app.selectedCities.push({key: key, label: label});
app.saveSelectedCities();
app.toggleAddDialog(false);
});
如果該app存在將會初始化app.selectedCities,并且執(zhí)行app.selectedCities.push() 和app.saveSelectedCities()。
測試
- 當(dāng)?shù)谝淮芜\(yùn)行時(shí),應(yīng)用立刻向用戶展示,initialWeatherForecast 中的天氣數(shù)據(jù)。
- 添加一個(gè)新的城市后確保會展示兩個(gè)卡片。
- 刷新瀏覽器來確保加載了最新的數(shù)據(jù)。
6使用serive worker來緩存app shell
Progressive Web Apps是非常快速并且可以加載在本地的,這意味著它可以在在線,離線,以及不穩(wěn)定的網(wǎng)絡(luò)情況下使用,為了實(shí)現(xiàn)這個(gè)目標(biāo),我們使用了一個(gè)serive worker(PWA服務(wù))來緩存app shell,來確保始終保持可用狀態(tài)并且機(jī)器可靠。
如果你對service worker不熟悉,你可以通過閱讀service worker 來了解它可以做什么并且它的生命周期是怎么工作的等等,如果你完成看了這個(gè)代碼實(shí)例,一定要查看 Debugging Service Workers code lab 來深入了解。
service workers提供了一種獎金是增強(qiáng)的特征,這些特性僅僅作用于支持service workers的瀏覽器,比如,使用service workers你可以緩存app shell和你的應(yīng)用所需要的數(shù)據(jù),所以這些數(shù)據(jù)即便在離線的環(huán)境下也可以工作。如果瀏覽器不支持service workers,支持離線的代碼并沒有工作,用戶也能得到一個(gè)基本的用戶體驗(yàn),并且檢測你所使用的瀏覽器的時(shí)候會花費(fèi)近本可以忽略不計(jì)的性能,對你所使用的瀏覽器也沒有任何影響。
注冊service worker
為了讓應(yīng)用可以離線工作,要做的第一件事情就是注冊一個(gè)service worker,這是一段在后臺運(yùn)行的腳本程序,并不要用戶去打開它,也不需要任何的操作。
這只需要兩步
- 創(chuàng)建一個(gè)js文件來運(yùn)行service worker。
- 聲明這個(gè)js文件是service worker。
第一步,在跟目錄下創(chuàng)建一個(gè)空文件叫做service-worker.js。這個(gè)文件必須放在根目錄。因?yàn)閟ervice worker的作用域范圍是跟它所在的位置來決定的。
然后,需要檢查瀏覽器是不是支持service worker,如果支持會注冊service worker,將下面的代碼添加至app.js中。
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(function() { console.log('Service Worker Registered'); });
}
緩存站點(diǎn)的資源
當(dāng)service worker被注冊以后,用戶首次訪問頁面的時(shí)候,一個(gè)install事件函數(shù)就會被觸發(fā)。在這個(gè)事件的回調(diào)函數(shù)中,我們能夠緩存所有的應(yīng)用需要用到的資源。
當(dāng)service worker被激活后,他應(yīng)該打開緩存的對象,并且將其所需要i的資源存儲進(jìn)去。將下面的代碼加入到service-worker.js(你可以在your-first-pwapp-master/work中找到) :
var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
首先我們需要使用caches.open()打開cache對象,并且定義一個(gè)cache的名稱,這樣我們就可以給cache文件迭代本本,或者將數(shù)據(jù)分離,以至于我們能夠輕松地升級數(shù)據(jù)而不影響其他數(shù)據(jù)。
一旦cache數(shù)據(jù)被打開,我們可以調(diào)用 cache.addAll() 并且往其中傳入一個(gè)url列表,然后加載這些資源。但是,一旦 cache.addAll() 操作失敗,那么整個(gè)cache加載都會失敗(原文是cache.addAll() is atomic,我的理解是單線程的,就是說一旦其中一個(gè)緩存失敗,那么其他都會無法繼續(xù)--譯者)。
ok,讓我們來熟悉控制臺并學(xué)習(xí)怎么調(diào)試service workers。在你刷新頁面之前,打開控制臺,找到Appliction,并且打開Service worker的選項(xiàng)。如圖
如果你看到的是上圖這樣的頁面的話,說明你打口的頁面沒有已經(jīng)注冊的Service worker
你需要重新加載頁面,Service worker的選項(xiàng)應(yīng)該如下圖所示。
當(dāng)你看到控制臺如上圖顯示的話,這意味著service worker已經(jīng)正在工作了。
現(xiàn)在讓我們開始展示你在使用service worker可能遇到的問題,為了演示問題所在,請?jiān)趕ervice-worker.js的install事件下面添加一個(gè)activate監(jiān)聽器。
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
});
在service-worker開始運(yùn)行的時(shí)候,activate監(jiān)聽事件就會被激活。
打開控制臺,然后重新刷新下網(wǎng)頁,然后找到Application選項(xiàng),找到service workers選項(xiàng),在已經(jīng)被激活的service workers上,點(diǎn)擊inspect(如果你沒找到點(diǎn)擊show all,然后每個(gè)服務(wù)都有一個(gè)start,點(diǎn)擊start就有inspect了-譯者語),理論上說,控制開會出現(xiàn)[ServiceWorker] Activate這樣的信息,但是并沒有出現(xiàn),現(xiàn)在你回到service worker選項(xiàng),你會發(fā)現(xiàn)一個(gè)新service worker正處于等待的狀態(tài)(包括activate監(jiān)聽事件也在這個(gè)狀態(tài))。
一般來說,只要頁面的還有一個(gè)tab的話,service worker會一直工作,所以你可以關(guān)閉然后再重新打開頁面或者點(diǎn)擊skipWaiting按鈕,但是有一個(gè)更加簡單的辦法可以讓你不必這么麻煩的操作,啟用update on reload(在service worker下第一行第二個(gè)選項(xiàng)--譯者語),當(dāng)你啟用update on reload這項(xiàng)服務(wù)后,頁面在每次刷新后都會強(qiáng)制更新。
現(xiàn)在開啟update on reload并且確定新service worker已經(jīng)被激活了。
注意:你可能會看到service worker出現(xiàn)一個(gè)錯誤,忽略這個(gè)錯誤就行,它是安全的(如下圖)。
以上就是控制臺關(guān)于調(diào)試app的一些方法,稍后哦我們會向你展示一些技巧。現(xiàn)在讓我們回到怎么構(gòu)建應(yīng)用程序。
好了,現(xiàn)在讓我們來完成activate事件監(jiān)聽函數(shù)的一些邏輯代碼用來更新緩存,用下面的代碼來更新。
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
return self.clients.claim();
});
一旦你的app shell變化,上述的代碼會確保你的service worker緩存也可以跟著更新。
最后,讓我們更新一下app shell所要求的文件列表,這需要你的app所用的所有文件,包括圖片,js文件,css文件等,在你的service-worker.js文件頭,用以下代碼代替 var filesToCache = []
var filesToCache = [
'/',
'/index.html',
'/scripts/app.js',
'/styles/inline.css',
'/images/clear.png',
'/images/cloudy-scattered-showers.png',
'/images/cloudy.png',
'/images/fog.png',
'/images/ic_add_white_24px.svg',
'/images/ic_refresh_white_24px.svg',
'/images/partly-cloudy.png',
'/images/rain.png',
'/images/scattered-showers.png',
'/images/sleet.png',
'/images/snow.png',
'/images/thunderstorm.png',
'/images/wind.png'
];
到這里我們的app其實(shí)還不能工作,我們已經(jīng)緩存了app shell的組件,但是我們?nèi)匀恍枰獜谋镜鼐彺嬷屑虞d他們。
從緩存中加載app shell
service worker可以收到我們從PWA中發(fā)起的請求,并且響應(yīng),這意味著我們可以怎樣處理這些請求,并且什么樣的請求可以被緩存下來。
例如:
self.addEventListener('fetch', function(event) {
// Do something interesting with the fetch here
});
讓我們來更新app shell,將下面的代碼加入service-worker.js 中
self.addEventListener('fetch', function(e) {
console.log('[ServiceWorker] Fetch', e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});
從里到外,caches.match()方法從請求觸發(fā)的fetch事件中拿到情怯的內(nèi)容,并且去判斷請求的資源是否存在于緩存中,然后以我們緩存中的文件作為響應(yīng),或者使用fetch函數(shù)來加載資源(如果緩存中沒有該資源的話)。而返回的數(shù)據(jù)最終通過e.respondWith()返回給頁面。
測試吧
現(xiàn)在你的app已經(jīng)可以在離線模式下使用了!讓我們來試一試吧!
首先你要刷新下你的頁面,然后點(diǎn)擊Application面板找到cache storage,并且展開該部分,你應(yīng)該在左邊會看到你的app shell緩存的名稱。當(dāng)你惦記你的app shell緩存,你將會看到所有已經(jīng)被緩存的資源。
現(xiàn)在,讓我們開始離線測試。回到控制臺的service worker選項(xiàng),啟動offline的復(fù)選框,你將會看到network選項(xiàng)邊上有一個(gè)黃色的警告圖標(biāo),這表示你處于離線狀態(tài)。
然后刷新頁面,你就會發(fā)現(xiàn)你的頁面也可以正常的去操作。
而下一步就是修改app本身的和service worker的邏輯,讓天氣對象的數(shù)據(jù)可以被保存下來,并且可以在app處于離線狀態(tài)的時(shí)候,將最新的緩存數(shù)據(jù)顯示出來。
TIPS:如果你要清除所有保存的數(shù)據(jù)的話(localstorage,indexDB以及緩存的文件),并且刪除任何service worker,你可以控制臺的Application選項(xiàng)上點(diǎn)擊Clear storage清除數(shù)據(jù)。
當(dāng)心邊界條件問題
之前提到過,這段代碼一定不要用在生產(chǎn)環(huán)境下,因?yàn)橛性S多的邊界條件問題。
緩存依賴于每次修改內(nèi)容后緩存鍵的改變
例如,緩存的方法要求你在每次內(nèi)容變化之后更新鍵值,否則,緩存不會變化,并且重新提供舊的內(nèi)容,所以確保你的項(xiàng)目中鍵值在每次內(nèi)容更新后都會變化。每次修改后緩存的資源都會被重新下載
另一個(gè)問題就是,當(dāng)一個(gè)文件被修改后,整個(gè)緩存也要被重新的下載。這就意味著你即使有一個(gè)簡單的拼寫錯誤,也會讓整個(gè)緩存重新的下載,這很影響效率。瀏覽器的緩存可能會阻止service worker緩存的更新
還有一個(gè)重要的問題,第一次訪問的時(shí)候,請求的資源是直接經(jīng)過htpps加密的,這個(gè)時(shí)候可能不會返回緩存的資源,除此之外,瀏覽器可能返回舊的緩存資源,這就導(dǎo)致了service worker不會被更新。在生產(chǎn)環(huán)境中采取cache-first的策略
我們的app使用了優(yōu)先緩存的策略,這導(dǎo)致了所有的后續(xù)請求,都會從緩存中返回而不會去請求網(wǎng)絡(luò)。cache-first的策略很容易實(shí)現(xiàn),但是也會為將來帶來諸多問題。一旦主頁和注冊的service worker被緩存下來,去修改service worker的配置非常困難(因?yàn)閟ervice worker的配置依賴于它的位置),你就會發(fā)現(xiàn)你的app很難進(jìn)行迭代升級。
我應(yīng)該如何避免這些問題呢。
我們應(yīng)該如何避免這些問題呢?比如使用一個(gè)叫sw-precache的庫,他可以幫助你精密地控制資源的生命周期,能夠確保請求直接訪問網(wǎng)絡(luò),并且?guī)湍闾幚硭屑值膯栴}。
實(shí)時(shí)調(diào)試service worker
調(diào)試service worker是一件有挑戰(zhàn)性的東西,當(dāng)你涉及到緩存之后,你想要緩存進(jìn)行更新,但是實(shí)際上它沒有進(jìn)行更新,事情就會變得像一場噩夢一樣。在service worker典型的生命周期和你的代碼之間,你很快就會受挫。但是幸運(yùn)的是,有一些工具可以幫助你處理這些事情。
重新開始
TIPS:如果你要清除所有保存的數(shù)據(jù)的話(localstorage,indexDB以及緩存的文件),并且刪除任何service worker,你可以控制臺的Application選項(xiàng)上點(diǎn)擊Clear storage清除數(shù)據(jù)。(上面有了,只是又出現(xiàn)了,只好跟著又翻譯一遍--譯者語)
一些其他的問題
- 一旦service worker被注銷掉,它會一直保留直到瀏覽器被關(guān)閉。
- 如果你打開了多個(gè)窗口,除非你對其進(jìn)行了刷新,使用了新的service worker,新的service worker才會開始工作,
- 注銷一個(gè)service worker不會清空緩存,所以如果緩存的鍵值沒有進(jìn)行修改的話,你可能獲得的還是舊的數(shù)據(jù)。
- 如果一個(gè)service worker已經(jīng)存在,除非你使用immediate control的方式或者刷新頁面,否則新注冊的service worker不會立即接替控制。
7.使用Service Worker來緩存應(yīng)用數(shù)據(jù)
選擇一個(gè)正確的 caching strategy 很重要,它取決你在應(yīng)用中使用的數(shù)據(jù)類型。比如像天氣信息,股票信息這種對實(shí)時(shí)性要求很高的數(shù)據(jù),應(yīng)該經(jīng)常被刷新,但是例如用戶的頭像或者文字的內(nèi)容應(yīng)該以較低的頻率來刷新一樣。
cache-first-then-network這個(gè)策略是一個(gè)理想的選擇(欽點(diǎn)的(?? . ??)--譯者語),這個(gè)方法拿到數(shù)據(jù)非常的快速,然后返回新的數(shù)據(jù),與現(xiàn)請求網(wǎng)絡(luò)再去緩存相比,用戶不需要等很長時(shí)間就可以拿到數(shù)據(jù)。
cache-first-then-network需要我們發(fā)起兩個(gè)異步請求,一個(gè)請求緩存,一個(gè)請求網(wǎng)絡(luò)。我們應(yīng)用中的網(wǎng)絡(luò)請求不能被修改,但是我們要修改一下service-worker.js的緩存請求代碼。
一般情況下,請求應(yīng)該立即返回緩存的數(shù)據(jù),提供app能夠使用的最新的數(shù)據(jù),然后當(dāng)網(wǎng)絡(luò)請求返回后存到緩存中以供下次調(diào)用。(就是說刷新一下,拿到的是上次網(wǎng)絡(luò)請求返回的數(shù)據(jù),而后臺這邊又會發(fā)起一個(gè)請求拿到目前最新的數(shù)據(jù)保存到緩存中,一定程度上解決了網(wǎng)絡(luò)請求慢的問題--譯者語)
攔截網(wǎng)絡(luò)請求之后使用緩存來響應(yīng)
我們需要修改service worker來攔截對天氣API的請求,然后把其請求該API的結(jié)果存儲下來,以便我們以后調(diào)用,那么在 cache-first-then-network的策略下,我們希望請求返回給我們的是最新的數(shù)據(jù),如果不是,那么也沒事,因?yàn)槲覀円呀?jīng)把數(shù)據(jù)存在了緩存里,直接調(diào)用就行了。
在service worker里,我們添加一個(gè)dataCacheName變量,以至于我們可以從app shell中將應(yīng)用數(shù)據(jù)分離出來。當(dāng)你的app shell更新了,其緩存消失了,但是你的數(shù)據(jù)還在,不會受到影響,并可以隨時(shí)調(diào)用。記住,若是將來你的數(shù)據(jù)格式改變了,你需要一種能讓app shell和應(yīng)用數(shù)據(jù)保持同步的辦法。
將下面的代碼加入到service-worker.js中
var dataCacheName = 'weatherData-v1';
接下來,我們需要更新activate的回調(diào),以防刪除appshell的緩存之后,應(yīng)用數(shù)據(jù)也會被刪除。
if (key !== cacheName && key !== dataCacheName) {
(原文中就是這樣的,不過我認(rèn)為應(yīng)該是少了一個(gè)括號--譯者語)
最后,來修改fetch事件的回調(diào),添加代碼來將請求數(shù)據(jù)API和其他的請求分開來。
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetch', e.request.url);
var dataUrl = 'https://query.yahooapis.com/v1/public/yql';
if (e.request.url.indexOf(dataUrl) > -1) {
/*
* When the request URL contains dataUrl, the app is asking for fresh
* weather data. In this case, the service worker always goes to the
* network and then caches the response. This is called the "Cache then
* network" strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
*/
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response){
cache.put(e.request.url, response.clone());
return response;
});
})
);
} else {
/*
* The app is asking for app shell files. In this scenario the app uses the
* "Cache, falling back to the network" offline strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
*/
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
}
});
上面的代碼對請求進(jìn)行攔截,判斷請求的URL是否為天氣API,如果是的話,我們就使用fetch發(fā)起新的請求,一旦有響應(yīng)返回,就會將其存入到緩存,然后把請求返回給原請求。
其實(shí)應(yīng)用到現(xiàn)在還不能正式運(yùn)行呢(啊,我去--譯者語),我們雖然已經(jīng)實(shí)現(xiàn)了從app shell拿取緩存,但是即使我們緩存了數(shù)據(jù),依然需要通過網(wǎng)絡(luò)來發(fā)起請求。(第一次需要通過網(wǎng)絡(luò),但是在這之后,你每次查看天氣,它不光會緩存目前的,還會緩存未來幾個(gè)小時(shí)的,也就是說未來幾個(gè)小時(shí)即使你沒有網(wǎng)絡(luò)也能看天氣,哇,牛!--譯者語)
發(fā)起網(wǎng)絡(luò)請求
之前提到過,app需要啟動兩個(gè)異步請求,一個(gè)訪問緩存,一個(gè)訪問網(wǎng)絡(luò)。可以訪問最新的緩存亦可以訪問網(wǎng)絡(luò),這就是漸進(jìn)式增強(qiáng)的一個(gè)很好的例子,因?yàn)榫彺婵赡懿皇窃谒械臑g覽器都可以使用,若不能使用的話,網(wǎng)絡(luò)請求仍然可以很好地使用。
為了實(shí)現(xiàn)網(wǎng)絡(luò)請求,我們需要做
- 檢查window全局對象是否有caches對象
- 向緩存發(fā)起請求
- 如果服務(wù)器的請求沒有返回任何結(jié)果,需要使用本地緩存。
- 向服務(wù)器發(fā)起請求。
- 保存在數(shù)據(jù)本地以便調(diào)用。
- 熱更新。
從緩存抓取數(shù)據(jù)
接下來我們要檢查是否存在caches這個(gè)對象并且拿到最新的緩存數(shù)據(jù),找到TODO add cache logic here comment 中,它在app.getForecast()方法中,加入下面的代碼
if ('caches' in window) {
/*
* Check if the service worker has already cached this city's weather
* data. If the service worker has the data, then display the cached
* data while the app fetches the latest data.
*/
caches.match(url).then(function(response) {
if (response) {
response.json().then(function updateFromCache(json) {
var results = json.query.results;
results.key = key;
results.label = label;
results.created = json.query.created;
app.updateForecastCard(results);
});
}
});
}
這時(shí)應(yīng)用會做出兩個(gè)請求,一個(gè)是XHR到天氣API,一個(gè)是緩存請求,若緩存中有數(shù)據(jù),其返回非常快,然后更新卡片的數(shù)據(jù),這都是當(dāng)天氣API的請求未返回的情況下,一旦天氣數(shù)據(jù)從服務(wù)端返回,就會更新卡片的信息。
注意!我們知道網(wǎng)絡(luò)請求和緩存請求都更新了天氣數(shù)據(jù),但是app怎么知道哪一個(gè)是最新的呢?下面的代碼就解決了這個(gè)問題,將它添加到app.updateForecastCard:里。
var cardLastUpdatedElem = card.querySelector('.card-last-updated');
var cardLastUpdated = cardLastUpdatedElem.textContent;
if (cardLastUpdated) {
cardLastUpdated = new Date(cardLastUpdated);
// Bail if the card has more recent data then the data
if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) {
return;
}
}
每次卡片數(shù)據(jù)更新后,app存儲的時(shí)間戳卡片的數(shù)據(jù)中,app只要判斷時(shí)間戳是不是已經(jīng)存在就行了。
試一試吧
現(xiàn)在這個(gè)app已經(jīng)可以實(shí)現(xiàn)完整的離線功能了,保存幾個(gè)城市,然后點(diǎn)擊刷新按鈕來刷新數(shù)據(jù),然后在離線的環(huán)境下刷新新app再試試。
之后去cache storage頁面(控制臺的application下),展開觀察,在左邊,你應(yīng)該可以看到你的app shell緩存的名稱,當(dāng)你點(diǎn)擊它,你會看到所有已經(jīng)被緩存的資源。
8.如何在原生應(yīng)用集成PWA
9.上線和慶祝。。。
未完待續(xù)。。。。