document.write 的痛

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)地「优化」掉。

见:chromestatus.com/featur

虽然前面的种种实验还了 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 未来会被抛弃,我们应该积极寻找替代方案。

我就是块砖,如果大家有什么更神奇的方案欢迎分享。

发布于 2018-04-04 17:26