webpack 是一个 JS 应用程序的静态模块打包器。它会递归的构建一个依赖关系图,包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。 Webpack 的核心原理是将应用程序的所有模块视为一个图形结构,图中的每一个点代表一个模块,边代表模块之间的依赖关系。webpack 通过分析模块之间的依赖关系,生成一个包含所有模块的图谱,并将其转换为浏览器可以理解的代码。
webpack 的工作流程可以分为以下几个阶段
webpack 的运行流程是一个串行的过程,从启动到结束会一次执行以下流程:
- 初始化参数: 从配置文件和Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始编辑
- 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的loader对模块进行翻译,在找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经历过本步骤的处理
- 完成模块编译:在经历第4步使用 loader 翻译完所有模块后,得到了每一个模块的最终代码和依赖关系图
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转成一个单独的文件加入到输出列表中,这一步是可以修改输出内容的最后机会
- 输出完成:在确定好输出内容后,根据配置好的输出路径和文件名,写入到文件系统中
在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到事件后可以做出相应的处理,并且插件可以调用 webpack 提供的 API 来修改 webpack 的运行结果。webpack 的插件机制是基于事件驱动的,使用了 Tapable 库来实现。
专有名词
EnTry
- 入口起点指示webpack 应该使用哪个模块来开始构建其内部依赖图。一个入口起点可以是一个字符串、数组或对象。webpack会从这个入口起点找出有那些模块和库有直接或间接的依赖关系。
配置示例如下:
js
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
}
}
Output
- output 属性高搜 webpack 在哪里输出他所创建的bundles,以及如何命名这些文件,默认是
./dist
。基本上,整个应用程序结构,都会被你编译到你指定的输出路径的文件中
Module
- 在 webpack 中,模块是指任何可以被 webpack 处理的文件。webpack 支持多种类型的模块,包括JavaScript、css、图片、字体等。其中原生支持的类型为:js和json,其他类型的文件需要通过 loader 来处理。
Chunk
- chunk 是 webpack 在编译过程中生成的一个代码块,它可以包含一个或多个模块。webpack 会根据模块之间的依赖关系,将它们打包成一个或多个 chunk
Loader
Loader 让 webpack 能够去处理那些非 JavaScript 文件。提供了webpack 处理非 JavaScript 文件的能力。loader 可以将文件转换为有效模块,webpack 也可以处理这些模块,并将它们添加到依赖图中。
比如说我们需要处理css文件,可以使用 css-loader
和 style-loader
来处理 css 文件。
css-loader
负责解析 css 文件中的 import 和 url() style-loader
负责将 css 插入到 DOM 中
js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
或者是处理.vue文件,使用 vue-loader
来处理 vue 文件
js
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
}
]
}
}
Plugin
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。它是对webpack功能的扩展
插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件借卡功能机器强大,可以用来处理各种各样的任务。插件的使用方式是通过 apply
方法来注册插件
js
const webpack = require('webpack');
class MyPlugin {
apply(compiler) {
// 在这里注册插件
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
console.log('编译完成');
});
}
}
module.exports = {
plugins: [
new MyPlugin()
]
}
重点环节详解
1️⃣ 初始化阶段
- 读取配置文件(webpack.config.js)
- 合并命令行参数
- 初始化默认参数
- ⭐关键点:插件挂载
手写一个简易 Webpack
这里实现一个简单的 webpack 帮助理解原理,实现的mini-webpack需求如下:
以下是一个简单的项目结构,用来测试我们的迷你 webpack: entry为 src/index.js,需要对该代码进行打包输出静态资源,能在浏览器正常执行,代码如下:
javascript
import { add } from './math.js';
console.log(add(1, 2));
math.js 文件如下:
javascript
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
最终执行迷你 webpack 输出结果:
bash
node mini-webpack.js
如何实现以上需求呢??
很明显的它有下面的特性:
- 入口文件,并且可以引用其他模块的资源,资源下面又会引用其他的模块资源,那么就能形成一个数结构。如下:

左侧是当前的需求的模块结构,右侧是可能会扩展的模块结构
观察发现:需要处理一个根节点,多个子节点的树结构,那么在开发过程中估计需要使用到递归的方式来处理
当前的项目文件就是入口为src/index.js的一个树结构的数据,其中文件内容为节点的value,文件内容的import或者require函数里面的引用文件是指向子节点的指针,所有的操作都是在操作这棵文件树
- 代码的执行需要有完整的上下文环境,才能执行成功,也就是说引用的模块资源需要在执行时能被找到并且使用。最简单暴力的实现方式就是把所有的模块都放在一个文件中执行,这样就能保证执行时能找到引用的模块资源。但是会有命名冲突的问题,因为都在同为一个上下文。
解法是:使用函数作用域来隔离模块的上下文环境,使用闭包来实现模块的隔离,方式很妙
实现如下:
mini-webpack.js
javascript
// 本次实现的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')
输出结果如下,肉眼看是如何运行的:
javascript
(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 几乎能胜任任何场景