最近将我的网站DNS解析整体搬迁到了 Cloudflare 上去。Cloudflare 可以对网站进行优化,但是在开启HTML Auto Minify与Rocket Loader时,发现访问网页时DOMContentLoaded事件缺失。下面来分享一下我的解决经历和方案。
Auto Minify 与 Rocket Loader简介
Auto Minify 可以删除网站源代码(包括CSS,Javascript和HTML)中不必要的字符(如空格和注释等),从而压缩其文件大小,减少需要传输给访问者的数据量,进而缩短页面加载时间。
Rocket Loader通过异步加载 JavaScript (网页内嵌的以及第三方脚本)来缩短绘制时间。更多介绍请参见他们的博客。
我是怎么发现DOMContentLoaded事件缺失的
众所周知,document.readyState在Chrome中定义为三种状态:
- 其值为
loading
时代表网页正在加载。 - 为
interactive
时代表DOM元素可以被访问,但是图像,样式表和框架等资源依然还在加载。 - 为
complete
时代表网页各种资源已经全部加载完成。
在loading
-> interactive
状态间变化时会触发window的DOMContentLoaded
事件,在interactive
-> complete
状态间变化时会触发window的load
事件。
然而,在同时开启HTML Auto Minify与Rocket Loader后,访问网站我发现原本触发window的DOMContentLoaded
事件后定义应该执行的函数都没有执行。首先我排除了是网站代码本身的问题,因为网站在本地运行正常,并且我也通过以下代码确保了在document.readyState为interactive
及complete
时直接执行函数:
if (document.readyState === "interactive" ||
document.readyState === "complete") {
foo();
} else {
window.addEventListener("DOMContentLoaded", foo);
}
因而百思不得其解。
然后我通过将以下代码嵌入网页,输出每次状态变化时document.readyState的值:
console.log(document.readyState);
document.onreadystatechange = function () {
console.log(document.readyState)
}
发现,在开启HTML Auto Minify与Rocket Loader后,document.readyState此时只存在loading
和complete
两个状态了,缺失了interactive
状态,并且在loading
-> complete
变化时只会触发window的load
事件,而不会触发DOMContentLoaded
事件。
这似乎是Rocket Loader的Bug,也可能是故意为之。
根据Cloudflare给出的Rocket Loader的原理,其会将所有 JavaScript 的加载一直推迟到渲染之后再进行。渲染之后document.readyState应该已经是interactive
了,而在执行js代码时,如果同时开启了HTML Auto Minify,通过上述实验发现此时document.readyState被错误设定为了loading
(猜测可能是Rocket Loader中某段代码在开始执行js代码时通过代码错误地赋值为了loading
;因为Rocket Loader是闭源的,所以这样做的原因不详。当然也有可能是其他原因),因而导致应该直接执行函数的情况下判断失效,仍然在注册DOMContentLoaded
事件。
解决方案
既然知道了原因,那么解决方案也很简单,将下述代码添加到所有注册DOMContentLoaded
事件代码之前,无须更改原先任何代码即大功告成。
var inCloudFlare = true;
window.addEventListener("DOMContentLoaded", function () {
inCloudFlare = false;
});
if (document.readyState === "loading") {
window.addEventListener("load", function () {
if (inCloudFlare) window.dispatchEvent(new Event("DOMContentLoaded"));
});
}
此代码判断如果load
事件发生后DOMContentLoaded
事件还未发生,则由代码手动触发DOMContentLoaded
事件,从而将DOMContentLoaded
事件等效于load
事件。这样做唯一的缺点是在document.readyState为complete
时才触发DOMContentLoaded
事件,但是这是目前必须的代价。
当然你也可以直接关闭全局的对 HTML 的 Auto Minify,或者对使用DOMContentLoaded
事件的HTML页面通过配置 Page Rules 页面规则关闭Auto Minify,实测也能解决问题。