前端开发者都知道,过多的请求对性能影响很大。而且有些 CDN 不仅按流量收费,请求数也收费,如果网页里有大量小文件,显然不划算。
为此不少开发者将零碎的小文件进行合并优化,例如 JS/CSS 合并在一起,图片合并成精灵图等。
不过传统的合并方式有一定的局限性,只能合并同类型的文件。例如 JS、CSS 等文本格式的数据可以合并,但 JS 和图片显然无法合并,毕竟一个是文本格式,一个是二进制格式。而且合并过程需对现有资源进行修改,最终发布的文件与原始文件差异很大。
有没有什么办法,可将任何类型的资源并成在一起,并且不改变原始文件?
Google 推出了一个 Web Bundles
方案,可将任意类型的资源打包成一个文件:
细节可参考:https://web.dev/web-bundles/
不过 Web Bundles
注重的是离线分享。如文中所提到,在没有网络的飞机上,可将网页小游戏通过单个文件的方式分享给旁边的人一起玩。
演示可见,通过本地文件打开的网站仍保留原始 URL。
由于 Web Bundles
目前仍未正式启用,需在 flags 中手动开启,因此该方案仍无法解决本文提出的问题。
事实上,我们大可不必关心资源类型,将所有文件都当做二进制文件合并在一起,运行时再通过 JS 提取。
但是,网页里的资源引用的仍是原始 URL,例如 <img src="a.gif">
。怎样才能让网页使用 JS 提供的数据,而不是从原始 URL 加载?
这就需要借助 HTML5 的一个黑科技 —— Service Worker。它能拦截网页产生的 HTTP 请求,并能控制返回内容。这样即可实现所有资源都从单个文件中提取!
既然要调用 Service Worker,那么是否得修改现有的 HTML 文件,在其中添加脚本?
事实上不需要!用户首次访问时,无论访问哪个路径,后端都返回 Service Worker 安装页;安装完成后页面自动刷新,这时请求即可被 Service Worker 拦截,从而使用资源包中的 HTML 文件。
至于实现其实很简单,使用 404.html
即可!
虽然我们将资源请求数降低到只有 1 个,但流量仍然是存在的。并且任何一个资源更新都得重新下载整个资源包,导致流量成本进一步增长。
有没有什么办法,可大幅降低流量成本?很简单,使用免费 CDN 即可。你可将资源包发布 GitHub、NPM 等空间,然后通过 jsdelivr、unpkg 等免费 CDN 加速。
这样,你的网站只需提供 404.html
和 sw.js 两个极小的文件即可,其他所有内容都可从免费空间获取!
演示站点:https://fanhtml5.github.io/
原始文件:https://github.com/fanhtml5/test-site (多个文件,总共数 MB)
发布文件:https://github.com/fanhtml5/fanhtml5.github.io (只有两个,压缩后不到 2kB)
类似 jsdelivr、unpkg 这么好用的免费 CDN 并不多,用在这里太过浪费,作为开发者也不建议过度使用它们。
我们可使用更低廉更广泛的免费空间 —— 各大网站的贴图相册,例如知乎、B 站、简书等文章的贴图,它们不仅支持 CORS,而且允许空 referrer,完全可用于存储数据。
参照之前写的《利用 canvas 实现数据压缩》文章,我们可将原始数据编码成图片像素,从而可将任意文件写入图片并上传到相册;运行时再解码还原,将原始文件写入 Storage Cache 供 Service Worker 使用。
至于稳定性,可将图片上传到多个站点作冗余。如果加载失败或 Hash 不正确则使用下一个备用图片,从而大幅提升稳定性和安全性。
为了尽可能减少隐私泄露,同时防止外链限制,我们可通过 referrer-policy
对 referrer 进行隐藏。例如:
var img = new Image()
img.crossOrigin = true
img.referrerPolicy = 'no-referrer'
img.src = 'https://pic3.zhimg.com/80/v2-a492fc0204ad0275e0b609ceac2dab10.png'
这样请求中就没有 Referer
头了。
不过需注意的是,为了能读取图片中的像素数据,必须使用 CORS 模式,即设置 crossOrigin
属性。这种模式下请求会出现 Origin
头。虽然大部分网站不会使用该头限制外链,但它会泄露你的站点域名。这仍不完美。
为了能隐藏 Origin
请求头,这里使用一种简单古老但有效的黑科技 —— 使用无源的页面加载图片,例如通过 Data URI 创建的 iframe:
var iframe = document.createElement('iframe')
iframe.src = `data:text/html,
<script>
var img = new Image();
img.crossOrigin = true;
img.src = 'https://pic3.zhimg.com/80/v2-a492fc0204ad0275e0b609ceac2dab10.png';
</script>
`
document.body.appendChild(iframe)
这个方案虽然无法让 Origin
请求头消失,但可将其设置为 null
,从而保护你的站点域名不被泄露。
图片加载完成后,再通过 postMessage
将像素数据发送给主页面。这样即可同时隐藏 Referer
和 Origin
信息,最大程度防止隐私泄露!
基于上述思路,这里实现了一个简单的工具,暂且称之 web2img
。
GitHub: https://github.com/EtherDream/web2img
工具演示: