[譯]前端離線指南(上)html
原文連接:The offline cookbook 做者:Jake Archibald前端
爲您的站點提供必定量的可用空間來執行其所需的操做。該可用空間可在站點中全部存儲之間共享:LocalStorage、IndexedDB、Filesystem,固然也包含Caches。html5
您能獲取到的空間容量是不必定的,同時因爲設備和存儲條件的差別也會有所不一樣。您能夠經過下面的代碼來查看您已得到的空間容量:git
navigator.storageQuota.queryInfo("temporary").then((info) => {
console.log(info.quota);
// Result: <quota in bytes>
console.log(info.usage);
// Result: <used data in bytes>
});
複製代碼
然而,與全部瀏覽器存儲同樣,若是設備面臨存儲壓力,瀏覽器就會隨時捨棄這些存儲內容。但遺憾的是,瀏覽器沒法區分您珍藏的電影,和您沒啥興趣的遊戲之間有啥區別。es6
爲解決此問題,建議使用 requestPersistent API:github
// 在頁面中運行
navigator.storage.requestPersistent().then((granted) => {
if (granted) {
// 啊哈,數據保存在這裏呢
}
});
複製代碼
固然,用戶必需要授予權限。讓用戶參與進這個流程是頗有必要的,由於咱們能夠預期用戶會控制刪除。若是用戶手中的設備面臨存儲壓力,並且清除不重要的數據還沒能解決問題,那麼用戶就須要根據本身的判斷來決定刪除哪些項目以及保留哪些項目。web
爲了實現此目的,須要操做系統將「持久化」源等同於其存儲使用空間細分中的本機應用,而不是做爲單個項目報告給瀏覽器。json
不管您打算緩存多少內容,除非您告訴ServiceWorker應當在什麼時候以及如何去緩存內容,ServiceWorker不會去主動使用緩存。下面是幾種用於處理請求的策略。api
適用於: 您認爲在站點的「該版本」中屬於靜態內容的任何資源。您應當在install
事件中就緩存這些資源,以便您能夠在處理請求的時候依靠它們。promise
self.addEventListener('fetch', (event) => {
// 若是某個匹配到的資源在緩存中找不到,
// 則響應結果看起來就會像一個鏈接錯誤。
event.respondWith(caches.match(event.request));
});
複製代碼
...儘管您通常不須要經過特殊的方式來處理這種狀況,但「緩存,回退到網絡」涵蓋了這種策略。
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
// 或者簡單地再也不調用event.respondWith,這樣就會
// 致使默認的瀏覽器行爲
});
複製代碼
...儘管您通常不須要經過特殊的方式來處理這種狀況,但「緩存,回退到網絡」涵蓋了這種策略。
self.addEventListener('fetch', (event) => {
event.respondWith(async function() {
const response = await caches.match(event.request);
return response || fetch(event.request);
}());
});
複製代碼
其中,針對已緩存的資源提供「Cache only」的行爲,針對未緩存的資源(包含全部非GET請求,由於它們根本沒法被緩存)提供「Network only」的行爲。
在老舊硬盤、病毒掃描程序、和較快網速這幾種因素都存在的狀況下,從網絡中獲取資源可能比從硬盤中獲取的速度更快。不過,經過網絡獲取已經在用戶設備中保存過的內容,是一種浪費流量的行爲,因此請牢記這一點。
// Promise.race 對咱們來講並不太好,由於若當其中一個promise在
// fulfilling以前reject了,那麼整個Promise.race就會返回reject。
// 咱們來寫一個更好的race函數:
function promiseAny(promises) {
return new Promise((resolve, reject) => {
// 確保promises表明全部的promise對象。
promises = promises.map(p => Promise.resolve(p));
// 只要當其中一個promise對象調用了resolve,就讓此promise對象變成resolve的
promises.forEach(p => p.then(resolve));
// 若是傳入的全部promise都reject了,就讓此promise對象變成resject的
promises.reduce((a, b) => a.catch(() => b))
.catch(() => reject(Error("All failed")));
});
};
self.addEventListener('fetch', (event) => {
event.respondWith(
promiseAny([
caches.match(event.request),
fetch(event.request)
])
);
});
複製代碼
適用於: 對頻繁更新的資源進行快速修復。例如:文章、頭像、社交媒體時間軸、遊戲排行榜等。
這就意味着您能夠爲在線用戶提供最新內容,可是離線用戶獲取到的是較老的緩存版本。若是網絡請求成功,您可能須要更新緩存。
不過,這種方法存在缺陷。若是用戶的網絡斷斷續續,或者網速超慢,則用戶可能會在從本身設備中獲取更好的、可接受的內容以前,花很長一段時間去等待網絡請求失敗。這樣的用戶體驗是很是糟糕的。請查看下一個更好的解決方案:「緩存而後訪問網絡
」。
self.addEventListener('fetch', (event) => {
event.respondWith(async function() {
try {
return await fetch(event.request);
} catch (err) {
return caches.match(event.request);
}
}());
});
複製代碼
這種策略須要頁面發起兩個請求,一個是請求緩存,一個是請求網絡。首先展現緩存數據,而後當網絡數據到達的時候,更新頁面。
有時候,您能夠在獲取到新的數據的時候,只替換當前數據(好比:遊戲排行榜),可是具備較大的內容時將致使數據中斷。基本上講,不要在用戶可能正在閱讀或正在操做的內容忽然「消失」。
Twitter在舊內容上添加新內容,並調整滾動的位置,以便讓用戶感知不到。這是可能的,由於 Twitter 一般會保持使內容最具線性特性的順序。 我爲 trained-to-thrill 複製了此模式,以儘快獲取屏幕上的內容,但當它出現時仍會顯示最新內容。 頁面中的代碼
async function update() {
// 儘量地發起網絡請求
const networkPromise = fetch('/data.json');
startSpinner();
const cachedResponse = await caches.match('/data.json');
if (cachedResponse) await displayUpdate(cachedResponse);
try {
const networkResponse = await networkPromise;
const cache = await caches.open('mysite-dynamic');
cache.put('/data.json', networkResponse.clone());
await displayUpdate(networkResponse);
} catch (err) {
}
stopSpinner();
const networkResponse = await networkPromise;
}
async function displayUpdate(response) {
const data = await response.json();
updatePage(data);
}
複製代碼
若是您未能從網絡和緩存中提供某些資源,您可能須要一個常規回退策略。
適用於: 次要的圖片,好比頭像,失敗的POST請求,「離線時不可用」的頁面。
self.addEventListener('fetch', (event) => {
event.respondWith(async function() {
// 嘗試從緩存中匹配
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
try {
// 回退到網絡
return await fetch(event.request);
} catch (err) {
// 若是都失敗了,啓用常規回退:
return caches.match('/offline.html');
// 不過,事實上您須要根據URL和Headers,準備多個不一樣回退方案
// 例如:頭像的兜底圖
}
}());
});
複製代碼
您回退到的項目多是一個「安裝依賴項」(見《前端離線指南(上)》中的「安裝時——以依賴的形式」小節)。
在服務器上渲染頁面可提升速度,但這意味着會包括在緩存中沒有意義的狀態數據,例如,「Logged in as…」。若是您的頁面由 ServiceWorker 控制,您可能會轉而選擇請求 JSON 數據和一個模板,並進行渲染。
importScripts('templating-engine.js');
self.addEventListener('fetch', (event) => {
const requestURL = new URL(event.request);
event.responseWith(async function() {
const [template, data] = await Promise.all([
caches.match('/article-template.html').then(r => r.text()),
caches.match(requestURL.path + '.json').then(r => r.json()),
]);
return new Response(renderTemplate(template, data), {
headers: {'Content-Type': 'text/html'}
})
}());
});
複製代碼
您沒必要只選擇其中的一種方法,您能夠根據請求URL選擇使用多種方法。好比,在trained-to-thrill中使用了:
只須要根據請求,就能決定要作什麼:
self.addEventListener('fetch', (event) => {
// Parse the URL:
const requestURL = new URL(event.request.url);
// Handle requests to a particular host specifically
if (requestURL.hostname == 'api.example.com') {
event.respondWith(/* some combination of patterns */);
return;
}
// Routing for local URLs
if (requestURL.origin == location.origin) {
// Handle article URLs
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (requestURL.pathname.endsWith('.webp')) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (request.method == 'POST') {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
new Response("Flagrant cheese error", {
status: 512
})
);
return;
}
}
// A sensible default pattern
event.respondWith(async function() {
const cachedResponse = await caches.match(event.request);
return cachedResponse || fetch(event.request);
}());
});
複製代碼
感謝下列諸君爲本文提供那些可愛的圖標:
同時感謝 Jeff Posnick 在我點擊「發佈」按鈕以前,爲我找到多處明顯錯誤。