一、引言
在 Vue 项目中实现路由懒加载时,我们经常会看到三种不同的写法:
// 方式一:ES6 动态 import()
component: () => import('@/views/Home.vue')
// 方式二:Webpack require.ensure
component: resolve => require(['@/views/Home.vue'], resolve)
// 方式三:import() + babel-plugin-dynamic-import-node
// babel.config.js 配置了 'dynamic-import-node' 插件
component: () => import('@/views/Home.vue')
这三种方式看起来相似,但在 Webpack 编译过程中的行为完全不同,最终产物也有很大差异。本文将深入分析:
- ES6 import 语句和 import() 表达式的本质区别
- Webpack 如何特殊处理 import() 实现代码分割
- 每种方式的工作原理和转换过程
- 它们各自的优势和适用场景
- 如何根据项目需求选择最佳方案
- Webpack target 配置的关键作用
二、理解 import:ES6 标准 vs Webpack 实现
在深入路由懒加载之前,我们需要先理解 JavaScript 中 import 的两种完全不同的形式。
2.1 ES6 静态 import 语句
语法:
import React from 'react';
import { useState } from 'react';
核心特点:
"ES6 模块是编译时输出接口,在代码静态解析阶段就会生成。"
- 静态语法:必须在文件顶层使用,不能在条件语句、函数内部使用
- 编译时处理:在代码静态解析阶段就确定了模块依赖关系
- 路径固定:路径必须是字符串字面量,不能是变量
// ❌ 错误:不能在函数内部使用
function loadModule() {
import module from './module'; // SyntaxError
}
// ❌ 错误:不能使用变量
const path = './module';
import module from path; // SyntaxError
2.2 ES6 动态 import() 表达式
语法:
import('./module.js').then(module => {
// 使用模块
});
// 或使用 async/await
const module = await import('./module.js');
核心特点:
"CommonJS 模块加载 ES6 模块,不能使用 require 命令,而要使用 import() 函数。"
- 动态语法:可以在任何地方使用(条件语句、函数内部等)
- 运行时处理:在代码执行时才加载模块
- 异步加载:返回一个 Promise
- 路径可变:路径可以是变量或表达式
// ✅ 正确:可以在函数内部使用
function loadModule() {
return import('./module.js');
}
// ✅ 正确:可以使用变量
const language = 'zh';
import(`./i18n/${language}.js`).then(module => {
// 使用模块
});
2.3 Webpack 对 import() 的特殊处理
虽然 import() 是 ES6 标准语法,但 Webpack 对它进行了特殊处理,赋予了额外的功能。
根据 Webpack 官方文档 - Code Splitting:
"Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports."
"Calls to import() are treated as split points, meaning the requested module and its children are split out into a separate chunk."
Webpack 的特殊处理:
- 代码分割点:Webpack 将
import()识别为代码分割点 - 创建独立 chunk:被导入的模块会被打包成独立的 chunk 文件
- 生成加载代码:Webpack 生成异步加载该 chunk 的运行时代码
- 支持魔法注释:可以通过注释控制 chunk 名称、预加载等行为
示例:
// 普通动态导入
import('./module.js')
// Webpack 魔法注释
import(
/* webpackChunkName: "my-chunk" */
/* webpackPrefetch: true */
'./module.js'
)
Webpack 编译后的效果:
// 源代码
const module = await import('./Home.vue');
// Webpack 编译后(简化版)
const module = await __webpack_require__.e(/* chunkId */ 123)
.then(__webpack_require__.bind(null, /* moduleId */ 456));
// 生成的文件
// dist/main.js - 主 bundle
// dist/123.js - Home.vue 的独立 chunk
2.4 关键区别总结
| 特性 | 静态 import | 动态 import() | Webpack 处理的 import() |
|---|---|---|---|
| 语法类型 | 声明语句 | 表达式 | 表达式 |
| 使用位置 | 仅文件顶层 | 任何位置 | 任何位置 |
| 加载时机 | 编译时 | 运行时 | 运行时 |
| 加载方式 | 同步 | 异步 | 异步 |
| 返回值 | 直接导入 | Promise | Promise |
| 路径类型 | 字符串字面量 | 任意表达式 | 任意表达式 |
| 代码分割 | 否 | 否(原生) | ✅ 是(Webpack) |
| 独立 chunk | 否 | 否(原生) | ✅ 是(Webpack) |
重要理解:
- ES6 原生的
import()只是异步加载模块,不会自动进行代码分割 - Webpack 处理的
import()在异步加载的基础上,额外实现了代码分割和 chunk 生成 - 在浏览器中,原生
import()会发起网络请求加载模块文件 - 在 Webpack 打包后,
import()会被转换为 Webpack 的运行时代码,加载打包后的 chunk
三、方式一:ES6 动态 import()
2.1 用法
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}
2.2 工作原理
Babel 处理阶段
在没有额外 Babel 插件的情况下,import() 语法会被保留或由 @vue/babel-preset-app 处理,但不会被转换为其他形式。
Webpack 编译阶段
- 识别 import() 语法:Webpack 识别到
import()是一个代码分割点 - 创建独立 chunk:为该模块创建一个单独的 chunk 文件
- 生成运行时代码:生成异步加载该 chunk 的运行时代码
Webpack 生成的运行时代码(target: 'web' 默认):
// Webpack 运行时代码 - ES2015 风格
__webpack_require__.e = (chunkId) => { // 箭头函数
const promises = []; // const
const installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
const promise = new Promise((resolve, reject) => { // 箭头函数
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
const script = document.createElement('script');
script.src = `${__webpack_require__.p}${chunkId}.js`; // 模板字符串
document.head.appendChild(script);
}
return Promise.all(promises);
};
// 调用方式
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))
2.3 优势
- 符合 ES 标准:
import()是 ECMAScript 标准语法,未来兼容性好 - 代码分割:自动实现代码分割,生成独立的 chunk 文件
- 按需加载:只在需要时才加载对应的代码,减少首屏加载时间
- 代码现代化:代码更简洁、易读
2.4 适用场景
- 现代浏览器环境(Chrome 63+, Safari 11.1+, Firefox 67+)
- 需要代码分割和按需加载的项目
- 追求代码现代化的新项目
2.5 潜在问题
老旧浏览器兼容性问题:
Webpack 生成的运行时代码默认使用 ES2015 语法(箭头函数、const、模板字符串),在不支持 ES6 的老旧浏览器中会报错:
Uncaught SyntaxError: Unexpected token '=>'
解决方案: 配置 Webpack target(详见第五章)
三、方式二:Webpack require.ensure
3.1 用法
// src/router.js
{
path: '/home',
component: resolve => require(['@/views/Home.vue'], resolve)
}
3.2 工作原理
Babel 处理阶段
require.ensure 是 Webpack 特有的语法,Babel 不会对其进行转换,直接传递给 Webpack。
Webpack 编译阶段
根据 Webpack 官方文档:
"require.ensure: Split out the given dependencies to a separate bundle that will be loaded asynchronously."
- 识别 require.ensure 语法:Webpack 识别到这是一个代码分割点
- 创建独立 chunk:为该模块创建一个单独的 chunk 文件
- 生成运行时代码:生成异步加载该 chunk 的运行时代码
生成的运行时代码与 import() 相同:
// 与 import() 生成的运行时代码完全一致
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))
3.3 优势
- 代码分割:实现代码分割,生成独立的 chunk 文件
- 按需加载:只在需要时才加载对应的代码
- 历史兼容:在 Webpack 1/2 时代广泛使用
3.4 劣势
- 非标准语法:这是 Webpack 特有的语法,不是 JavaScript 标准
- 已被废弃:Webpack 官方推荐使用
import()替代 - 代码不够现代化:语法相对复杂,可读性较差
3.5 适用场景
- 维护老旧项目(Webpack 1/2 时代的项目)
- 需要兼容老旧浏览器,但不想配置 target(不推荐)
3.6 重要说明
require.ensure 和 import() 生成的运行时代码相同,兼容性取决于 Webpack 的 target 配置,而不是语法本身。
四、方式三:babel-plugin-dynamic-import-node
4.1 用法
源代码:
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}
Babel 配置:
// babel.config.js
module.exports = {
presets: ['@vue/app'],
plugins: ['dynamic-import-node']
}
4.2 工作原理
Babel 处理阶段
根据 babel-plugin-dynamic-import-node 文档:
"Babel plugin to transpile import() to a deferred require(), for node."
转换过程:
// 转换前
component: () => import('@/views/Home.vue')
// 转换后
component: () => require('@/views/Home.vue')
Webpack 编译阶段
- 识别 require() 语法:Webpack 看到的是
require()而不是import() - 同步打包:
require()是同步导入,Webpack 不会创建新的 chunk - 打包到当前文件:模块被直接打包到当前 chunk 中
Webpack 生成的代码:
// 直接同步加载,没有异步逻辑
component: () => __webpack_require__(/* moduleId */ 456)
构建产物对比:
# 不使用 dynamic-import-node(正常拆包)
dist/js/chunk-vendors.js
dist/js/app.js
dist/js/home.js # 单独的路由 chunk ✅
dist/js/profile.js # 单独的路由 chunk ✅
# 使用 dynamic-import-node(拆包失效)
dist/js/chunk-vendors.js
dist/js/app.js # 所有路由都在这里 ❌
4.3 优势
- 提升编译速度:同步导入比异步导入编译更快
- 避免运行时代码:没有异步加载的运行时代码,避免了 ES6 语法问题
- 适合 Node.js 环境:这个插件最初是为 Node.js 环境设计的
4.4 劣势
- 代码拆包失效:所有路由组件被打包到一个文件中
- 首屏加载慢:失去了按需加载的性能优势
- bundle 体积大:所有代码都在一个文件中,体积过大
4.5 适用场景
推荐场景:仅在开发环境使用
// babel.config.js
module.exports = {
presets: ['@vue/app'],
plugins: [
// 只在开发环境使用,提升构建速度
process.env.NODE_ENV === 'development' && 'dynamic-import-node'
].filter(Boolean)
};
原因:
- 开发环境不需要代码分割,使用同步导入可以加快编译速度
- 生产环境需要代码分割以优化性能
不推荐场景:生产环境使用
// ❌ 不要在生产环境使用
plugins: ['dynamic-import-node']
原因:
- 代码拆包失效,失去按需加载的性能优势
- 首屏加载时间过长
- bundle 体积过大
4.6 常见误区
误区:dynamic-import-node 能解决兼容性问题
真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 Webpack 的 target。
五、Webpack target 配置的关键作用
5.1 target 配置的作用
"Note that webpack runtime code is not the same as the user code you write, you should transpile that code with transpilers like Babel if you want to target specific environments."
"When no information about the target or the environment features is provided, then ES2015 will be used."
关键理解:
- Webpack 的运行时代码不会被 Babel 转译
- target 配置决定运行时代码的 ES 版本
- 默认 target: 'web' 生成 ES2015 代码
5.2 target: 'web' (默认)
// webpack.config.js 或 vue.config.js
module.exports = {
configureWebpack: {
target: 'web' // 默认值
}
};
生成的运行时代码:
__webpack_require__.e = (chunkId) => { // ES6 箭头函数
const promises = []; // ES6 const
const script = document.createElement('script');
script.src = `${__webpack_require__.p}${chunkId}.js`; // ES6 模板字符串
// ...
};
结果:
- ✅ 现代浏览器正常运行
- ❌ 老旧浏览器报语法错误
5.3 target: ['web', 'es5']
// webpack.config.js 或 vue.config.js
module.exports = {
configureWebpack: {
target: ['web', 'es5'] // 生成 ES5 代码
}
};
生成的运行时代码:
__webpack_require__.e = function requireEnsure(chunkId) { // ES5 function
var promises = []; // ES5 var
var script = document.createElement('script');
script.src = __webpack_require__.p + chunkId + '.js'; // 字符串拼接
// ...
};
结果:
- ✅ 现代浏览器正常运行
- ✅ 老旧浏览器正常运行
- ✅ 代码分割功能正常工作
5.4 target 配置的重要性
无论使用 import() 还是 require.ensure,兼容性都取决于 target 配置,而不是语法本身。
六、三种方式对比总结
6.1 完整对比表
| 特性 | import() | require.ensure | import() + dynamic-import-node |
|---|---|---|---|
| 标准化 | ✅ ES 标准 | ❌ Webpack 特有 | ✅ ES 标准(源码) |
| 代码分割 | ✅ 是 | ✅ 是 | ❌ 否 |
| 按需加载 | ✅ 是 | ✅ 是 | ❌ 否 |
| 编译速度 | 中等 | 中等 | ✅ 快 |
| 代码现代化 | ✅ 高 | ❌ 低 | ✅ 高(源码) |
| 老旧浏览器兼容 | 取决于 target | 取决于 target | ✅ 是(但失去拆包) |
| 推荐使用 | ✅ 是 | ❌ 否 | ⚠️ 仅开发环境 |
6.2 转换流程对比
场景一:import() + target: 'web' (默认)
源代码: () => import('@/views/Home.vue')
↓
Babel: 不转换
↓
Webpack: 创建 chunk,生成 ES2015 运行时代码
↓
结果:
- 代码拆包 ✅
- 现代浏览器 ✅
- 老旧浏览器 ❌
场景二:import() + target: ['web', 'es5'] (推荐)
源代码: () => import('@/views/Home.vue')
↓
Babel: 不转换
↓
Webpack: 创建 chunk,生成 ES5 运行时代码
↓
结果:
- 代码拆包 ✅
- 现代浏览器 ✅
- 老旧浏览器 ✅
场景三:import() + dynamic-import-node
源代码: () => import('@/views/Home.vue')
↓
Babel: 转换为 () => require('@/views/Home.vue')
↓
Webpack: 同步打包,不创建 chunk
↓
结果:
- 代码拆包 ❌
- 编译速度 ✅
- 适合开发环境
场景四:require.ensure + target: ['web', 'es5']
源代码: resolve => require(['@/views/Home.vue'], resolve)
↓
Babel: 不转换
↓
Webpack: 创建 chunk,生成 ES5 运行时代码
↓
结果:
- 代码拆包 ✅
- 现代浏览器 ✅
- 老旧浏览器 ✅
- 但使用了历史遗留 API ⚠️
七、最佳实践配置
7.1 配置一:标准配置(推荐)
适用场景: 需要支持老旧浏览器,需要代码分割
// vue.config.js
module.exports = {
configureWebpack: {
target: ['web', 'es5'] // 关键配置
}
};
// babel.config.js
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};
// router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}
优势:
- ✅ 配置简单
- ✅ 兼容老旧浏览器
- ✅ 代码分割正常工作
- ✅ 开发和生产环境行为一致
7.2 配置二:性能优化配置(推荐)
适用场景: 需要支持老旧浏览器,需要代码分割,希望提升开发环境编译速度
// vue.config.js
module.exports = {
configureWebpack: {
target: ['web', 'es5'] // 关键配置
}
};
// babel.config.js
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
// 只在开发环境使用
process.env.NODE_ENV === 'development' && 'dynamic-import-node'
].filter(Boolean)
};
// router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}
优势:
- ✅ 开发环境编译快
- ✅ 生产环境代码分割正常
- ✅ 兼容老旧浏览器
- ✅ 兼顾性能和开发体验
7.3 配置三:现代浏览器配置
适用场景: 只需要支持现代浏览器(Chrome 63+, Safari 11.1+)
// vue.config.js
module.exports = {
configureWebpack: {
target: 'web' // 默认值
}
};
// babel.config.js
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};
// router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}
优势:
- ✅ 配置最简单
- ✅ 生成的代码更小
- ✅ 运行时性能更好
八、常见误区澄清
误区 1:import() 不兼容老旧浏览器
真相: import() 语法本身不是问题,Webpack 生成的运行时代码才是问题。配置 target: ['web', 'es5'] 后完全兼容。
误区 2:require.ensure 更兼容
真相: require.ensure 和 import() 生成的运行时代码相同,兼容性取决于 target 配置,而不是语法本身。
误区 3:dynamic-import-node 能解决兼容性问题
真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 target。
九、参考资料
- 阮一峰《ES6 入门教程》- Module 的加载实现
- Webpack 官方文档 - target 配置
- Webpack 官方文档 - Module Methods
- Webpack 官方文档 - Code Splitting
- babel-plugin-dynamic-import-node - GitHub
- Vue Router 官方文档 - 路由懒加载
- MDN - 动态 import()
如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题欢迎在评论区讨论。