[译]前端离线指南(上)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 在我点击“发布”按钮以前,为我找到多处明显错误。