从 ES6 到 Webpack:彻底搞懂 import() 与路由懒加载的三种实现方式
在 Vue 项目中实现路由懒加载时,我们经常会看到三种不同的写法: 这三种方式看起来相似,但在 Webpack 编译过程中的行为完全不同,最终产物也有很大差异。本文将深入分析: 在深入路由懒加载之前,我们需要先理解 JavaScript 中 语法: 核心特点: 语法: 核心特点: 虽然 根据 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 编译后的效果: 重要理解: 在没有额外 Babel 插件的情况下, Webpack 生成的运行时代码(target: 'web' 默认): 老旧浏览器兼容性问题: Webpack 生成的运行时代码默认使用 ES2015 语法(箭头函数、const、模板字符串),在不支持 ES6 的老旧浏览器中会报错: 解决方案: 配置 Webpack target(详见第五章) 根据 Webpack 官方文档: 生成的运行时代码与 import() 相同: require.ensure 和 import() 生成的运行时代码相同,兼容性取决于 Webpack 的 target 配置,而不是语法本身。 源代码: Babel 配置: 根据 babel-plugin-dynamic-import-node 文档: 转换过程: Webpack 生成的代码: 构建产物对比: 原因: 原因: 误区:dynamic-import-node 能解决兼容性问题 真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 Webpack 的 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." 关键理解: 生成的运行时代码: 结果: 生成的运行时代码: 结果: 无论使用 import() 还是 require.ensure,兼容性都取决于 target 配置,而不是语法本身。 适用场景: 需要支持老旧浏览器,需要代码分割 优势: 适用场景: 需要支持老旧浏览器,需要代码分割,希望提升开发环境编译速度 优势: 适用场景: 只需要支持现代浏览器(Chrome 63+, Safari 11.1+) 优势: 真相: 真相: 真相: 这个插件会导致代码拆包失效,不是解决兼容性问题的正确方法。正确的方法是配置 target。 如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题欢迎在评论区讨论。一、引言
// 方式一: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')二、理解 import:ES6 标准 vs Webpack 实现
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; // SyntaxError2.2 ES6 动态 import() 表达式
import('./module.js').then(module => {
// 使用模块
});
// 或使用 async/await
const module = await import('./module.js');"CommonJS 模块加载 ES6 模块,不能使用 require 命令,而要使用 import() 函数。"
// ✅ 正确:可以在函数内部使用
function loadModule() {
return import('./module.js');
}
// ✅ 正确:可以使用变量
const language = 'zh';
import(`./i18n/${language}.js`).then(module => {
// 使用模块
});2.3 Webpack 对 import() 的特殊处理
import() 是 ES6 标准语法,但 Webpack 对它进行了特殊处理,赋予了额外的功能。import() 识别为代码分割点// 普通动态导入
import('./module.js')
// Webpack 魔法注释
import(
/* webpackChunkName: "my-chunk" */
/* webpackPrefetch: true */
'./module.js'
)// 源代码
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 的独立 chunk2.4 关键区别总结
特性 静态 import 动态 import() Webpack 处理的 import() 语法类型 声明语句 表达式 表达式 使用位置 仅文件顶层 任何位置 任何位置 加载时机 编译时 运行时 运行时 加载方式 同步 异步 异步 返回值 直接导入 Promise Promise 路径类型 字符串字面量 任意表达式 任意表达式 代码分割 否 否(原生) ✅ 是(Webpack) 独立 chunk 否 否(原生) ✅ 是(Webpack) import() 只是异步加载模块,不会自动进行代码分割import() 在异步加载的基础上,额外实现了代码分割和 chunk 生成import() 会发起网络请求加载模块文件import() 会被转换为 Webpack 的运行时代码,加载打包后的 chunk三、方式一:ES6 动态 import()
2.1 用法
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}2.2 工作原理
Babel 处理阶段
import() 语法会被保留或由 @vue/babel-preset-app 处理,但不会被转换为其他形式。Webpack 编译阶段
import() 是一个代码分割点// 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 优势
import() 是 ECMAScript 标准语法,未来兼容性好2.4 适用场景
2.5 潜在问题
Uncaught SyntaxError: Unexpected token '=>'三、方式二: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 编译阶段
"require.ensure: Split out the given dependencies to a separate bundle that will be loaded asynchronously."
// 与 import() 生成的运行时代码完全一致
__webpack_require__.e(/* chunkId */ 123).then(__webpack_require__.bind(null, /* moduleId */ 456))3.3 优势
3.4 劣势
import() 替代3.5 适用场景
3.6 重要说明
四、方式三:babel-plugin-dynamic-import-node
4.1 用法
// src/router.js
{
path: '/home',
component: () => import('@/views/Home.vue')
}// babel.config.js
module.exports = {
presets: ['@vue/app'],
plugins: ['dynamic-import-node']
}4.2 工作原理
Babel 处理阶段
"Babel plugin to transpile import() to a deferred require(), for node."
// 转换前
component: () => import('@/views/Home.vue')
// 转换后
component: () => require('@/views/Home.vue')Webpack 编译阶段
require() 而不是 import()require() 是同步导入,Webpack 不会创建新的 chunk// 直接同步加载,没有异步逻辑
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 优势
4.4 劣势
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']4.6 常见误区
五、Webpack target 配置的关键作用
5.1 target 配置的作用
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 配置的重要性
六、三种方式对比总结
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 配置三:现代浏览器配置
// 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 能解决兼容性问题
九、参考资料