通过源代码掌握webpack打包原理
webpack启动过程分析
-
开始: 从 webpack命令行说起
- 通过 npm scripts运行webpack
- 开发环境: npm run dev
- 生产环境: npm run build
- 通过webpack直接运行
-
webpack entry.js bundle.js
-
这个过程发生了什么?
- 通过 npm scripts运行webpack
-
查找webpack入口文件
-
在命令行运行以上命令后,npm会让命令行工具进入node_modules/.bin目录查找是否存在webpack.sh(mac或linux系统)或webpack.cmd(windows系统)文件, 如果存在,就执行,不存在,就抛出错误。
-
实际的入口文件是: node_modules/webpack/bin/webpack.js
运行一个命令,如果是全局安装,mac或linux会从useLocal/bin下查找; 如果是局部安装,会从当前目录下的node_modules/.bin目录查找,由于webpack和webpack-cli是局部安装,所以是从node_modules/.bin目录查找
-
哪个包提供了webpack命令呢? webpack还是webpack-cli呢 - 查看webpack和webpack-cli,发现是在webpack下提供此命令的
-
- webpack最终找到 webpack-cli(webpack-command)这个npm包, 并且执行CLI
webpack-cli源码分析
- webpack-cli做的事情
- 引入yargs, 对命令行进行定制
- 分析命令行参数,对各个参数进行转换, 组成编译配置项
- 引用webpack, 根据配置项进行编译和构建
- webpack-cli处理不需要经过编译的命令
- 从 NON_COMPILATION_CMD分析出不需要编译的命令
- webpack-cli提供的不需要编译的命令
const NON_COMPILATION_ARGS = [ "init", // 创建一份webpack配置文件 "migrate", // 进行webpack版本迁移 "add", // 往webpack配置文件中增加属性 "remove", // 往webpack配置文件中删除属性 "serve", // 运行webpack-serve "generate-loader", // 生成webpack-loader代码 "generate-plugin", // 生成webpack-plugin代码 "info" // 返回与本地环境相关的一些信息 ]
- 命令行工具包yargs介绍
- 提供命令和分组参数
- 动态生成help帮助信息
- webpack-cli使用args分析
- 参数分组(config/config-args.js),将命令划分为9类
- Config options:配置相关参数(文件名称、运行环境等)
- Basic options: 基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
- Module options:模块参数,给loader设置扩展
- Output options: 输出参数(输出路径、输出文件名称)
- Advanced options: 高级用法(设记录置、缓存设置、监听频率、bail等)
- Resolving options: 解析参数(alias和解析的文件后缀设置)
- Optimizing options: 优化参数
- Stats options: 统计参数
- options: 通用参数(帮助命令、版本信息等)
- 参数分组(config/config-args.js),将命令划分为9类
- webpack-cli执行的结果
- webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数 options
- 最终会根据配置参数实例化 webpack 对象, 然后执行构建流程
Tapable插件架构与Hooks设计
-
webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行
-
核心对象 Compiler 继承 Tapable
-
核心对象 Compilation 继承 Tapable
-
Tapable是什么?
- Tapable是一个类似于Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。
- Tapable库暴露里很多Hook(钩子)类,为插件提供挂载的钩子
const { SyncHook, // 同步钩子 SyncBailHook, //同步熔断钩子 SyncWaterfallHook, //同步流水钩子 SyncLoopHook, //同步循环钩子 AsyncParallelHook, //异步并发钩子 AsyncParallelBailHook, //异步并发熔断钩子 AsyncSeriesHook, //异步串行钩子 AsyncSeriesBailHook, //异步串行熔断钩子 AsyncSeriesWaterfallHook //异步串行流水钩子 } = require("tapable ")
type function Hook 所有钩子的后缀 Waterfall 同步方法,但是它会传值给下一个函数 Bail 熔断:当函数有任何返回值,就会在当前执行函数停止 Loop 监听函数返回true表示继续循环,返回undefine表示循环结束 Sync 同步方法 AsyncSeries 异步串行钩子 AsyncParallel 异步并行执行钩子 -
Tapabel的使用-new Hook 新建钩子
- Tapabel暴露出来的都是类方法, new 一个类方法获得我们需要的钩子
- class 接收数组参数options,非必传。 类方法会根据传参, 接收同样数量的参数。
const hook1 = new SyncHook(["arg1","arg2","arg3"]);
-
Tapable的使用-钩子的绑定与执行
- Tabpack提供了同步&异步绑定钩子 的方法,并且他们都有绑定事件和执行事件对应的方法
Async* Sync* 绑定:tapAsync/tapPromise/tap 绑定:tap 执行:callAsync/promise 执行:call -
Tapable 的使用-hook 基本用法示例
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]); //绑定事件到webapck事件流 hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3 //执行绑定的事件 hook1.call(1,2,3)
-
Tapable 的使用-实际例子演示
定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、 brake( accelerate 接受一个参数)、异步钩子 calculateRoutes 使用钩子对应的绑定和执行方法 calculateRoutes 使用 tapPromise 可以返回一个 promise 对象 const { SyncHook, AsyncSeriesHook } = require('tapable'); class Car { constructor() { this.hooks = { accelerate: new SyncHook(['newspeed']), brake: new SyncHook(), calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"]) } } } const myCar = new Car(); //绑定同步钩子 myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); //绑定同步钩子 并传参 myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); //绑定一个异步Promise钩子 myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { // return a promise return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); },1000) }) }); myCar.hooks.brake.call(); myCar.hooks.accelerate.call(10); console.time('cost'); //执行异步钩子 myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo').then(() => { console.timeEnd('cost'); }, err => { console.error(err); console.timeEnd('cost'); });
Tapable 是如何和 webpack 联系起来的?
if (Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
}
- 模拟 Compiler.js
module.exports = class Compiler { constructor() { this.hooks = { accelerate: new SyncHook(['newspeed']), brake: new SyncHook(), calculateRoutes: new AsyncSeriesHook(["source", "target","routesList"]) } } run(){ this.accelerate(10) this.break() this.calculateRoutes('Async', 'hook', 'demo') } accelerate(speed) { this.hooks.accelerate.call(speed); } break() { this.hooks.brake.call(); } calculateRoutes() { this.hooks.calculateRoutes.promise(...arguments).then(() => { }, err => { console.error(err); }); } }
- 插件 my-plugin.js
class MyPlugin{ constructor() { } apply(compiler){ compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) => { return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); },1000) }); }); } }
- 模拟插件执行
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
const compiler = new Compiler();
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
compiler.run();
Webpack 流程篇
-
webpack的编译都按照下面的钩子调用顺序执行
- WebpackOptionsApply
- 将所有的配置 options 参数转换成 webpack 内部插件
- 使用默认插件列表
- 举例:
- output.library -> LibraryTemplatePlugin
- externals -> ExternalsPlugin
- devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
- AMDPlugin, CommonJsPlugin
- RemoveEmptyChunksPlugin
- Compiler hooks
- 流程相关:
- ·(before-)run
- ·(before-/after-)compile
- ·make
- ·(after-)emit ·done
- 监听相关:
- ·watch-run
- ·watch-close
- 流程相关:
- Compilation
- Compiler 调用 Compilation 生命周期方法
- ·addEntry -> addModuleChain
- ·finish (上报模块错误)
- ·seal
- Compiler 调用 Compilation 生命周期方法
- ModuleFactory
- Module
- NormalModule
- Build
- ·使用 loader-runner 运行 loaders
- ·通过 Parser 解析 (内部是 acron)
- ·ParserPlugins 添加依赖
- Build
- Compilation hooks
- 模块相关:
- ·build-module
- ·failed-module
- ·succeed-module
- 资源生成相关:
- ·module-asset
- ·chunk-asset
- 优化和 seal相关:
- (after-)seal
- optimize
- optimize-modules
- (-basic/advanced)
- after-optimize-modules
- after-optimize-chunks
- after-optimize-tree
- optimize-chunk-modules (-basic/advanced)
- after-optimize-chunk-modules
- optimize-module/chunk-order
- before-module/chunk-ids
- (after-)optimize-module/chunk-ids
- before/after-hash
- Chunk 生成算法
-
- webpack 先将 entry 中对应的 module 都生成一个新的 chunk
-
- 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
-
- 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
-
-
重复上面的过程,直至得到所有的 chunks
-
-
- 模块相关:
最后:
如果你有梦想,一定要来大城市,这里可以帮您实现你想要的
有些梦想,需要借助城市的力量才能实现
其它系列文章链接