背景
前天晚上在清理 Chrome 插件的时候,发现了好久之前因好奇而安装的 Lighthouse 插件,它是 Chrome 团队出的一个自动化页面审计工具,可以帮助开发者优化页面。继续怀着好奇的心理,我打开了我的网站主页,然后点了一下 Generate report 按钮,得分惨不忍睹……
嗯由于特别惨不忍睹,所以我就不放结果了,放一张 Lighthouse 的界面代替好了……当然,优化的过程倒是不算太难,按照网上搜到的教程添加 Service Worker 和 Manifest,然后给一些标签加上推荐的属性就可以了。
由于其它的内容都没什么难度,因此我便把研究的目光转向了 Service Worker。
什么是 Service Worker
网上到处都是,不赘述了……不过有几点值得注意:
- 它与 Web Worker 是完全不同的两个东西,Service Worker 完全只是一个浏览器与服务器之间的代理;
- Service Worker 必须运行在可信的域名下,而且还有严格的 Scope 的限制,毕竟它可以拦截并修改所有的数据,如果被黑客植入网站,甚至可以用来做与“缓存投毒”相似的攻击。
如何调试 Service Worker
刚才提到 Service Worker 必须运行在可信的域名下(否则会报 Error),除了 HTTPS 以外就只有 localhost
了,然而我的网站是通过域名来加载不同子站的,因此 localhost
无法使用。上网查了一会儿,发现可以通过添加如下的两个参数启动 Chrome:
./chrome --user-data-dir=/path/to/your/tmpdir --unsafely-treat-insecure-origin-as-secure=http://your-dev-host
其中 unsafely-treat-insecure-origin-as-secure
大家都明白,就不讲了;要注意这里的 user-data-dir
是必选的,可能是 Chrome 认为随便指定一个可信域名太危险了,必须要把这个进程的目录与正常的目录隔离开。此时再进入 http://your-dev-host
,就能正常注册 Service Worker 啦!
打开的 Chrome 窗口中会有一行提示,说“你这样操作会降低安全性”,所以平时千万不要这么做。我在调试完成、关掉这个 Chrome 进程后,也把上面指定的临时文件夹删掉了。
Service Worker 毕竟也是一段 JS,因此调试方法与普通 JS 一致,可以下断点可以加 debugger
,只是它 console.log
的位置不太一样,在 Console 面板的最上方可以点击显示着 top
的那个下拉框,找到对应的选项即可。
通过 Service Worker 实现缓存
其实这个网上也到处都是,然而由于有着严格的安全限制,因此我要想把它种到每个域名的根路径中,需要对 Nginx 做一些配置(虽然也不算难就是了):
server {
listen 80;
server_name ~^(?<subdomain>.+)\.rexskz\.info$;
location /sw.js {
root /path/to/wwwroot/sw;
}
# several lines hidden...
location / {
root /path/to/wwwroot/$subdomain;
}
}
至于 sw.js
的内容,我一开始是直接抄的 这里,很好用,只要把里面的 offlineResources
和 ignoreFetch
两个变量结合自己网站的情况改一改就行了。这段代码的思路大概是:当 Service Worker 收到一个 Fetch 请求的时候,会去做如下检查:
- 对于符合
ignoreFetch
中任意一个正则的网址,或者非 GET 请求,始终拉取新数据,拉取失败时显示offlineResources
中对应的内容; - 对于
Accept
头中包含text/html
的请求,优先拉取新数据,拉取失败时使用缓存数据,若没有缓存则显示offlineResources
中对应的内容; - 对于其它资源,优先使用缓存数据,若没有缓存则拉取新数据,拉取失败时显示
offlineResources
中对应的内容。
怎么样,是不是很简单呢?
使用 Service Worker 修改图片请求
WebP 是一个大家都知道、但某些浏览器打死也不支持的图片格式。虽然网上有很多解决方案,但是对于个人网站来说,全站适配 WebP 的成本还是挺高的,例如我的这个技术博客用的是 Typecho,但 狗粮站 用的是 Ghost,还有其它的一些奇怪的网页。因此在折腾 Service Worker 的时候,我在想,既然这玩意儿能充当代理的角色,那肯定也可以把图片格式转换一下咯?
七牛图床的解决方案
反正我用的全都是七牛的图床,图片后缀也只有 JPG 和 PNG,转 WebP 只需要在 URL 后面加点东西就好了:
function onFetch(event) {
const request = event.request;
if (/\.jpg$|\.png$/.test(request.url)) {
// 只在支持 WebP 的浏览器下修改 URL
// 至于某狐、某边和某动物园目前是没法享受这种格式了
if (request.headers.has('accept') && /webp/.test(request.headers.get('accept'))) {
switch (request.url.match(/\.([a-z]+)$/i)[1]) {
case 'jpg':
newUrl = request.url + '?imageMogr2/format/webp/quality/95';
break;
case 'png':
newUrl = request.url + '?imageMogr2/format/webp/quality/100';
break;
}
}
}
// several lines hidden...
// 问题:如何用 newUrl 替换掉 event.request 中的 URL
}
问题出现!我的第一反应是看看代码中有没有现成的方案,找到了这一段:
// 这里居然用了 fetch!
return fetch(request).then(response => {
log('(network)', request.method, request.url);
return response;
}).catch(() => {
return offlineResponse(request);
});
原来 Request
跟最新的 Fetch API 是相辅相成的!我激动得写下了如下代码:
let request = event.request;
// newUrl = xxx
event.respondWith(fetch(newUrl));
然后……就出现了跨域问题。
跨域问题的解决
我又去翻了一下 Request
的文档,发现可以用它的构造函数造出个新的 request
然后直接替掉:
let request = event.request;
// newUrl = xxx
request = new Request(newUrl, request);
在想为什么可以跨域,于是发现 Request
中有一个叫 mode
的属性,还是可以在构造函数中自定义的,于是心里咯噔了一下:Request
难道可以用来跨域?于是我在 F12 的 Console 里试了试:
// 当前页面的域名是另一个
req = new Request('https://www.rexskz.info/manifest.json', { mode: 'no-cors' })
await fetch(req)
然后就被 Chrome 的 CORB(嗯对,不是大家熟悉的 CORS)给拦住了。看来还是没问题的嘛。
P.S. CORB 是 Chrome 用来降低侧信道攻击的危害而搞出来的东西。
一个无法优雅解决的问题
Service Worker 的注册是异步的,而且经过了那么多次的试验,它似乎总是在最后才加载完成,那岂不是意味着第一次访问我的网站时还是加载的原始格式的图片?于是我开始想办法如何让它阻塞的加载。
翻 文档 发现,可以在注册后的 then
中监听它的状态,如果是 activated
则表示可以使用了(可以在这儿提示用户“页面已缓存”)。于是我脑洞大开,想一开始把 <body>
中的东西都设成 display: none
,加载完之后再搞回来。然而我还是太年轻,浏览器看到这样隐藏的元素,有时候也会加载其中的图片资源,具体可以看参考一下 《Web 图片资源的加载与渲染时机》,因此我只能通过去掉 DOM 的方式来阻止图片的加载。
对于 Vue 等框架编写的网站来说很简单,只需要用一个条件(例如 v-if
)来控制 DOM 是否出现即可,但对于我的网站来说,暂时没有特别优雅的实现方法。虽然可以一开始将网页渲染成这样:
<body>
<!-- 我瞎编的,只要不是 text/javascript 就行 -->
<script type="text/sw-placeholder">
原本要渲染的 HTML 内容
</script>
</body>
然后当 activated
的时候将里面的内容提出来扔到 <body>
中,但是这样实在是太不优雅,所以还是算了。
当然,即使没有解决这个问题,网站也得到了很好的优化,毕竟 WebP 体积特别小,在 Service Worker 的缓存空间有限的情况下,可以多存储一些文件。
最后附上一张图,是我现在网站的 Lighthouse 报告:
我已经很满意了,剩下的优化有时间再去折腾吧。
版权声明:除文章开头有特殊声明的情况外,所有文章均可在遵从 CC BY 4.0 协议的情况下转载。