3、动态 import() 语法:ES2020 提案的 import() 函数,实现按需加载的核心 API
好,咱们进入正题。
前面聊了静态 import 的局限,说白了就是「必须提前声明,不能变」。那有没有办法让代码「想加载时才加载」?有,就是 import() 函数。
这个 API 是 ES2020 正式进入标准的。但说实话,我在 2018 年的项目中就已经通过 Webpack 的 require.ensure 在用了。后来标准统一,大家都用 import(),舒服多了。
3.1 什么是动态 import()?
动态 import() 是一个函数调用。它接收一个模块路径作为参数,返回一个 Promise。
你想想看,Promise 意味着什么?意味着你可以决定「什么时候去执行这个函数」。点击按钮时执行、路由切换时执行、甚至滚动到某个位置时执行——全由你控制。
核心区别一句话:
- 静态 import:编译时确定,必须写在文件顶部
- 动态 import():运行时确定,可以在任意位置调用
3.2 基本用法
语法非常简单:
// 静态导入(传统方式)
import { formatDate } from './utils/date'
// 动态导入(按需加载)
const module = await import('./utils/date')
module.formatDate(new Date())
嗯,这里要注意:动态 import() 返回的是一个模块的命名空间对象。也就是说,你拿到的 module 包含了这个模块所有的导出。
如果你只想用某个默认导出:
const { default: myFunction } = await import('./myModule')
我个人习惯用解构赋值,代码更干净。
3.3 实战场景:点击按钮加载大模块
我在项目中遇到过这样一个场景:一个图表页面,里面引用了 ECharts。ECharts 打包后将近 1MB。如果所有用户进来都加载,首屏时间直接爆炸。
解决方案?只有用户点击「查看图表」按钮时,才去加载 ECharts。
// 按钮点击事件
document.getElementById('showChart').addEventListener('click', async () => {
try {
const echarts = await import('echarts')
const myChart = echarts.init(document.getElementById('chartContainer'))
myChart.setOption({ /* 图表配置 */ })
} catch (error) {
console.error('图表模块加载失败:', error)
// 可以展示一个友好的错误提示
}
})
你看,代码逻辑非常直观。用户不点按钮,ECharts 永远不会被加载。这就是按需加载的核心思想。
小技巧:动态 import() 配合 try/catch 使用,可以优雅处理网络异常。我曾经遇到过 CDN 资源临时挂掉的情况,catch 里做了降级处理,用户完全没感知。
3.4 动态 import() 与静态 import 的对比
我整理了一张对比表,方便你快速理解差异:
| 对比维度 | 静态 import | 动态 import() |
|---|---|---|
| 加载时机 | 编译时(模块解析阶段) | 运行时(代码执行到该行时) |
| 代码位置 | 必须位于文件顶部 | 任意位置(函数内、条件语句内) |
| 返回值 | 同步,直接使用导出 | Promise,异步获取模块 |
| Tree Shaking | 支持(构建工具可分析) | 部分支持(取决于工具实现) |
| 适用场景 | 核心模块、首屏必需模块 | 非首屏模块、条件性加载模块 |
3.5 动态路径与变量拼接
静态 import 的路径必须是字符串字面量,不能拼接。但动态 import() 可以。
为什么会这样?因为静态 import 需要在构建时就确定模块依赖关系,而动态 import 是在运行时才去解析路径。
// 静态 import 不允许这样
// import { Component } from `./pages/${pageName}` // 语法错误
// 动态 import() 可以
const page = await import(`./pages/${pageName}.js`)
不过这里有个坑:如果你用 Webpack 或 Vite 打包,动态路径可能会导致打包出很多 chunk。我曾经踩过这个坑——用变量拼接路径,结果把所有页面都打包进去了,按需加载变成了「全量加载」。
避坑指南:使用动态路径时,尽量让路径的「可变部分」保持在最小范围。比如 import(`./pages/${pageName}`) 比 import(`./${folder}/${pageName}`) 更安全。因为构建工具会尝试解析所有可能的路径,路径越模糊,打包出来的 chunk 越多。
3.6 动态 import() 的常见模式
在实际项目中,我总结了几种常用模式:
- 路由懒加载:每个路由对应一个 chunk,用户访问时才加载
- 条件加载:根据用户权限、设备类型等条件决定加载哪个模块
- 预加载:在空闲时提前加载,但不用立即执行
- 错误重试:加载失败后自动重试
举个预加载的例子:
// 用户鼠标悬停在按钮上时,提前加载模块
button.addEventListener('mouseenter', () => {
// 预加载,但不阻塞
const modulePromise = import('./heavyModule')
// 可以存下来,等真正需要时直接用
window.__preloadedModule = modulePromise
})
button.addEventListener('click', async () => {
const module = await (window.__preloadedModule || import('./heavyModule'))
module.doSomething()
})
这个技巧我在一个数据大屏项目里用过。用户鼠标移到「查看详情」按钮上时,后台已经开始加载数据模块了。等用户真正点击时,几乎零等待。体验非常好。
3.7 浏览器兼容性
动态 import() 已经是现代浏览器的标配了。Chrome 63+、Firefox 67+、Safari 11.1+ 都支持。如果你需要兼容 IE 之类的老古董,那就得用 Babel 加插件转译,或者直接用 Webpack 的 require.ensure。
不过说真的,2024 年了,还在纠结 IE 的项目应该不多了吧?
总结一下:动态 import() 是前端代码分割的基石。它把「加载模块」这件事从编译时解放到了运行时,让我们可以精细控制每一段代码的加载时机。配合构建工具的分割策略,就能实现真正的按需加载。
下一章,我们会聊 Webpack 和 Vite 是如何处理动态 import() 的,以及如何配置 chunk 名称、分割策略。到时候我会分享一些我在大厂项目里的实战配置,保证干货满满。