本文中的SW经过一段时间测试应该是没有BUG了,不过我本人已经不再使用本文中编写的SW,故不再维护
对SW有兴趣的可以去看新版实现
更新内容
参考内容
本文参考了以下教程/文档,这些都是很不错的资源,读者可以自行参阅
- 店长写的Butterfly 主题的 PWA 实现方案
- service Worker API文档
- Cyfan写的欲善其事,必利其器 - 论如何善用ServiceWorker
- PWA 文档
教程
本文仅贴出本人使用的方案,其余方案请见店长写的教程
配置Json
说到生成图标第一步肯定是想办法弄到图标,有条件的小伙伴可以找别人帮自己设计一个,没有的话就跟我一样用工具生成吧:
这个网站生成的图标虽然下载要收费,但是并不妨碍我们截图
接下来就是根据图标生成我们要的图标包了,我们可以使用这个网站:
进入网站后点击那大大的Select your Favicon image
按钮,然后选择你处理好的图标文件。
如果你的图标不是正方形,网站会提醒你,如果满意网站的修复效果点击Continue with this picture
即可,不满意的话自己修改完再上传就可以了。
可能是我网络的问题,进入下一步的时候可能会出现白屏,刷新页面重新来一遍就行了。
然后向下滚动,找到Favicon for Android Chrome
栏目,点击里面的Assets
选项卡,勾选Create all documented icons
:
然后点击最下面的Generate your Favicons and HTML code
,等待Download your package
后面的按钮转好点击就嫩下载下来ZIP压缩包了。
将压缩包内的要用到的图标统统放到博客网站中,具体放到哪看个人喜好,然后把site.webmanifest
改名为manifest.json
并放到source
文件夹下。接下来修改manifest.json
的内容,这里给出我的Json,其中name
、start_url
是必须项目,同时图标目录一定要改成自己的(我没有512x512大小的图标所以我把那一项删了,有的话留着就行):
解释:
- name - 应用名称,用于安装横幅、启动画面显示
- short_name - 应用短名称,用于主屏幕显示
- description - 网站描述(目前我没发现在哪里派上用场了)
- theme_color - 主题颜色
- background_color - 背景色
- display - 显示方式:(
fullscreen
能占多少屏幕就占多少、standalone
独立应用、minimal-ui
带地址栏、browser
和浏览器一样) - scope - 作用域,保持默认即可,具体内容见参考资料
- start_url - 应用启动地址,保持默认即可,具体内容见参考资料
- icons - 图标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | { "lang": "en", "name": "\u5c71\u5cb3\u5e93\u535a", "short_name": "\u5c71\u5cb3\u5e93\u535a", "description": "kmar.top", "theme_color": "#242424", "background_color": "#242424", "display": "standalone", "scope": "/", "start_url": "/", "icons": [ { "src": "logo/android-chrome-36x36.png", "sizes": "36x36", "type": "image/png" }, { "src": "logo/android-chrome-48x48.png", "sizes": "48x48", "type": "image/png" }, { "src": "logo/android-chrome-72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "logo/android-chrome-96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "logo/android-chrome-144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "logo/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "logo/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" }, { "src": "logo/android-chrome-384x384.png", "sizes": "384x384", "type": "image/png" } ] }
|
修改配置文件
修改主题配置文件,注意里面的文件路径要改成自己的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | - # pwa: - # enable: false - # manifest: /pwa/manifest.json - # apple_touch_icon: /pwa/apple-touch-icon.png - # favicon_32_32: /pwa/32.png - # favicon_16_16: /pwa/16.png - # mask_icon: /pwa/safari-pinned-tab.svg + pwa: + enable: true + manifest: /manifest.json + apple_touch_icon: /logo/apple-touch-icon.png + favicon_32_32: /logo/favicon-32x32.png + favicon_16_16: /logo/favicon-16x16.png + mask_icon: /logo/safari-pinned-tab.svg
|
编写SW文件
接下来就是重中之重了:编写sw.js
,这里直接把我的放出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | const CACHE_NAME = 'kmarCache'
const VERSION_CACHE_NAME = 'kmarCacheTime'
const MAX_ACCESS_CACHE_TIME = 60 * 60 * 24 * 10
function time() { return new Date().getTime() }
const dbHelper = { read: (key) => { return new Promise((resolve) => { caches.match(key).then(function (res) { if (!res) resolve(null) res.text().then(text => resolve(text)) }).catch(() => { resolve(null) }) }) }, write: (key, value) => { return new Promise((resolve, reject) => { caches.open(VERSION_CACHE_NAME).then(function (cache) { cache.put(key, new Response(value)); resolve() }).catch(() => { reject() }) }) }, delete: (key) => { caches.match(key).then(response => { if (response) caches.open(VERSION_CACHE_NAME).then(cache => cache.delete(key)) }) } }
const dbTime = { read: (key) => dbHelper.read(new Request(`https://LOCALCACHE/${encodeURIComponent(key)}`)), write: (key, value) => dbHelper.write(new Request(`https://LOCALCACHE/${encodeURIComponent(key)}`), value), delete: (key) => dbHelper.delete(new Request(`https://LOCALCACHE/${encodeURIComponent(key)}`)) }
const dbAccess = { update: (key) => dbHelper.write(new Request(`https://ACCESS-CACHE/${encodeURIComponent(key)}`), time()), check: async (key) => { const realKey = new Request(`https://ACCESS-CACHE/${encodeURIComponent(key)}`) const value = await dbHelper.read(realKey) if (value) { dbHelper.delete(realKey) return time() - value < MAX_ACCESS_CACHE_TIME } else return false } }
self.addEventListener('install', () => self.skipWaiting())
const cacheList = { sample: { url: /[填写正则表达式]/g, time: Number.MAX_VALUE, clean: true } }
const replaceList = { sample: { source: ['//www.kmar.top'], dist: '//kmar.top' } }
function findCache(url) { for (let key in cacheList) { const value = cacheList[key] if (url.match(value.url)) return value } return null }
function replaceRequest(request) { let url = request.url; let flag = false for (let key in replaceList) { const value = replaceList[key] for (let source of value.source) { if (url.match(source)) { url = url.replace(source, value.dist) flag = true } } } return flag ? new Request(url) : null }
function blockRequest(request) { return false }
async function fetchEvent(request, response, cacheDist) { const NOW_TIME = time() dbAccess.update(request.url) const maxTime = cacheDist.time let remove = false if (response) { const time = await dbTime.read(request.url) if (time) { const difTime = NOW_TIME - time if (difTime < maxTime) return response } remove = true } const fetchFunction = () => fetch(request).then(response => { dbTime.write(request.url, NOW_TIME) if (response.ok || response.status === 0) { const clone = response.clone() caches.open(CACHE_NAME).then(cache => cache.put(request, clone)) } return response }) if (!remove) return fetchFunction() const timeOut = () => new Promise((resolve => setTimeout(() => resolve(response), 400))) return Promise.race([ timeOut(), fetchFunction()] ).catch(err => console.error('不可达的链接:' + request.url + '\n错误信息:' + err)) }
self.addEventListener('fetch', async event => { const replace = replaceRequest(event.request) const request = replace === null ? event.request : replace const cacheDist = findCache(request.url) if (blockRequest(request)) { event.respondWith(new Response(null, {status: 204})) } else if (cacheDist !== null) { event.respondWith(caches.match(request) .then(async (response) => fetchEvent(request, response, cacheList)) ) } else if (replace !== null) { event.respondWith(fetch(request)) } })
|
这里我把开了两个缓存空间,一个是kmarCache
,一个是kmarCacheTime
。前者是用来存储缓存内容的,后者是用来存储时间戳的。
注册SW
请注意,因为安全问题,sw
不允许以任何形式跨域加载,比如你的域名是kmar.top
,那么你只能从kmar.top
中加载sw.js
,其余任何形式都是不允许的,包括使用IP地址代替域名。
核心的注册代码很简单:
1 | navigator.serviceWorker.register('/sw.js')
|
其中,register
的参数就是sw.js
的地址,对于我而言,合法的地址有:
/sw.js
https://kmar.top/sw.js
其余任何形式的加载都是非法的,包括但不限于:
http://kmar.top/sw.js
#非法,因为sw仅允许通过https注册(本地127.0.0.1
允许http)https://wulawula.com/sw.js
#非法,跨域https://11.53.15.145/sw.js
#即使11.53.15.145
是我的博客的IP地址依然非法,被视为跨域
同时需要注意的是,注册sw
后sw
并不会立即生效,只有在刷新页面后才会有效果。解决方案十分简单粗暴,即在安装成功后直接用代码刷新页面,这里我们就用到了js
的then
。
现在我们找个地方把注册代码放进去就可以了,我是放在了[butterfly]/layout/includes/layout.pug
的开头:
1 2 3 4 5 6 7 8 9 10 11 12 | + script. + (() => { + const sw = navigator.serviceWorker + const error = () => document.addEventListener('DOMContentLoaded', + () => btf.snackbarShow('当前浏览器不支持SW,建议更换浏览器以获取最佳体验~')) + if (!sw?.register('/sw.js')?.then(() => { + // 这里我是用的这种粗暴的方式检测SW是否安装成功 + // 不过大佬说这么检测不太靠谱 + // 保守的小伙伴可以用另一个方案 + if (!sw.controller) location.reload() + })?.catch(error)) error() + })()
|
1 2 3 4 5 6 7 8 9 | + script. + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').then(() => { + if (localStorage.getItem('install') !== 'true') { + localStorage.setItem('install', 'true') + location.reload() + } + }) + }
|
检测PWA
浏览器打开网页,按F12
,找到Lighthouse
项目,勾选渐进式 Web 应用
,点击生成报告
就能判断PWA是否生效。
调试SW
浏览器打开网页,按F12
,找到应用程序
项目,在里面就可以调试ServiceWorker
。
补充
现在,ServiceWorker
的缓存完全由代码控制,用户想要刷新缓存只能使用开发者工具删除缓存,这无疑是非常不友好的。
这在另一篇教程中通过添加刷新缓存的按钮得到了解决:《给博客添加刷新缓存按钮》
创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o