diff --git a/website/templates/sw.js b/website/templates/sw.js new file mode 100644 index 00000000..21136bee --- /dev/null +++ b/website/templates/sw.js @@ -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)); + } +});