一文搞懂 webpack 核心原理,手写 webpack 竟如此简单?
背景
webpack
作为前端开发中的重中之重,却鲜有人对其核心原理进行深入研究,本文完全是跟随其核心方法及流程由浅入深分析讲解,旨在能为你在 webpack
核心原理的理解上带来些许灵感,更上一层楼!
前提
- 随手打开过
webpack
的官网 - 随手打开过
babel
的官网 - 简单了解过
tapable
的功能 - 简单了解过
node
是干嘛的 - 简单写过点
js
- 执行过
npm run build
(这一步很关键!!!) - 有手就行
版本
5.73.0
示例
手写 webpack
核心原理完整示例代码链接
详情
-
目录结构
.
├── main
│ └── index.js
├── package-lock.json
├── package.json
├── src
│ ├── div.js
│ ├── mul.js
│ ├── sub.js
│ └── sum.js
├── webpack.config.js
-
文件内容
sum.js
function sum(num1, num2) {
return num1 + num2
}
module.exports = { sum }
sub.js
function sub(num1, num2) {
return num1 - num2
}
module.exports = { sub }
mul.js
function mul(num1, num2) {
return num1 * num2
}
module.exports = { mul }
div.js
function div(num1, num2) {
return num1 / num2
}
module.exports = { div }
index.js
const { sum } = require("../src/sum.js");
const { sub } = require("../src/sub.js");
const { mul } = require("../src/mul.js");
const { div } = require("../src/div.js");
const sumResult = sum(50, 50);
const subResult = sub(50, 50);
const mulResult = mul(50, 50);
const divResult = div(50, 50);
console.log({
sumResult,
subResult,
mulResult,
divResult,
});
webpack.config.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
entry: "./main/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].bundle.js",
},
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: "webpack! webpack! webpack! webpack! webpack!",
footer: true,
}),
],
};
-
打包产物
(() => {
var __webpack_modules__ = {
"./src/div.js": (module) => {
function div(num1, num2) {
return num1 / num2;
}
module.exports = {
div: div,
};
},
"./src/mul.js": (module) => {
function mul(num1, num2) {
return num1 * num2;
}
module.exports = {
mul: mul,
};
},
"./src/sub.js": (module) => {
function sub(num1, num2) {
return num1 - num2;
}
module.exports = {
sub: sub,
};
},
"./src/sum.js": (module) => {
function sum(num1, num2) {
return num1 + num2;
}
module.exports = {
sum: sum,
};
},
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
var _require = __webpack_require__("./src/sum.js"),
sum = _require.sum;
var _require2 = __webpack_require__("./src/sub.js"),
sub = _require2.sub;
var _require3 = __webpack_require__("./src/mul.js"),
mul = _require3.mul;
var _require4 = __webpack_require__("./src/div.js"),
div = _require4.div;
var sumResult = sum(50, 50);
var subResult = sub(50, 50);
var mulResult = mul(50, 50);
var divResult = div(50, 50);
console.log({
sumResult: sumResult,
subResult: subResult,
mulResult: mulResult,
divResult: divResult,
});
})();
})();
/*! webpack! webpack! webpack! webpack! webpack! */
-
结果分析
-
打包之后的代码是一个
IIFE
,主要包含以下内容- webpack_modules:
key
为模块路径,value
为模块内容作为执行体的函数 - webpack_module_cache:缓存
- webpack_require:加载模块的方法
- IIFE:这里执行
__webpack_require__
方法并返回module.exports
- webpack_modules:
-
babel-loader
已将代码处理为es5
的语法 -
BannerPlugin
已将内容写入打包后的文件
源码浅析
-
npm run build
- 执行
./node_modules/.bin/webpack
文件 webpack-cli
负责处理执行command
及config
相关的配置并传入webpack
webpack
根据传入的config
以一个或多个js
文件为入口,递归检查每个js
模块的依赖,从而构建一个依赖关系图,然后依据该图将整个应用程序打包成一个或多个bundle
-
run
- 初始化
command
配置并注册相关的callback
- 解析
command
执行callback
加载webpack
- 执行
webpack
生成compiler
runWebpack
核心代码如下:
async runWebpack(options, isWatchCommand){
const callback = (error, stats) => { ... }
compiler = await this.createCompiler(options, callback);
}
createCompiler
核心代码如下:
async createCompiler(options, callback) {
let config = await this.loadConfig(options);
config = await this.buildConfig(config, options)
compiler = this.webpack(config.options, callback)
}
webpack-cli
相对于 webpack
主要做了以下几件事情:
- 解析命令行的指令
- 解析
webpack.config.js
- 执行
this.webpack(config, cb)
得到compiler
对象
-
webpack
- 该方法接收
options
和callback
两个参数 - 根据是否传入了
callback
决定是否执行compiler.run()
create
方法用于创建一个包含compiler
对象的object
,核心代码如下:
const create = () => {
let compiler;
const webpackOptions = options;
compiler = createCompiler(webpackOptions)
return { compiler, ...}
}
webpack
核心代码如下:
const webpack = (options, callback) => {
const create = () => {
let compiler;
const webpackOptions = options;
compiler = createCompiler(webpackOptions)
}
if (callback) {
const { compiler, ... } = create();
compiler.run()
}
}
-
createCompiler
- 对
options
进行normalized
处理 - 实例化
compiler
- 注入
compiler
到plugin
的apply
方法中 - 执行相关的
hooks
- 在
process()
中将所有的option
转为相应的plugin
进行处理 - 返回
compiler
createCompiler
核心代码如下:
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
}
process
核心代码如下:
class WebpackOptionsApply extends OptionsApply {
process(options, compiler) {
// 1. 处理 options 转为 plugins
// 2. 处理 entry 并在 EntryOptionPlugin 中注册 entryOption hook
new EntryOptionPlugin().apply(compiler);
// 3. 执行上一步注册的 entryOption hook
compiler.hooks.entryOption.call(options.context, options.entry);
return options;
}
}
EntryOptionPlugin
核心代码如下:
class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
static applyEntryOption(compiler, context, entry) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
EntryPlugin
核心代码如下:
class EntryPlugin {
apply(compiler) {
// 1. 注册关键的 make hook callback 以便后面调用
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
}
-
Compiler
- 简单来讲,
compiler
是一个包含当前运行webpack
所有配置的对象,被实例化时会利用tapable
初始化大量hook
以便在webpack
整个生命周期去注册,因此,一旦被创建便存在于webpack
的整个生命周期中。 Compiler
核心代码如下:
class Compiler {
constructor(context, options) {
this.hooks = Object.freeze({
// 初始化大量的 hook
})
}
compile(callback) {
const params = this.newCompilationParams();
// 2. 创建 compilation 对象
const compilation = this.newCompilation(params);
// 3. 执行 make hook callback
this.hooks.make.callAsync(compilation, err => {
this.hooks.finishMake.callAsync(compilation, err => {
compilation.seal()
})
})
}
// compiler 被创建后执行 run 方法
run(callback) {
const onCompiled = (err, compilation) => {}
const run = () => {
// 1. 执行 compile
this.compile(onCompiled);
};
run();
}
}
-
Compilation
compilation
代表了一次单一的版本构建和资源的生成,即在编译过程中每当检测到某个文件发生变化,就会执行一次新的编译过程,从而生成编译结果。compilation
表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,也就是说它只存在于编译阶段。
class Compilation {
constructor() {
this.hooks = Object.freeze({ ... })
this.entries = new Map(); // 存放所有的入口信息
this.modules = new Set(); // 存放解析后所有的模块信息
this.chunks = new Set(); // 存放 chunks 信息
this.assets = {}; // 存放生成的资源信息
}
seal() {} // 对资源进行封存最终输出 assets
}
-
make
- 执行
make
的callback
,从entry
开始对module
进行add
、build
、parse
factorize
会执行resolver
去处理module
和loader
的路径相关信息addModule
用于生成模块间的ModuleGraph
buildModule
会通过runLoader
执行对应的loaders
- 执行
parse
方法解析module
生成AST
- 执行
processModuleDependencies
递归处理模块间的依赖并重复执行上述流程
class EntryPlugin {
apply(compiler) {
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
})
}
}
执行 make
的回调开始编译,由于编译涉及大量的引用和回调,此处仅沿着执行线进行简单梳理,流程如下:
以上就是处理单个模块的核心流程,拿到本次处理结果后会执行 processModuleDependencies
根据依赖对模块进行递归处理直到 parse
完所有的模块保存在 compilation
中
_doBuild(options, compilation, resolver, fs, hooks, callback) {
const processResult = (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
this._source = this.createSource({...})
return callback();
}
runLoaders({ ... }, (err, result) => {
processResult(err, result.result)
})
}
callback(err) {
const handleParseResult = result => {
return handleBuildDone();
};
const source = this._source.source();
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
handleParseResult(result)
}
-
seal
- 执行
compilation.seal()
- 处理
Optimization
相关配置 - 根据处理好的
modules
生成chunks
- 生成
assets
挂载到compilation.assets
上 - 根据
output
配置生成文件夹 - 执行
emitFiles
输出资源到文件夹
源码小结
总结
webpack
真正的执行过程远比此处复杂得多,所以本文仅仅只是提取了执行过程中的核心方所做的事情进行详解,使你更轻易的了解到它所做的事情;如果你有时间且有兴趣,不妨可以自己从头撸上一遍,毕竟阅读源码的过程是无聊且枯燥的,能不看就尽量别看了吧,除非你刻意要做那个很卷的人!
至此,我们大概可以明确了手写 webpack
的基本思路,执行过程如下所示:
手写思路
-
package.json
"scripts": {
"build": "node ./webpack/index.js",
}
执行该指令模拟去加载那个可执行文件
-
index
require("./webpack-cli")
-
webpack-cli
// 1、获取文件的配置
const webpackConfig = require("../webpack.config.js");
// 2、获取命令行配置
const getCommandOption = () => {
const commandOptionList = process.argv.slice(2);
const option = {};
commandOptionList.forEach((item) => {
const [key, value] = item.split("=");
if (key[0] !== "-" || key[1] !== "-") {
console.error("options is invaild");
return false;
}
if (key.slice(2) && value) {
option[key.slice(2)] = value;
}
});
return option;
};
// webpack-cli 包含大量处理 command 相关的逻辑,这里我就不写了,有兴趣可自行阅读源码
// 3、合并配置
const config = { ...webpackConfig, ...commandConfig };
// 4、执行 webpack
webpack(config, (err, stats) => {
if (err) {
console.err(error);
}
console.log("stats", stats);
});
// 至此,webpack-cli 已经完成了自己的任务
// 关于为什么安装 webpack 也会提示一并安装 webpack-cli ? 我们可以只安装 webpack 吗 ?
// 当然可以。因为 webpack 依赖 webpack-cli 会处理配置相关的功能。
// 如果我们可以自己解析并生成 config 交给 webpack 处理,那就只安装 webpack 就可以了
// 毕竟 vite 之前的 vue 和 react ,二者也都没有安装过 webpack-cli
-
webpack
const Compiler = require("./Compiler")
// 简单处理下 entry 兼容 string 和 object 的形式传入, 数组自己可以尝试处理下哈
const getNormalizedEntryStatic = (entry) => {
if (typeof entry === "string") {
return {
main: entry,
};
}
if (Object.prototype.toString.call(entry) === "[object Object]") {
return { ...entry };
}
};
const createCompiler = (options) => {
options.entry = getNormalizedEntryStatic(options.entry);
const compiler = new Compiler(options);
// 将 compiler 传入 plugins 中
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
return compiler;
};
const webpack = (config, callback) => {
// 1、创建 compiler 对象
const compiler = createCompiler(config);
if (callback) {
// 2、调用 run 方法之前我们初始化好的 run hook
compiler.hooks.run.call();
// 3、执行 compiler 的 run 方法
compiler.run((err, stats) => {
if (err) {
console.error(err);
}
console.log(stats);
});
}
};
module.exports = webpack
-
Compiler
const { SyncHook } = require("tapable");
const Compilation = require("./Compilation")
class Compiler {
constructor(options) {
// 1、简单初始化两个 hook 意思一下
this.hooks = {
run: new SyncHook(),
done: new SyncHook(),
};
// 2、保存一个 context,这里跳过用户配置简单处理
this.context = process.cwd();
// 3、保存一份 options
this.options = options;
}
run() {
// 4、创建 Compilation 这里我们把这个 compiler 直接传过去
const compilation = new Compilation(this);
// 5、从 entry 开始处理
compilation.addEntry();
}
}
module.exports = Compiler
-
Compilation
const fs = require("fs");
const path = require("path");
const parse = require("./parse")
const runLoaders = require("./runLoaders")
const codeGeneration = require("./codeGeneration")
class Compilation {
constructor(compiler) {
this.compiler = compiler;
this.entries = new Set(); // 入口模块
this.modules = new Set(); // 依赖模块
this.chunks = new Set(); // chunks
this.sourceCode = ""; // 源码
this.assets = {}; // 即将打包的资源
}
// 读取模块 => 用 loader 处理模块 => 解析模块
build(moduleName, modulePath) {
const sourceCode = fs.readFileSync(modulePath, "utf-8");
runLoaders(this, modulePath, sourceCode);
return parse(this, moduleName, modulePath);
}
addEntry() {
// 1、从入口开始依次处理
const { context, options } = this.compiler;
for (const entryName in options.entry) {
if (Object.hasOwnProperty.call(options.entry, entryName)) {
const entryPath = options.entry[entryName];
// 2、build 处理
const buildCompletedModule = this.build(
entryName,
path.resolve(context, entryPath)
);
// 3、处理完成添加到 entries
this.entries.add(buildCompletedModule);
// 4、对资源进行打包输出
this.seal(entryName, buildCompletedModule);
}
}
}
addChunk(entryName, entryModule, callback) {
const chunk = {
name: entryName,
entryModule: entryModule,
modules: Array.from(this.modules).filter((i) =>
i.name.includes(entryName)
),
};
this.chunks.add(chunk);
callback();
}
seal(entryName, entryModule) {
this.addChunk(entryName, entryModule, () => {
this.emitAsset();
});
}
emitAsset() {
const output = this.compiler.options.output;
this.chunks.forEach((chunk) => {
const outputFileName = output.filename.replace("[name]", chunk.name);
this.assets[outputFileName] = codeGeneration(chunk);
});
if (!fs.existsSync(output.path)) {
fs.mkdirSync(output.path);
}
Object.keys(this.assets).forEach((fileName) => {
const filePath = path.join(output.path, fileName);
fs.writeFileSync(filePath, this.assets[fileName]);
});
this.compiler.hooks.done.call();
}
}
module.exports = Compilation
-
runLoaders
// 遍历加载所有的 loader 作用在源码模块上
const runLoaders = (compilation, modulePath, sourceCode) => {
const usedLoaders = [];
const rules = compilation.compiler.options.module.rules;
for (const rule of rules) {
if (rule.test.test(modulePath)) {
if (rule.loader) {
usedLoaders.push(rule.loader);
}
if (rule.use) {
usedLoaders.push(...rule.use);
}
}
}
// 倒序处理
for (let i = usedLoaders.length - 1; i >= 0; i--) {
compilation.sourceCode = require(usedLoaders[i])(sourceCode);
}
};
module.exports = runLoaders
-
parse
const path = require("path");
const parser = require("@babel/parser");
const { genAbsPath, genModuleId } = require("./utils");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const types = require("@babel/types");
const parse = (compilation, moduleName, modulePath) => {
// 1. 创建模块
const module = {
id: genModuleId(compilation.compiler.context, modulePath),
name: [moduleName],
__source: "",
dependencies: new Set(),
};
// 2. 生成 ast
const ast = parser.parse(compilation.sourceCode, {
sourceType: "module",
});
// 3. 对源码进行转换 这里仅针对 require 引入
traverse(ast, {
CallExpression: ({ node }) => {
if (node.callee.name === "require") {
const nodeName = node.arguments[0].value;
const nodeDirName = path.posix.dirname(modulePath);
const nodeAbsPath = genAbsPath(
path.posix.join(nodeDirName, nodeName),
compilation.compiler.options.resolve.extensions,
nodeName,
nodeDirName
);
const moduleId = genModuleId(compilation.compiler.context, nodeAbsPath);
node.callee = types.identifier("__webpack_require__");
node.arguments = [types.stringLiteral(moduleId)];
// 判断模块是否重复引入处理
const loadedModules = Array.from(compilation.modules).map((i) => i.id);
if (!loadedModules.includes(moduleId)) {
module.dependencies.add(moduleId);
} else {
compilation.modules.forEach((i) => {
if (i.id === moduleId) {
i.name.push(nodeName);
}
});
}
}
},
});
// 4. 代码生成
const { code } = generator(ast);
// 5. 在 module 对象上挂载一份生成好的源码
module.__source = code;
// 6. 处理模块依赖
module.dependencies.forEach((dep) => {
const depModule = compilation.build(moduleName, dep);
compilation.modules.add(depModule);
});
return module;
};
module.exports = parse;
-
utils
const fs = require("fs");
const path = require("path");
const genAbsPath = (modulePath, extensions, nodeName, nodeDirName) => {
if (fs.existsSync(modulePath)) return modulePath;
for (const extension of extensions) {
if (fs.existsSync(modulePath + extension)) {
return modulePath + extension;
}
}
};
const genModuleId = (context, modulePath) => {
return `./${path.posix.relative(context, modulePath)}`;
};
module.exports = {
genAbsPath,
genModuleId,
};
-
codeGeneration
// 简单模拟下代码生成的过程
const codeGeneration = (chunk) => {
const { entryModule, modules } = chunk;
return `
(() => {
var __webpack_modules__ = {
${modules
.map((module) => {
return `
'${module.id}': (module) => {
${module.__source}
}
`;
})
.join(",")}
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
${entryModule.__source}
})();
})();
`;
};
module.exports = codeGeneration;
点击查看更多内容
2人点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦