document.write 的痛
1. 代码执行是严格遵循顺序的
不废话,先看下面代码和执行结果
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script>
document.write('<!--');
</script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
浏览器对资源加载是有优化的,当页面上存在很多个 js 文件时,它们会同时加载(注意加载并不意味着执行)。上面这个例子中虽然 6 个 js 文件都同时加载,但是由于中间被 script 插入了一行 html 注释,后面的 4、5、6 即便加载了也没有被执行。
那么如果插入的不是注释而是别的 js 文件呢?再看另一段代码
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script>
document.write('<script src="4.js"><\/script>');
</script>
<script src="5.js"></script>
<script src="6.js"></script>
浏览器依然优化了直接写在 html 文件中的 script 引用,它们是同时加载的。但是 4 是在 script 中插入到文档的,而代码执行是严格遵循顺序的,要执行这个 script 就必须等到 1、2、3 加载并执行完毕后才能够开始执行。所以 4 会在 1、2、3 执行完毕后才开始加载。4 插入到文档的位置是在 5 和 6 之前,所以 5 和 6 虽然很早就加载完毕,但也必须等待 4 执行完毕后才能够执行。
直接观察到的现象就是 document.write
阻断了后面代码的执行。但阻断代码执行的根本原因应该是代码执行顺序。这是可以想办法绕过去的。比如把 4 的加载放在所有 script 的最前面,或者更暴力的办法是所有资源都通过 document.write
出来:
<script>
document.write([
'<script src="1.js"><\/script>',
'<script src="2.js"><\/script>',
'<script src="3.js"><\/script>',
'<script src="4.js"><\/script>',
'<script src="5.js"><\/script>',
'<script src="6.js"><\/script>'
].join('\n'));
</script>
这代码虽然看起来很丑,但实际执行速度和完全不使用 document.write
是几乎没差别的。
2. 一波来自 Chrome 的优化
从 Chrome 55 开始,使用 document.write
来加载跨域脚本时浏览器会抛出警告。
在 2G 网络环境下,document.write
的这种使用姿势会被 Chrome 智能(zhàng)地「优化」掉。
见:https://www.chromestatus.com/feature/5718547946799104
虽然前面的种种实验还了 document.write
的清白,但是它还是没有逃过潮流的趋势。
既然如此,我们只能想办法找到对应的替代方案。
3. document.write 存在的场景以及替代方案
从现状看来,会用到 document.write
的唯一场景就是带条件的资源文件同步加载。
比如为了兼容不支持某些特性的浏览器需要加载对应的 Polyfill,但是又考虑到自身本就支持的浏览器,所以要做兼容性检测,只针对不支持的浏览器加载 Polyfill。
<script>
if (!window.Promise) {
document.write('<script src="polyfill/promise.js"><\/script>');
}
if (!window.fetch) {
document.write('<script src="polyfill/fetch.js"><\/script>');
}
</script>
还有一些与具体平台相关的代码也会考虑同步按需引入,比如:
<script>
if(/MicroMessenger/i.test(navigator.userAgent)) {
document.write('<script src="jweixin-1.2.0.js"><\/script>');
}
if(/AlipayClient/i.test(navigator.userAgent)) {
document.write('<script src="alipayjsapi.min.js"><\/script>');
}
</script>
对于以上两种场景都有一些对应的替代方案:
将检测的逻辑搬到服务器端
Polyfill 的加载可以通过 UA 检测到浏览器的版本,根据兼容性列表可以查出客户端使用的浏览器是否支持某些特性,然后再决定要不要加载这些 Polyfill。如果需要加载,服务器端代码直接往 html 里塞入对应的 script 标签。
平台相关的代码加载同样可以在服务器端直接通过 UA 中的关键字来决定要不要在 html 文件放对应的 script 标签。
用多入口解决多平台问题
为每个平台打包专用的入口。比如一个页面如果要同时在微信和支付宝中运行,在前端项目构建时就可以打包 3 个入口文件,一个给微信专用,一个给支付宝专用,最后再打包一个通用的。在这个通过的版本中做一些环境检测并跳转到正确版本的逻辑,这样也可以解决问题。
放弃 CDN
如果不使用 CDN 其实不会有那么多问题,Chrome 对 document.write
限制仅针对跨域,如果不用 CDN 就很容易做到同域请求资源,绕过 Chrome 的限制。当然,我们的目的是找替代方案,而不是找绕行方法,你可以忽略这段。
记得以前见过一个方案,是服务器端提供一个 polyfill.js,然后这个 js 文件是动态的,里面检测了浏览器版本等信息,然后输出对应的 Polyfill 代码。这个想法很不错,可遗憾的是国内的 CDN 并不支持通过 http 的 Vary 头来计算缓存的 key,所以这个方案只能被归为「放弃 CDN」的方案之一。
4. 总结
- 代码被阻断执行是因为要确保顺序,并不是
document.write
的锅。 - 从趋势上看起来
document.write
未来会被抛弃,我们应该积极寻找替代方案。
我就是块砖,如果大家有什么更神奇的方案欢迎分享。