Appearance
Webpack
从「模块打包工具」这个本质出发,Webpack 的一切概念——Entry、Output、Loader、Plugin——都围绕一个核心目标:把浏览器无法直接理解的各类资源,转换、组织、打包成可运行的静态文件。本篇按「核心概念 → 模块化原理 → 编译与产物 → 工程化进阶 → 对比与选型」五个维度,串联 Webpack 从入门到原理的完整知识体系。
导航目录
一、核心概念篇
二、模块化原理篇
三、编译与产物篇
四、工程化进阶篇
五、对比与选型篇
Webpack 介绍
- Webpack 是一个 JavaScript 应用程序的静态模块打包工具,它可以将多个模块打包成一个或多个 bundle 文件
核心概念
Webpack 把项目里的一切资源(JS、CSS、图片、字体等)都视为模块。它从入口出发,递归分析模块间的依赖关系,构建出一张依赖图(dependency graph),再按规则将这些模块转换、合并成浏览器可运行的静态资源。
Webpack 的运行离不开五个核心概念,它们贯穿整个构建过程:
| 概念 | 作用 |
|---|---|
| Entry | 依赖图的起点,告诉 Webpack 从哪个模块开始构建 |
| Output | 打包结果输出到哪里、如何命名 |
| Loader | 让 Webpack 能处理非 JS 资源(转换为有效模块) |
| Plugin | 参与构建生命周期,执行更广泛的任务(优化、注入) |
| Mode | development / production,启用对应内置优化 |
入口 (Entry)
- 入口起点(entry point)指示 Webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始
- 进入入口起点后,Webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的
单入口 vs 多入口
- 单入口:
entry: './src/index.js',适合单页应用(SPA) - 多入口:
entry: { app: './app.js', admin: './admin.js' },适合多页应用(MPA),每个入口生成独立的 bundle
输出 (Output)
- output 属性告诉 Webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
javascript
module.exports = {
output: {
path: path.resolve(__dirname, "dist"), // 输出目录(绝对路径)
filename: "[name].[contenthash].js", // 使用 contenthash 做长效缓存
clean: true, // 构建前清空输出目录
},
};占位符与缓存
[contenthash] 会根据文件内容生成哈希,内容不变则哈希不变,配合 HTTP 强缓存可最大化利用浏览器缓存;相比之下 [hash](整个构建的哈希)粒度太粗,[chunkhash](chunk 级别)则介于两者之间。
Loader
- Webpack 只能理解 JavaScript 和 JSON 文件
- Loader 让 Webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中
- 多个 loader 从右往左执行(或从下往上)
核心概念
Loader 本质是一个导出为函数的 JS 模块,接收源文件内容作为参数,返回转换后的内容。它是一个「转换器」,让 Webpack 具备处理任意资源的能力。
Loader 的执行顺序:配置数组中的 loader 从右到左(从下到上)依次执行,前一个 loader 的输出作为后一个的输入,形成一条转换流水线。
text
原始 .scss 源码
──► sass-loader(SCSS 编译为 CSS)
──► css-loader(解析 CSS 依赖)
──► style-loader(注入到 DOM)
──► 有效 JS 模块以处理样式为例,配置 use: ['style-loader', 'css-loader', 'sass-loader'],实际执行顺序为 sass-loader → css-loader → style-loader。
常用 Loader:
| Loader | 作用 |
|---|---|
babel-loader | 将 ES6+ / JSX 转译为兼容性代码 |
css-loader / style-loader | 解析 CSS 依赖 / 将 CSS 注入 DOM |
sass-loader / less-loader | 编译 SCSS / Less 为 CSS |
postcss-loader | 自动加前缀、压缩等 CSS 后处理 |
ts-loader | 编译 TypeScript |
| Asset Modules(webpack5 内置) | 替代 file-loader / url-loader 处理资源 |
Loader 编写要点
- 单一职责:每个 loader 只做一件事,便于组合复用
- 无状态:不应在多次编译间保留状态
- 使用
this.async()处理异步操作
插件 (Plugin)
- loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务
- 插件功能包括:打包优化,资源管理,注入环境变量等
核心概念
Plugin 是一个带有 apply 方法的类。Webpack 启动时调用 apply(compiler),插件通过 compiler.hooks 注册到构建生命周期的各个**钩子(Hook)**上,从而在合适的时机介入构建流程。它基于 Webpack 的 Tapable 事件流机制实现。
Loader 与 Plugin 的区别:
| 维度 | Loader | Plugin |
|---|---|---|
| 本质 | 转换函数 | 带 apply 方法的类 |
| 作用 | 转换单个模块的内容 | 介入整个构建生命周期 |
| 运行时机 | 模块加载/解析阶段 | 从初始化到输出的任意钩子 |
| 粒度 | 文件级 | 构建级 |
常用 Plugin:
| Plugin | 作用 |
|---|---|
HtmlWebpackPlugin | 自动生成 HTML 并注入打包后的资源 |
MiniCssExtractPlugin | 将 CSS 抽离为独立文件 |
DefinePlugin | 注入全局常量(如环境变量) |
CleanWebpackPlugin | 构建前清理输出目录(webpack5 可用 output.clean) |
TerserPlugin | JS 压缩混淆(生产模式默认启用) |
BundleAnalyzerPlugin | 可视化分析产物体积 |
一个最简 Plugin 的形态
javascript
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap("MyPlugin", (stats) => {
console.log("构建完成!");
});
}
}CommonJS 模块打包原理
核心概念
Webpack 打包后的产物,本质上是一个自执行函数(IIFE),内部用一个 modules 对象存放所有模块(键为模块路径,值为包裹模块代码的函数),并实现了一套简易的 require 函数:通过 cache 缓存已加载模块,首次加载时执行模块函数并把结果挂到 module.exports 上。这就是「模块化」在浏览器里得以运行的底层机制。
下面通过手写实现,逐步还原 Webpack 对不同模块系统的打包处理。
CommonJS 模块 简易实现
功能说明:展示 CommonJS 模块的基本加载机制,包括模块缓存和导出处理。
js
(() => {
let modules = {
"./src/title.js": (module) => {
// CommonJS 模块
module.exports = "title";
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();CommonJS 加载 CommonJS 模块
功能说明:展示 CommonJS 模块之间的相互加载和导出机制。
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// CommonJS 模块
exports.name = "title_hl";
exports.age = "title_18";
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();CommonJS 加载 ES6 模块
功能说明:展示 CommonJS 如何加载 ES6 模块,包括 ES6 模块的导出转换机制。
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// ES6 模块导出
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORTS, // 值是一个 getter
age: () => age,
});
// 默认导出
const DEFAULT_EXPORTS = "title";
// 命名导出
const age = 18;
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给 exports 添加属性,标记为 ES 模块
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果 definition 有,exports 没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
(() => {
let title = require("./src/title.js");
console.log(title);
})();
})();ES6 模块加载 ES6 模块
功能说明:展示 ES6 模块之间的相互加载机制,包括 ES6 模块的动态绑定特性。
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// ES6 模块的导出
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORTS, // 值是一个 getter
age: () => age,
});
// 默认导出
const DEFAULT_EXPORTS = "title";
// 命名导出
let age = 18;
// ES Module 获取的是最终的值
// 导出的值的引用
setTimeout(() => {
age = 20;
}, 1000);
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给 exports 添加属性,标记为 ES 模块
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果 definition 有,exports 没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
let exports = {};
(() => {
// 只要打包前的模块是一个 ES module,那么就会调用 require.r 方法进行处理
require.r(exports);
let title = require("./src/title.js");
console.log(title);
})();
})();ES6 模块加载 CommonJS 模块
功能说明:展示 ES6 模块如何加载 CommonJS 模块,包括模块适配机制。
js
(() => {
let modules = {
"./src/title.js": (module, exports) => {
// CommonJS 模块
module.exports = {
name: "title_name",
age: "title_age",
};
},
};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给 exports 添加属性,标记为 ES 模块
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果 definition 有,exports 没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
// 处理默认导出
require.n = (module) => {
var getter =
module && module.__esModule ? () => module["default"] : () => module;
return getter;
};
let exports = {};
(() => {
// 只要打包前的模块是一个 ES module,那么就会调用 require.r 方法进行处理
require.r(exports);
let title = require("./src/title.js");
var title_default = require.n(title);
console.log(title_default());
console.log(title.age);
})();
})();模块异步加载原理
功能说明:展示 Webpack 如何实现模块的异步加载,包括 JSONP 加载机制和 chunk 管理。
核心概念
当代码中使用 import() 动态导入时,Webpack 会把该模块单独打包成一个 chunk 文件。运行时通过 JSONP 方式(动态创建 script 标签)按需加载这个 chunk,加载完成后触发回调,把模块合并进 modules 并 resolve 对应的 Promise。
- 1、通过 chunkId 去查找对应的代码块文件(如 hello.main.js)
- 2、通过动态创建
script标签加载 chunk 文件(JSONP 方式) - 3、加载完成后,会调用 webpackJsonpCallback 函数,将 chunk 文件中的模块添加到 modules 中,并通过 installedChunks 记录 chunk 文件加载状态
text
动态 import ──► 生成 chunkId ──► 查找 chunk 文件 ──► JSONP 加载 script
──► chunk 执行 ──► 执行 webpackJsonpCallback ──► 触发回调 resolve ──► Promise 返回模块js
let modules = {};
let cache = {};
function require(moduleId) {
let cacheModule = cache[moduleId];
if (cacheModule) {
return cacheModule.exports;
}
let module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports);
return module.exports;
}
// 判断对象是否具有某个属性
require.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// 给 exports 添加属性,标记为 ES 模块
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 绑定属性
require.d = (exports, definition) => {
for (let key in definition) {
// 如果 definition 有,exports 没有,则绑定
if (require.o(definition, key) && !require.o(exports, key)) {
// getter
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
require.f = {};
// 返回此文件对应的访问路径
require.p = "";
// 返回此代码块对应的文件名
require.u = function (chunkId) {
return chunkId + ".main.js";
};
// 存放加载的代码块的状态
// key 是代码块的名字
// 0 表示已经加载完成了
var installedChunks = {
main: 0,
// 'hello': [resolve, reject, promise]
};
require.e = (chunkId) => {
let promises = [];
require.f.j(chunkId, promises);
return Promise.all(promises);
};
/**
* 通过 JSONP 异步加载一个 chunkId 对应的代码块文件,其实就是 hello.main.js
* 会返回一个 Promise
* @param {*} chunkId 代码块 ID
* @param {*} promises promise 数组
*/
require.f.j = function (chunkId, promises) {
// 当前的代码块的数据
let installedChunkData;
// 创建一个 promise
const promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
promises.push(promise);
const url = require.p + require.u(chunkId);
require.l(url);
};
// 加载远程的模块
require.l = function (url) {
let script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
var chunkLoadingGlobal = (window["webpack5"] = []);
chunkLoadingGlobal.push = webpackJsonpCallback;
/**
*
* @param {*} chunkIds 代码块 ID 数组
* @param {*} moreModules 额外的模块定义
*/
function webpackJsonpCallback([chunkIds, moreModules]) {
const resolves = [];
for (let i = 0; i < chunkIds.length; i++) {
const chunkId = chunkIds[i];
resolves.push(installedChunks[chunkId][0]);
installedChunks[chunkId] = 0; // 表示此代码块已经下载完毕
}
// 合并模块定义到 modules 去
for (const moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
// 依次取出 resolve 方法并执行 => promise resolve
while (resolves.length) {
resolves.shift()();
}
}
/**
* require.e 异步加载 hello 代码块文件 hello.main.js
* promise 成功后会把 hello.main.js 里面的代码定义合并到 require.m 对象上,也就是 modules 上
* 调用 require 方法加载 ./src/hello.js 模块,获取模块的导出对象,进行打印
*/
require
.e("hello")
.then(require.bind(require, "./src/hello.js"))
.then((result) => {
console.log(result);
});Webpack 编译流程
功能说明:展示 Webpack 的完整编译流程,从初始化到输出的各个步骤。
text
初始化参数(合并配置)
└► 创建 Compiler 对象
└► 加载插件(调用 apply)
└► 执行 run 开始编译
└► 从 entry 构建依赖图
└► 调用 Loader 编译模块 ◄─┐
│ 有依赖? ── 是 ─────┘
│ 否
└► 组装 Chunk
└► 生成输出文件
└► 写入文件系统- 初始化参数:从配置文件和 Shell 语句中读取参数,与 Webpack 默认参数合并,得出最终的配置对象
- 初始化 Compiler 对象:使用上一步得到的参数创建 Compiler 对象
- 加载插件:加载所有配置的插件,调用插件的
apply函数并传递 Compiler 对象 - 执行编译:执行 Compiler 对象的 run 方法开始执行编译
- 构建依赖图:执行 Compilation 对象的 build,根据配置中的 entry 找出入口文件
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译
- 递归处理依赖:找出该模块依赖的模块,递归编译直到所有入口依赖的文件都处理完毕
- 生成 Chunk:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 生成输出文件:把每个 Chunk 转换成一个单独的文件加入到输出列表
- 写入文件系统:根据配置确定输出的路径和文件名,把文件内容写入到文件系统
两个核心对象
- Compiler:全局唯一,代表完整的 Webpack 环境配置,从启动到关闭的整个生命周期只创建一次
- Compilation:代表一次资源构建,watch 模式下文件每次变化都会创建新的 Compilation,因此支持增量构建
Module、Chunk、Bundle
- Module:各个源码文件,Webpack 中一切皆模块
- Chunk:多模块合并成的代码块
- Bundle:最终的输出文件
三者关系
它们是构建过程中不同阶段的产物:Module 是输入(开发者编写的一个个源文件),Chunk 是中间过程(Webpack 根据依赖关系与分包策略把模块组织成的代码块),Bundle 是输出(Chunk 最终生成的物理文件)。多数情况下 Chunk 与 Bundle 一一对应,但如 MiniCssExtractPlugin 可能让一个 Chunk 产出 JS 和 CSS 两个 Bundle。
text
Module a.js ┐
Module b.js ├──► Chunk main ──┬──► Bundle main.js
Module c.css ┘ └──► Bundle main.cssTree Shaking
核心概念
Tree Shaking(摇树优化)是指在打包时移除未被使用的代码(dead code)。它依赖 ES Module 的静态结构特性:import / export 在编译阶段就能确定依赖关系,因此 Webpack 能静态分析出哪些导出从未被引用,从而在压缩阶段删除它们。
生效前提:
- 使用 ES Module 语法(
import/export),CommonJS 因为是运行时动态加载无法 Tree Shaking - 生产模式(
mode: 'production')下自动开启,依赖 Terser 完成删除 - 依赖包需支持 ESM,且
package.json中合理声明sideEffects
sideEffects 副作用陷阱
如果一个模块被 import 但只产生副作用(如引入全局 CSS、polyfill),却没有导出被使用,Tree Shaking 可能误删它。可在 package.json 中声明:
json
{
"sideEffects": ["*.css", "./src/polyfill.js"]
}设为 false 表示所有模块都无副作用(可放心摇树),数组则表示这些文件有副作用、不可删除。
HMR 热更新原理
核心概念
HMR(Hot Module Replacement,模块热替换)能在不刷新整个页面的前提下,用新模块替换旧模块,保留应用状态。它是 webpack-dev-server 提供的开发体验核心能力。
工作流程:
text
文件修改
└► Webpack 重新编译(生成 hash 与补丁)
└► dev-server 通过 WebSocket 通知浏览器 hash
└► 浏览器 HMR runtime 发起 JSONP 拉取补丁
└► 拿到更新的 module
└► 执行 module.hot.accept 替换旧模块
└► 界面局部更新(状态保留)- 1、
webpack-dev-server启动时与浏览器建立 WebSocket 长连接 - 2、文件变化触发重新编译,生成本次编译的 hash 和描述变更的 manifest(.json) 与 补丁(.js)
- 3、通过 WebSocket 把新 hash 推送给浏览器端的 HMR runtime
- 4、runtime 通过 JSONP / fetch 拉取补丁文件
- 5、通过
module.hot.accept接受更新,替换模块并触发回调,实现局部刷新
框架的封装
React 的 react-refresh、Vue 的 vue-loader 内部都调用了 module.hot.accept,帮你处理组件级的热替换与状态保留,因此业务代码通常无需手写 HMR API。
Module Federation 模块联邦
核心概念
Module Federation(模块联邦)是 Webpack 5 的重磅特性,允许多个独立构建的应用在运行时相互共享模块。一个应用(Remote)可以暴露模块,另一个应用(Host)可以在运行时动态加载这些远程模块,是实现微前端的重要方案之一。
- Host(消费方):通过
remotes配置消费远程模块 - Remote(生产方):通过
exposes配置暴露自己的模块 - shared:声明共享依赖(如 React),避免重复加载,实现单例
javascript
// remote 应用
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: { "./Button": "./src/Button" },
shared: ["react", "react-dom"],
});
// host 应用
new ModuleFederationPlugin({
name: "hostApp",
remotes: { remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js" },
shared: ["react", "react-dom"],
});与传统方案的区别
相比 npm 包(构建时集成)或 iframe(运行时但完全隔离),Module Federation 实现了运行时的代码级共享,各应用可独立部署、独立发版,同时共享依赖,兼顾了解耦与性能。
性能优化
Webpack 性能优化分为构建速度和产物体积两个维度。
一、提升构建速度
| 手段 | 说明 |
|---|---|
cache(webpack5 内置) | 持久化缓存,二次构建大幅提速 |
thread-loader | 多进程处理耗时 loader(如 babel) |
include / exclude | 缩小 loader 处理范围(如排除 node_modules) |
resolve.alias / extensions | 减少模块查找路径与后缀尝试 |
DllPlugin(较老方案) | 预编译第三方库,webpack5 已可用持久化缓存替代 |
二、优化产物体积
| 手段 | 说明 |
|---|---|
| Tree Shaking | 移除未使用代码 |
| 代码分割(SplitChunks) | 拆包 + 按需加载,减小首屏体积 |
| 压缩 | TerserPlugin(JS)、CssMinimizerPlugin(CSS) |
| Gzip / Brotli | compression-webpack-plugin 生成压缩产物 |
| 图片优化 | image-minimizer-webpack-plugin 压缩图片 |
| 按需引入 | 如 babel-plugin-import 按需加载组件库 |
优化前先分析
使用 webpack-bundle-analyzer 可视化产物构成,先定位体积大头再优化,避免盲目配置。构建慢时可用 speed-measure-webpack-plugin 定位耗时环节。
Vite 为什么比 Webpack 启动速度快?
核心差异
根本区别在于开发环境的构建策略:Webpack 是 Bundle-based(先打包再启动),Vite 是 ESM-based(不打包,按需编译)。项目越大,Webpack 启动越慢,而 Vite 的启动速度几乎与项目规模无关。
Vite
- 1、Vite 利用浏览器原生 ES Module 的支持,省略了打包步骤,直接启动开发服务器,当代码被请求时,根据需要动态编译模块
- 2、Vite 利用 esbuild 预编译依赖,esbuild 使用 Go 编写,比 JS 编写的打包器预编译速度更快
Webpack
- 1、Webpack 一切皆为模块,启动后会从 entry 开始递归解析 entry 依赖的所有模块,在编译过程中会根据配置的 Loader 对模块进行编译,编译完后再找出该模块依赖的模块,然后递归编译,所以 Webpack 启动会先编译 entry 依赖的所有模块,启动时间较慢
生产环境的取舍
Vite 快主要体现在开发环境。生产构建时 Vite 仍使用 Rollup 打包(为兼容性与产物优化)。Webpack 生态成熟、配置灵活、社区插件丰富,在复杂工程与老项目中仍是稳妥选择。
CommonJS 和 ES Module
| 特性 | CommonJS | ES Module |
|---|---|---|
| 导入语法 | require() | import |
| 导出语法 | module.exports / exports | export default / export |
| 加载方式 | 同步加载 | 异步加载 |
| 编译时机 | 运行时加载 | 编译时静态解析 |
| Node.js 支持 | 所有版本默认支持 | v12+ 支持,需 .mjs 扩展名或 "type": "module" |
关键差异说明:
加载机制
- CommonJS 在运行时同步加载模块(阻塞执行)
- ES Module 在编译阶段构建依赖关系图,异步加载
解析方式
javascript// CommonJS (动态) const module = condition ? "A" : "B"; const lib = require(module); // 运行时确定 // ES Module (静态) import lib from "./module.js"; // 编译时解析 - 路径必须是字面量!作用域
- CommonJS 导出值的拷贝(基础类型复制,引用类型共享地址)
- ES Module 导出值的值的引用(所有类型均为动态引用)
循环引用处理
- CommonJS:可能获得未初始化的中间状态
- ES Module:通过 "live binding" 机制保证最终一致性
浏览器兼容
- CommonJS 不能直接在浏览器运行(需打包工具转换)
- ES Module 被所有现代浏览器原生支持 (
<script type="module">)
Webpack 分包与拆包
Webpack 分包,也叫代码分割,是指将代码拆分成多个 bundle,实现按需加载或并行加载的技术。
它的核心价值主要有三点:
- 减少首屏加载时间:只加载当前页面需要的代码,显著提升首屏速度
- 提高缓存利用率:将不常变的第三方库单独打包,利用浏览器缓存
- 优化用户体验:按需加载减少初始下载量,非阻塞加载提升交互体验
在实际项目中,我们通常会对路由级别、大型组件和第三方库进行分包,实现最优的加载性能。
有哪些方式可以实现代码分割?
Webpack 提供了三种主要的代码分割方式:
第一种是入口起点:
javascriptmodule.exports = { entry: { app: "./app.js", admin: "./admin.js" }, };这种方式简单直接,但缺点是重复代码无法自动去重。
第二种是动态导入(最常用):
javascript// 使用 import() 语法 const loadModule = () => import("./module"); // React 项目中使用 const LazyComponent = React.lazy(() => import("./LazyComponent"));这是目前最推荐的方式,可以实现真正的按需加载。
第三种是 SplitChunksPlugin: 这是 Webpack 4 之后内置的插件,功能最强大,可以智能提取公共模块。
在实际项目中,我们通常会组合使用动态导入和 SplitChunksPlugin,达到最佳效果。
SplitChunksPlugin 的配置和使用
SplitChunksPlugin 是 Webpack 内置的智能分包插件,我通常这样配置:
javascriptmodule.exports = { optimization: { splitChunks: { chunks: "all", // 对所有模块进行分割 minSize: 20000, // 超过20KB才单独打包 cacheGroups: { // 第三方库 vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: 10, }, // 公共模块 common: { name: "common", minChunks: 2, // 被2个以上chunk引用 priority: 5, }, }, }, }, };关键配置说明:
chunks: 'all'是最佳实践,会对所有类型模块进行分割minSize避免生成过小文件,减少请求数cacheGroups是核心,可以定义不同的提取策略在实际项目中,我还会进一步细分第三方库,比如把 React、Vue 这些基础框架单独打包,因为它们更新频率低,可以充分利用缓存。
你在项目中如何进行代码分割?有什么最佳实践?
在我的项目中,我采用分层分包策略:
第一层:路由级分割
javascript// 每个路由单独打包 const Home = lazy(() => import(/* webpackChunkName: "home" */ "./Home")); const About = lazy(() => import(/* webpackChunkName: "about" */ "./About"));第二层:组件级分割 对于超过 50KB 的大型组件或复杂功能模块,我会单独分包。
第三层:第三方库优化
javascriptcacheGroups: { react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/ }, utils: { test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/ } }
代码分割可能会带来什么问题?如何避免?
代码分割确实可能带来一些问题,我主要关注以下几点:
问题一:请求过多 过度分割会导致 HTTP 请求数增加,反而影响性能。 解决方案:合理设置
minSize(通常 20-30KB),避免生成过小 chunk。问题二:加载顺序问题 动态加载的模块可能存在依赖关系问题。 解决方案:使用 Webpack 的魔法注释确保依赖关系:
javascriptimport(/* webpackPreload: true */ "./critical-dep");
结语
Webpack 的核心可以浓缩为一句话:以依赖图为骨架,用 Loader 做转换、用 Plugin 管流程,最终打包成可运行的静态产物。理解了「模块 → Chunk → Bundle」的流转,以及 Loader/Plugin 各自的介入时机,再面对 Tree Shaking、HMR、代码分割、Module Federation 这些进阶特性时,就能从「会配置」上升到「懂原理」。工具会更迭(Vite、Rspack、Turbopack),但打包器解决的核心问题——模块化、依赖分析、产物优化——是相通的。