近年来HTML的一项重要改进是,我们可以为图像(也包括iframe)添加loading="lazy"属性。它会指示浏览器,仅在图像即将进入视口时才加载它。
<img src="/images/your-image.png" loading="lazy">
这一特性简单而实用。试想,如果能够对脚本也实现类似的懒加载效果,那该多好?这样,我们就能仅在组件真正需要时,才去加载它们对应的脚本。
实际上,<img>元素还有另一个功能:它支持使用onload和onerror属性,在图像加载成功或失败时执行一段脚本。
<img
src="/images/your-image.png"
loading="lazy"
onload="() => console.log('image loaded')">
这个onload回调只有在图像加载完成后才会触发。如果图像是懒加载的,那么回调也只会在图像进入视口时才被触发。瞧,这不就是一个懒加载的脚本触发器吗?
不过,上述用法存在明显不足。首先,页面上会出现一个并非实际需要的图片;其次,你不得不将JavaScript代码内联编写,这在一定程度上违背了懒加载脚本的初衷。因此,我们需要对其进行改进。
利用onerror事件
图片本身可以是任何内容,甚至可以是“空”的。正如前文提到的,还有onerror回调,顾名思义,它会在图片加载失败时触发。
这并不意味着你需要将src指向一个不存在的图片路径,那样会导致控制台充满404错误。如果src指向的不是一个有效的图片,onerror回调同样会触发。一个简单的方法是使用data: URL格式“错误地编码”一个图像,这样做的好处是不会在控制台产生缺失图片的警告。
<img
src="data:,"
loading="lazy"
onerror="() => console.log('image not loaded')">
当然,这仍会在页面上显示一个“破损的图像”图标,我们稍后会解决这个问题。
引入动态导入
现在的问题在于,我们仍然需要内联JavaScript代码。如何解决呢?借助目前几乎得到普遍支持的ES模块特性,我们可以使用一种非常强大的技术——JavaScript的动态导入(dynamic import),在事件触发后异步加载脚本。
<img
src="data:,"
loading="lazy"
onerror="import('/js/some-component.js').then(_ => _.default(this))">
注:此方法同样适用于 onclick、onchange 等事件。
注:这里的下划线 _ 仅作为访问模块对象的简写,你也可以写成 .then(Module => Module.default(this))
动态组件示例
让我们看看 some-component.js 文件可能的样子:
// some-component.js
export default element => {
element.outerHTML = `
<div class="whatever">
<p>Hello world!</p>
</div>
`;
}
你可能已经注意到,在onerror回调中,我将this作为参数传递给了模块的默认导出函数。因为在当前事件回调的上下文中,this指向触发事件的<img>元素本身。
这样,我们就能在函数内部通过 element.outerHTML,轻松地将这个“破损的图片”替换为任意自定义的HTML标记。至此,一个真正的懒加载脚本方案就实现了!
进阶技巧:缓存与参数传递
1. 防止缓存冲突
如果你在同一个页面上多次使用此技术,需要为每个data: URL传递一个不同的“缓存破坏”索引或随机数,以确保它们被视为不同的资源独立触发。
<img
src="data:,abc123"
loading="lazy"
onerror="import('/js/some-component.js').then(_ => _.default(this))">
<img
src="data:,xyz789"
loading="lazy"
onerror="import('/js/some-other-component.js').then(_ => _.default(this))">
“data:,”后面的字符串可以是任意内容,只要彼此不同即可。
2. 传递参数
向懒加载的组件传递参数非常简单,只需在HTML元素上使用data-*属性。
<img
src="data:,"
loading="lazy"
data-message="hello world"
onerror="import('/js/some-component.js').then(_ => _.default(this))">
在组件脚本中,我们可以通过传入的element访问这些数据属性:
export default element => {
const { message } = element.dataset;
element.outerHTML = `
<div class="whatever">
<p>${message}</p>
</div>
`;
}
这种将图片懒加载机制与JavaScript动态导入相结合的方法,为现代前端开发中的资源按需加载提供了一个非常巧妙且实用的思路。