共计 9143 个字符,预计需要花费 23 分钟才能阅读完成。
Webpack 构建流程
webpack 外围构建流程总结
webpack5 和 webpack4 源码不同,本文参考 webpack5.38.1 源码
1. 外围构建流程总结
1.1 启动 webpack
1.1.1 执行 node_modules/.bin/webpack.cmd
1.1.2 执行 node_modules/webpack/bin/webpack.js
1.2 启动 webpack-cli
1.2.1 执行 node_modules/webpack-cli/bin/cli.js
1.2.2 查看 webpack 是否装置,没有则提醒装置
1.2.3 调用 runCLI() 解析命令行参数,创立编译对象;执行程序:runCLI() => cli.run() => cli.buildCommand()
1.3 创立编译对象 compiler
1.3.1 调用 cli.buildCommand() => cli.createCompiler() => webpack()
1.4 实例化编译对象 compiler,预埋外围钩子
1.4.1 挂载 Node 文件读写能力到 compiler
1.4.2 挂载所有插件到 compiler
1.4.3 挂载默认配置到 compiler
1.4.4 启动外围钩子的监听:compilation、make
1.5 执行办法 compiler.run(),启动编译
1.5.1 触发 beforeRun 钩子
1.5.2 触发 run 钩子
- 1.5.2.1 次要性能是 触发 compile、make 钩子,make 实现后处理回调
- 1.5.2.2 make 完结后,通过 onCompiled 写入代码到文件输入到 dist 目录
1.6 执行办法 compiler.compile(),实现编译,输入文件
1.6.1 触发钩子 beforeCompile
1.6.2 触发钩子 compile
1.6.3 触发钩子 thisCompilation、compilation
1.6.4 触发钩子 make(外围钩子)- 1.6.4.1 依据入口等配置,创立模块
- 1.6.4.2 实现编译性能:转换代码为 ast 语法树,再将其转换回 code 代码
- 1.6.4.3 对 chunk 进行解决
1.6.5 触发钩子 finishMake
1.6.6 触发钩子 afterCompile
2. 外围钩子的监听
- beforeRun => 未知
- run => 未知
- beforeCompile => 未知
- compile => 未知
- thisCompilation => 未知
- compilation => new WebpackOptionsApply() 中启动监听 => 作用:让 compilation 具备创立模块的能力
- make => new WebpackOptionsApply() 中启动监听 => 作用:打包入口
- finishMake => 未知
- afterCompile => 未知
3. 外围钩子触发程序
- beforeRun => compiler.run() 中触发
- run => compiler.run() 中触发
- beforeCompile => compiler.compile() 中触发
- compile => compiler.compile() 中触发
- thisCompilation => compiler.compile() 中触发 => compiler.newCompilation() 中触发
- compilation => compiler.compile() 中触发 => compiler.newCompilation() 中触发
- make => compiler.compile() 中触发
- finishMake => compiler.compile() 中触发
- afterCompile => compiler.compile() 中触发
4. 外围代码文件门路
// 基于版本:"webpack": "^5.38.1",
"webpack-cli": "^4.7.0"
// 文件门路:node_modules/.bin/webpack.cmd
node_modules/webpack/bin/webpack.js
node_modules/webpack-cli/bin/cli.js
node_modules/webpack/lib/webpack.js
node_modules/webpack/lib/EntryPlugin.js
node_modules/webpack/lib/Compiler.js
node_modules/webpack/lib/Compilation.js
webpack 构建流程详细分析
1. 启动 webpack(命令行执行 webpack)
- 1.1 执行脚本:node_modules/.bin/webpack.cmd
- 1.2 webpack.cmd 中先依据环境变量找到 node.exe
- 1.3 webpack.cmd 中再应用 node.exe 执行 js 脚本:node node_modules/webpack/bin/webpack.js
# node_modules/.bin/webpack.cmd
@ECHO off
SETLOCAL
CALL :find_dp0
# 1.2. 先依据环境变量找到 node.exe
IF EXIST "%dp0%\node.exe" (SET "_prog=%dp0%\node.exe") ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
# 1.3. 再应用 node.exe 执行 js 脚本
"%_prog%" "%dp0%\..\webpack\bin\webpack.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b
- 1.4 webpack.js 中判断是否装置了 webpack-cli
> 1.4.1 没有装置 webpack-cli => 则提醒装置
> 1.4.2 曾经装置 webpack-cli => 执行 runCli,require 了 node_modules/webpack-cli/bin/cli.js
// node_modules/webpack/bin/webpack.js
#!/usr/bin/env node
// ...
const runCli = cli => {const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`);
const pkg = require(pkgPath);
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
// 没有装置 webpack-cli,则提醒装置
if (!cli.installed) {// ...} else {runCli(cli);
}
2. 启动 webpack-cli
- 2.1 启动 webpack-cli:webpack.js 中导入了 node_modules/webpack-cli/bin/cli.js
- 2.2 cli.js 中查看 webpack 是否装置,没有则提醒装置
- 2.3 cli.js 中执行 runCLI(),解析命令行参数,创立编译对象。(例如:读取配置文件,合并默认配置项等。)
> 2.3.1 runCLI() 中执行 cli.run() => 解析命令行参数 => .../lib/webpack-cli.js
> 2.3.2 cli.run() 中执行 cli.buildCommand() => 创立编译对象 => .../lib/webpack-cli.js
// node_modules/webpack-cli/bin/cli.js
#!/usr/bin/env node
// ...
// 2.2 查看 webpack 是否装置
if (utils.packageExists('webpack')) {
// 参数 1:process.argv => 命令行参数 <br>
// 参数 2:Module.prototype._compile => nodejs 底层的编译办法 <br>
// 2.3 执行 runCLI
runCLI(process.argv, originalModuleCompile);
}
else{// ...}
3. 创立编译对象 compiler
- 3.1 调用 cli.createCompiler() 生成一个 compiler
// cli.buildCommand() 中调用 cli.createCompiler() 生成一个 compiler;// cli.buildCommand() 定义在:node_modules/webpack-cli/lib/webpack-cli.js
compiler = await this.createCompiler(options, callback);
// cli.createCompiler() 中调用 webpack(options) 返回一个 compiler
// webpack(options) 定义在:node_modules/webpack/lib/webpack.js
compiler = this.webpack(
// 合并 options
// ...
)
- 3.2 依据参数判断是否要完结命令行,是否 watch
if (isWatch(compiler) && this.needWatchStdin(compiler)) {process.stdin.on('end', () => {process.exit(0);
});
process.stdin.resume();}
4. 实例化编译对象 compiler,预埋外围钩子
- 4.1 webpack() 中调用 createCompiler(),定义在:node_modules/webpack/lib/webpack.js
- 4.2 createCompiler() 中创立一个 comipler 实例
> 4.2.1 createCompiler() 中应用 new Compiler() 实例化一个 compiler,定义在:.../lib/Compiler.js
> 4.2.2 compiler 继承了 tapable,因而它具备钩子的操作能力(监听事件,触发事件,webpack 是一个事件流)
- 4.3 在 compiler 对象身上挂载文件读写的能力:
new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);
- 4.4 在 compiler 对象身上挂载所有 plugins 插件
- 4.5 在 compiler 对象身上挂载默认配置项:
applyWebpackOptionsDefaults(options);
- 4.6 启动 compilation、make 钩子的监听,具体流程如下:
> 4.6.1 createCompiler() 办法中调用 new WebpackOptionsApply().process(options, compiler);
> 4.6.2 而后启动钩子 entryOption 的监听;(留神 hooks 都是 new Compiler 时定义)> 4.6.3 触发钩子 entryOption 的执行回调函数:启动了 compilation 钩子和 make 钩子的监听
// 4.6.2 而后启动钩子 entryOption 的监听,在 EntryOptionPlugin 中定义的。(留神 hooks 都在 new Compiler 时定义)new EntryOptionPlugin().apply(compiler);
// 4.6.3 触发钩子 entryOption 的执行回调函数:启动了 compilation 钩子和 make 钩子的监听;回调在 EntryOptionPlugin 中定义的。compiler.hooks.entryOption.call(options.context, options.entry);
// 4.6.3 触发钩子 entryOption 的执行回调时,执行如下代码;启动钩子 make 的监听。compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {const { context, entry, name} = this
console.log("make 钩子监听执行了~~~~~~")
// compilation.addEntry(context, entry, name, callback)
})
5. webpack() 中执行 compiler.run() 触发一系列钩子
webpack() 定义在:node_modules/webpack/lib/Compiler.js
- 5.1 触发 beforeRun 钩子
- 5.2 触发 run 钩子
- 5.3 执行 compile 办法(该办法外面持续触发了一堆钩子,参考第 6 点)
- 5.4 执行 compile 办法胜利后,执行 onCompiled 将模块代码写入文件:writeFile()
// run() 里就是一堆钩子按着程序触发
const run = () => {
// 5.1 触发 beforeRun 钩子
this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);
// 5.2 触发 run 钩子
this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);
this.readRecords(err => {if (err) return finalCallback(err);
// 5.3 执行 compile 办法
this.compile(onCompiled);
});
});
});
};
6. run() 中执行 compile()
run() 定义在:node_modules/webpack/lib/Compiler.js
- 6.1 触发钩子 beforeCompile
- 6.2 触发钩子 compile
- 6.3 触发钩子 thisCompilation、compilation (让 compilation 具备创立模块的能力)
- 6.4 触发钩子 make(重要:make 外面的 compilation.addEntry 依据入口等一些配置项创立模块,执行编译等操作)
- 6.5 触发钩子 finishMake
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// ...
this.hooks.compile.call(params);
// 在 newCompilation() 外面触发钩子 thisCompilation、compilation
const compilation = this.newCompilation(params);
// ...
this.hooks.make.callAsync(compilation, err => {
// ...
this.hooks.finishMake.callAsync(compilation, err => {// ...});
});
});
7. 对于 addEntry 的剖析(编译入口的重点)
- 7.1 make 钩子触发时执行 compilation.addEntry(),调用前的几个参数阐明:见以下代码正文
- 7.2 make 钩子触发时承受一个 compilation 传递给 addEntry(),addEntry() 是在 new compilation() 外面定义
// entry 以后打包模块的相对路径(/src/index.js)// options 外面包含 name、filename、publicPath 等配置
// context 以后我的项目根门路
// dep 是对以后入口模块依赖关系的解决
const {entry, options, context} = this;
const dep = EntryPlugin.createDependency(entry, options);
// 这里是 make 钩子的监听
// compilation 是 make 钩子触发的时候传给回调函数的值,addEntry 定义在 compilation 实例化的类外面
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {callback(err);
});
});
// 这里是触发 make 钩子
this.hooks.make.callAsync(compilation, err => { // ...})
- 7.3 进入入口函数过后依照以下顺序调用 => Compilation.js
addEntry() => _addEntryItem() => addModuleTree() => handleModuleCreation()
- 7.4 执行 addModuleTree(),在 compilation 当中咱们能够通过 NormalModuleFactory 来创立一个一般模块对象;而后通过 dependencyFactories 获取它
// addModuleTree() 中 moduleFactory 是 NormalModuleFactory 创立的一般模块对象
const moduleFactory = this.dependencyFactories.get(Dep);
- 7.5 handleModuleCreation() 办法中依照以下顺序调用 => Compilation.js
// 留神:factory 就是下面的参数 moduleFactory
factorizeModule() => factorizeQueue() => _factorizeModule() => factory.create()
- 7.6 factory.create() 创立一个模块
> 7.6.1 factory.create() 是定义在 NormalModuleFactory 中的一个成员办法(NormalModuleFactory .js)> 7.6.2 this.hooks.beforeResolve.callAsync 解决 loader 的门路编译工作,在回调外面触发钩子:factorize
> 7.6.3 this.hooks.factorize.callAsync 解决 loader,而后将 factoryResult 传给 callback()
- 7.7 factory.create() 执行实现,将 factoryResult 放到 callback() 中执行
> 7.7.1 factory.create 的 callback() 拿到 result( 理论就是 factoryResult)
const newModule = result.module;
> 7.7.2 newModule 传递给下面的 factorizeModule() 的 callback();// 回调 callback 拿到 newModule,而后执行 addModule
this.factorizeModule({// ...},(err, newModule){// 回调 callback 拿到 newModule
// ...
this.addModule()})
- 7.8 factory.create() 的回调中调用 addModule(newModule) 去创立 module
this.factorizeModule => ... => factory.create() => this.addModule()=> this.buildModule()
- 7.9 this.buildModule() 编译模块,最初将 doBuild 的执行后果传递给 callback;
> 7.9.1 doBuild() 编译实现的后果存在 this.modules 中
> 7.9.2 doBuild() 办法中实现了外围的编译性能:将 js 代码转为 ast 语法树 => 而后再转换成 code 代码
> 7.9.3 build 在 NormalModule.js 文件中,this.buildModule() => build => doBuild
其中一些知识点:
- 什么是取值函数?参考:https://es6.ruanyifeng.com/?search=get+&x=0&y=0#docs/class
const source = {get foo() {return 1}
};
class MyClass {constructor() {// ...}
get prop() {return 'getter';}
set prop(value) {console.log('setter:'+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
- AST 语法树,参考:https://segmentfault.com/a/1190000016231512
tapabel 库与钩子
tapabel 是什么?
tapable 是一个相似于 Node.js 中的 EventEmitter 的库,但更专一于自定义事件的触发和解决。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的模式存在。
tapabel 的分类
同步钩子(syncHook)
- 熔断钩子(syncBialHook)
- 瀑布钩子(syncWaterfallHook)
- 循环钩子(syncLoopHook)
异步钩子(asyncHook)
对于异步钩子的应用,在增加事件监听时会存在三种形式:tap tapAsync tapPromise
- 异步串行钩子(asyncSeriesHook)
- 异步并行钩子(asyncParallelHook)
- 异步并行熔断钩子(asyncParallelBailHook)
钩子的监听与触发
const {SyncHook} = require('tapable')
// 钩子的定义
let hook = new SyncHook(['name', 'age'])
// 钩子的监听
hook.tap('fn1', function (name, age) {console.log('fn1--->', name, age)
})
// 钩子的触发
hook.tap('fn2', function (name, age) {console.log('fn2--->', name, age)
})
hook.call('zoe', 18)
代码示例,参考:本文 Webpack 相干源码
相干知识点:
1. 公布订阅模式和观察者模式
2. NodeJs 中的 EventEmitter
3. Tapable 的相干知识点
特地鸣谢:拉勾教育前端高薪训练营
正文完