webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它会递归地构建一个依赖关系图,包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
Webpack 的核心原理是将应用程序的所有模块视为一个图形结构,图中的每一个点代表一个模块,边代表模块之间的依赖关系。webpack 通过分析模块之间的依赖关系,生成一个包含所有模块的图谱,并将其转换为浏览器可以理解的代码。
下面是 webpack 的一些基本概念,以及一个简单的 webpack 实现,帮助理解 webpack 的工作原理:
webpack 的工作流程可以分为以下几个阶段
webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化
Compiler对象,加载所有配置的插件,执行对象的run方法开始编译 - 确定入口:根据配置中的
entry找出所有的入口文件 - 编译模块:从入口文件出发,调用所有配置的
loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经历过本步骤的处理 - 完成模块编译:在经历第 4 步使用
loader翻译完所有模块后,得到了每一个模块的最终代码和依赖关系图 - 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk,再把每个Chunk转成一个单独的文件加入到输出列表中,这一步是可以修改输出内容的最后机会 - 输出完成:在确定好输出内容后,根据配置好的输出路径和文件名,写入到文件系统中
在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到事件后可以做出相应的处理,并且插件可以调用 webpack 提供的 API 来修改 webpack 的运行结果。webpack 的插件机制是基于事件驱动的,使用了 Tapable 库来实现。
专有名词
entry
- 入口起点指示 webpack 应该使用哪个模块来开始构建其内部依赖图。一个入口起点可以是一个字符串、数组或对象。webpack 会从这个入口起点找出有那些模块和库是入口起点(直接或间接)依赖的。
配置示例如下:
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
}
}
Output
output属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认是./dist。基本上,整个应用程序结构,都会被你编译到你指定的输出路径的文件中
Module
开发中的单个模块,在 webpack 的世界,⼀切皆模块,⼀个模块对应⼀个⽂件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块。
Chunk
chunk 是 webpack 在编译过程中生成的一个代码块,它可以包含一个或多个模块。webpack 会根据模块之间的依赖关系,将
它们打包成一个或多个 chunk。
例如,当你配置 Webpack 进行代码分割(Code Splitting)时,会生成多个 chunk:
- 入口 chunk:对应
entry配置,可以是一个或多个 - 异步 chunk:通过动态
import()生成
Loader
让 webpack 拥有了加载和解析⾮ JavaScript ⽂件的能⼒。loader 可以将文件转换为有效模块,webpack 也可以处理这些模块,并将它们添加到依赖图中。
比如说我们需要处理 css 文件,可以使用 css-loader 和 style-loader 来处理 css 文件。
css-loader 负责解析 css 文件中的 import 和 url()
style-loader 负责将 css 插入到 DOM 中
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
或者是处理 .vue 文件,使用 vue-loader 来处理 vue 文件
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
}
]
}
}
常见的 loader 有那些?
babel-loader:把 ES6 转换成 ES5css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件url-loader:和file-loader类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去image-loader:加载并且压缩图⽚⽂件source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试eslint-loader:通过 ESLint 检查 JavaScript 代码
Plugin
Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性
在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
插件的使用方式是通过 apply 方法来注册插件
const webpack = require('webpack');
class MyPlugin {
apply(compiler) {
// 在这里注册插件
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
console.log('编译完成');
});
}
}
module.exports = {
plugins: [
new MyPlugin()
]
}
常见的 plugin 有那些?
-
define-plugin:定义环境变量 -
html-webpack-plugin:简化 html ⽂件创建 -
uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码 -
webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度 -
webpack-bundle-analyzer: 可视化 webpack 输出⽂件的体积 -
mini-css-extract-plugin: CSS 提取到单独的⽂件中,⽀持按需加载
手写一个简易 Webpack
这里实现一个简单的 webpack 帮助理解原理,实现的 mini-webpack 需求如下:
以下是一个简单的项目结构,用来测试我们的迷你 webpack:
entry 为 src/index.js,需要对该代码进行打包输出静态资源,能在浏览器正常执行,代码如下:
import { add } from './math.js';
console.log(add(1, 2));
math.js 文件如下:
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
最终执行迷你 webpack 输出结果:
node mini-webpack.js
如何实现以上需求呢??
很明显的它有下面的特性:
- 入口文件,并且可以引用其他模块的资源,资源下面又会引用其他的模块资源,那么就能形成一个数结构。如下:

左侧是当前的需求的模块结构,右侧是可能会扩展的模块结构
观察发现:需要处理一个根节点,多个子节点的树结构,那么在开发过程中估计需要使用到递归的方式来处理
当前的项目文件就是入口为 src/index.js 的一个树结构的数据,其中文件内容为节点的 value,文件内容的 import 或者 require 函数里面的引用文件是指向子节点的指针,所有的操作都是在操作这棵文件树
- 代码的执行需要有完整的上下文环境,才能执行成功,也就是说引用的模块资源需要在执行时能被找到并且使用。最简单暴力的实现方式就是把所有的模块都放在一个文件中执行,这样就能保证执行时能找到引用的模块资源。但是会有命名冲突的问题,因为都在同为一个上下文。
解法是:使用函数作用域来隔离模块的上下文环境,使用闭包来实现模块的隔离,方式很妙
实现如下:
mini-webpack.js
// 本次实现的webpack是一个简化版的webpack,主要用于学习和理解webpack的原理
// 主要功能:
// 1. 功能简化:只实现最基本的模块打包功能
// 2. 无加载器系统:无法处理非js文件
// 3. 无插件系统:缺乏扩展能力
// 4. 无代码拆分:不支持代码分割和动态导入
// 5. 无热更新:不支持开发环境的实时重载
// 通过一以下示例,我们可以理解webpack的核心工作原理:
// - 构建依赖图
// - 模块转换
// - 代码生成
// - 运行时模拟
// 开始编写
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
// 分析模块:
// 1. 读取文件
// 2. 使用@babel/parser 将代码转换为抽象语法树 (AST)
// 3. 通过 @babel/traverse 遍历AST,找出所有的import语句和require语句,并且收集依赖
// 4. 使用@babel/core 将代码转换为ES5代码
// 5. 返回模块的文件名、依赖关系和转换后的代码
function parserModule(filename) {
// 读取文件内容
const content = fs.readFileSync(filename, "utf-8");
// 将代码转换为ast抽象语法树
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newFile = path.join(dirname, node.source.value);
debugger;
// 保存依赖
dependencies[node.source.value] = newFile;
},
});
// 将ast转换为es5代码, import 语句将会被转换为 require 语句
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"],
});
return {
filename,
dependencies,
code: code,
};
}
const entry = "./src/index.js";
console.log(parserModule(entry));
/**
{
filename: './src/index.js',
dependencies: { './math.js': 'src/math.js' },
code: '"use strict";\n' +
'\n' +
'var _math = require("./math.js");\n' +
'console.log((0, _math.add)(1, 2));'
}
*/
// 依赖图构建(makeDependencyGraph)
// 1. 从入口文件开始,递归分析所有依赖
// 2. 将分析结果构建成一个依赖图对象,其中键是模块路径,值是模块的依赖和代码
function makeDependenciesGraph(entry) {
const entryModule = parserModule(entry);
const graphArray = [entryModule];
for (let index = 0; index < graphArray.length; index++) {
// 获取当前模块的依赖记录对象
const { dependencies } = graphArray[index];
Object.values(dependencies).forEach((dependencyPath) => {
const childModule = parserModule(dependencyPath);
graphArray.push(childModule);
});
// 将数组转换为图结构:
// {
// 'filename': {
// 'dependencies': {
// 'path': 'filename',
// },
// code: 'code',
// }
// }
const graph = {};
graphArray.forEach((module) => {
graph[module.filename] = {
dependencies: module.dependencies,
code: module.code,
};
});
return graph;
}
}
console.log(makeDependenciesGraph(entry));
/**
{
'./src/index.js': {
dependencies: { './math.js': 'src/math.js' },
code: '"use strict";\n' +
'\n' +
'var _math = require("./math.js");\n' +
'console.log((0, _math.add)(1, 2));'
},
'src/math.js': {
dependencies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.add = add;\n' +
'exports.subtract = subtract;\n' +
'function add(a, b) {\n' +
' return a + b;\n' +
'}\n' +
'function subtract(a, b) {\n' +
' return a - b;\n' +
'}'
}
}
*/
// 代码生成(generateCode)
// 1. 将依赖图转换为可执行的js代码
// 2. 创建一个运行时环境,通过自定义的require函数来加载模块
// 3. 模拟CommonJS的模块加载机制
function generateCode(entry) {
const graph = JSON.stringify(makeDependenciesGraph(entry));
// 闭包 + 自执行
return `
(function (graph) {
// 模块加载函数
function require(module) {
// 相对路径转为决定路径
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
// 模块导出对象
const exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
}
require('${entry}');
})(${graph});
`;
}
const entryFile = "./src/index.js";
const output = generateCode(entryFile);
console.log(output)
fs.writeFileSync('./bundle.js', output, 'utf-8')
输出结果如下,肉眼看是如何运行的:
(function (graph) {
// 模块加载函数
function require(module) {
// 相对路径转为决定路径
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
// 模块导出对象
const exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
}
require("./src/index.js");
})({
"./src/index.js": {
dependencies: { "./math.js": "src/math.js" },
code: '"use strict";\n\nvar _math = require("./math.js");\nconsole.log((0, _math.add)(1, 2));',
},
"src/math.js": {
dependencies: {},
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.add = add;\nexports.subtract = subtract;\nfunction add(a, b) {\n return a + b;\n}\nfunction subtract(a, b) {\n return a - b;\n}',
},
});
上面👆🏻的代码可以直接在控制台执行输出哦
会发现 import 的模块转换成了 require 的形式,模块的导出内容执行后会放到 require 函数的 exports 对象上
模块的代码处理为节点的 value,并且收集散落在代码中的 import 和 require 语句,构建节点的指针数据。然后从根出发将树数据转换为数组数据,再将数组数据扁平为一个便于索引的对象数据
妙哉,算法的用处在这儿体现的非常的好
总结
webpack 是一个巨大的 node.js 应用,完整的 webpack 有非常多的功能和插件,在使用过程中只需要了解其整体架构和部分细节即可
它把复杂的视线影藏了起来,给我们暴露的是一些简单易用的 API,让用户可以快速达成目的。同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区的支持,补足了大量缺失的功能,让 webpack 几乎能胜任任何场景