Add sw.js

缓存入口页、脚本样式和读过的文章与配图:文章先走网络,3 秒内没有返回或断网时用缓存,没缓存过的文章给出离线说明;学习站不经过这里。
This commit is contained in:
CyC2018
2026-11-01 13:04:24 +08:00
parent eb6c867348
commit 8b56ab502b
+69
View File
@@ -0,0 +1,69 @@
// 离线与弱网:安装时缓存入口页和本次构建的脚本、样式;读过的文章、侧边栏和配图在访问时缓存。
// 文章和入口页先走网络,3 秒内没有返回或断网时用缓存;带内容哈希的文件直接用缓存。学习站(/learn/)有自己的缓存,这里不处理。
// 下面的构建号和文件列表由 build.py 写入。
var BUILD = '__BUILD__';
var SHELL = __SHELL__;
// 带哈希的脚本、样式不按构建号分开存:缓存的旧入口页(弱网时先显示)引用的旧文件还要能读到,超过上限时删最早的
var STATIC = 'cs-notes-static', PAGES = 'cs-notes-pages', IMAGES = 'cs-notes-images';
var LIMITS = {};
LIMITS[STATIC] = 40;
LIMITS[PAGES] = 120;
LIMITS[IMAGES] = 300;
self.addEventListener('install', function (event) {
// 构建号 BUILD 写在这个文件里,构建变化时浏览器会重新安装
event.waitUntil(caches.open(STATIC).then(function (cache) { return cache.addAll(SHELL); }).then(function () { return trim(STATIC); }).then(function () { return self.skipWaiting(); }));
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
// 缓存条目超过上限时删掉最早放入的
function trim(name) {
return caches.open(name).then(function (cache) {
return cache.keys().then(function (keys) {
return Promise.all(keys.slice(0, Math.max(0, keys.length - LIMITS[name])).map(function (key) { return cache.delete(key); }));
});
});
}
function put(name, request, response) {
if (!response || !response.ok || response.type !== 'basic') return;
var copy = response.clone();
caches.open(name).then(function (cache) { return cache.put(request, copy); }).then(function () { if (LIMITS[name]) return trim(name); });
}
function offlinePage(path) {
var title = decodeURIComponent(path.split('/').pop().replace(/\.md$/, ''));
return new Response('# 当前离线\n\n《' + title + '》还没有在这台设备上打开过,联网后再试。已经读过的文章离线时也能打开。\n',
{ status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-CS-Notes-Offline': '1' } });
}
// 先走网络,等待超过 wait 毫秒或失败时用缓存(所有缓存中找);都没有时由 fallback 给出内容
function networkFirst(request, name, wait, fallback) {
return caches.match(request, { ignoreSearch: true }).then(function (cached) {
var network = fetch(request).then(function (response) { put(name, request, response); return response; });
var settled = network.catch(function () { return null; });
var timer = new Promise(function (resolve) { setTimeout(resolve, wait); });
return Promise.race(cached ? [settled, timer] : [settled]).then(function (first) {
if (first) return first;
if (cached) return cached;
return fallback ? fallback() : Response.error();
});
});
}
function cacheFirst(request, name) {
return caches.match(request).then(function (hit) {
return hit || fetch(request).then(function (response) { put(name, request, response); return response; });
});
}
self.addEventListener('fetch', function (event) {
var request = event.request, url = new URL(request.url);
if (request.method !== 'GET' || url.origin !== self.location.origin || url.pathname.indexOf('/learn/') === 0 || url.searchParams.has('release')) return;
var path = url.pathname;
if (request.mode === 'navigate') {
// 网站只有首页一个入口,其它地址(旧文章的 .html 链接等)照常走网络
if (path === '/' || path === '/index.html') event.respondWith(networkFirst(new Request('/'), STATIC, 3000, null));
} else if (/^\/(app|vendor|style)-[0-9a-f]{16}\.(js|css)$|^\/search-index-[0-9a-f]+\.json$|^\/optimized\//.test(path)) {
event.respondWith(cacheFirst(request, /^\/optimized\//.test(path) ? IMAGES : STATIC));
} else if (/\.md$/.test(path)) {
event.respondWith(networkFirst(request, PAGES, 3000, path === '/_sidebar.md' ? null : function () { return offlinePage(path); }));
} else if (/\.(png|jpe?g|gif|svg|webp)$/.test(path)) {
event.respondWith(networkFirst(request, IMAGES, 3000, null));
}
});