简单概述 Webpack 整体运行流程
- 读取参数
- 实例化
Compiler
entryOption
阶段,读取入口文件Loader
编译对应文件,解析成AST
- 找到对应依赖,递归编译处理,生成
chunk
- 输出到
dist
webpack 打包主流程源码阅读
通过打断点的方式阅读源码,来看一下命令行输入 webpack 的时候都发生了什么?P.S. 以下的源码流程分析都基于 webpack4
先附上一张自己绘制的执行流程图
初始化阶段
- 初始化参数(
webpack.config.js+shell options
)
webpack
的几种启动方式
- 通过
webpack-cli
执行 会走到./node_modules/.bin/webpack-cli
(执行) - 通过
shell
执行webpack
,会走到./bin/webpack.js
- 通过
require("webpack")
执行 会走到./node_modules/webpack/lib/webpack.js
追加 shell
命令的参数,如-p , -w,
通过 yargs
解析命令行参数convert-yargs
把命令行参数转换成 Webpack 的配置选项对象 同时实例化插件 new Plugin()
- 实例化
Compiler
阅读完整源码点击这里:webpack.js
// webpack入口 const webpack = (options, callback) => { let compiler // 实例Compiler if (Array.isArray(options)) { // ... compiler = createMultiCompiler(options) } else { compiler = createCompiler(options) } // ... // 若options.watch === true && callback 则开启watch线程 if (watch) { compiler.watch(watchOptions, callback) } else { compiler.run((err, stats) => { compiler.close((err2) => { callback(err || err2, stats) }) }) } return compiler }
webpack 的入口文件其实就实例了 Compiler
并调用了 run
方法开启了编译,
- 注册
NodeEnvironmentPlugin
插件,挂载 plugin 插件,使用 WebpackOptionsApply 初始化基础插件
在此期间会 apply
所有 webpack
内置的插件,为 webpack
事件流挂上自定义钩子
源码仍然在webpack.js文件
const createCompiler = (rawOptions) => { // ...省略代码 const compiler = new Compiler(options.context) compiler.options = options //应用Node的文件系统到compiler对象,方便后续的文件查找和读取 new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging, }).apply(compiler) // 加载插件 if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { // 依次调用插件的apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。 if (typeof plugin === 'function') { plugin.call(compiler, compiler) } else { plugin.apply(compiler) } } } // 应用默认的Webpack配置 applyWebpackOptionsDefaults(options) // 随即之后,触发一些Hook compiler.hooks.environment.call() compiler.hooks.afterEnvironment.call() // 内置的Plugin的引入,对webpack options进行初始化 new WebpackOptionsApply().process(options, compiler) compiler.hooks.initialize.call() return compiler }
编译阶段
- 启动编译(
run/watch
阶段)
这里有个小逻辑区分是否是 watch
,如果是非 watch
,则会正常执行一次 compiler.run()
。
如果是监听文件(如:--watch
)的模式,则会传递监听的 watchOptions
,生成 Watching
实例,每次变化都重新触发回调。
如果不是监视模式就调用 Compiler
对象的 run
方法,befornRun->beforeCompile->compile->thisCompilation->compilation
开始构建整个应用。
const { SyncHook, SyncBailHook, AsyncSeriesHook } = require('tapable') class Compiler { constructor() { // 1. 定义生命周期钩子 this.hooks = Object.freeze({ // ...只列举几个常用的常见钩子,更多hook就不列举了,有兴趣看源码 done: new AsyncSeriesHook(['stats']), //一次编译完成后执行,回调参数:stats beforeRun: new AsyncSeriesHook(['compiler']), run: new AsyncSeriesHook(['compiler']), //在编译器开始读取记录前执行 emit: new AsyncSeriesHook(['compilation']), //在生成文件到output目录之前执行,回调参数:compilation afterEmit: new AsyncSeriesHook(['compilation']), //在生成文件到output目录之后执行 compilation: new SyncHook(['compilation', 'params']), //在一次compilation创建后执行插件 beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), //在一个新的compilation创建之前执行 make: new AsyncParallelHook(['compilation']), //完成一次编译之前执行 afterCompile: new AsyncSeriesHook(['compilation']), watchRun: new AsyncSeriesHook(['compiler']), failed: new SyncHook(['error']), watchClose: new SyncHook([]), afterPlugins: new SyncHook(['compiler']), entryOption: new SyncBailHook(['context', 'entry']), }) // ...省略代码 } newCompilation() { // 创建Compilation对象回调compilation相关钩子 const compilation = new Compilation(this) //...一系列操作 this.hooks.compilation.call(compilation, params) //compilation对象创建完成 return compilation } watch() { //如果运行在watch模式则执行watch方法,否则执行run方法 if (this.running) { return handler(new ConcurrentCompilationError()) } this.running = true this.watchMode = true return new Watching(this, watchOptions, handler) } run(callback) { if (this.running) { return callback(new ConcurrentCompilationError()) } this.running = true process.nextTick(() => { this.emitAssets(compilation, (err) => { if (err) { // 在编译和输出的流程中遇到异常时,会触发 failed 事件 this.hooks.failed.call(err) } if (compilation.hooks.needAdditionalPass.call()) { // ... // done:完成编译 this.hooks.done.callAsync(stats, (err) => { // 创建compilation对象之前 this.compile(onCompiled) }) } this.emitRecords((err) => { this.hooks.done.callAsync(stats, (err) => {}) }) }) }) this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.readRecords((err) => { this.compile(onCompiled) }) }) }) } compile(callback) { const params = this.newCompilationParams() this.hooks.beforeCompile.callAsync(params, (err) => { this.hooks.compile.call(params) //已完成complication的实例化 const compilation = this.newCompilation(params) //触发make事件并调用addEntry,找到入口js,进行下一步 // make:表示一个新的complication创建完毕 this.hooks.make.callAsync(compilation, (err) => { process.nextTick(() => { compilation.finish((err) => { // 封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件 compilation.seal((err) => { this.hooks.afterCompile.callAsync(compilation, (err) => { // 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程, // 不然运行流程将会一直卡在这不往下执行 return callback(null, compilation) }) }) }) }) }) }) } emitAssets() {} }
- 编译模块:(
make
阶段)
- 从 entry 入口配置文件出发, 调用所有配置的
Loader
对模块进行处理, - 再找出该模块依赖的模块, 通过
acorn
库生成模块代码的AST
语法树,形成依赖关系树(每个模块被处理后的最终内容以及它们之间的依赖关系), - 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环每个依赖;再递归本步骤直到所有入口依赖的文件都经过了对应的 loader 处理。
- 解析结束后,
webpack
会把所有模块封装在一个函数里,并放入一个名为modules
的数组里。 - 将
modules
传入一个自执行函数中,自执行函数包含一个installedModules
对象,已经执行的代码模块会保存在此对象中。 - 最后自执行函数中加载函数(
webpack__require
)载入模块。
class Compilation extends Tapable { constructor(compiler) { super(); this.hooks = {}; // ... this.compiler = compiler; // ... // 构建生成的资源 this.chunks = []; this.chunkGroups = []; this.modules = []; this.additionalChunkAssets = []; this.assets = {}; this.children = []; // ... } // buildModule(module, optional, origin, dependencies, thisCallback) { // ... // 调用module.build方法进行编译代码,build中 其实是利用acorn编译生成AST this.hooks.buildModule.call(module); module.build( /**param*/ ); } // 将模块添加到列表中,并编译模块 _addModuleChain(context, dependency, onModule, callback) { // ... // moduleFactory.create创建模块,这里会先利用loader处理文件,然后生成模块对象 moduleFactory.create({ contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) = > { const addModuleResult = this.addModule(module); module = addModuleResult.module; onModule(module); dependency.module = module; // ... // 调用buildModule编译模块 this.buildModule(module, false, null, null, err = > {}); }); } // 添加入口模块,开始编译&构建 addEntry(context, entry, name, callback) { // ... this._addModuleChain( // 调用_addModuleChain添加模块 context, entry, module = > { this.entries.push(module); }, // ... ); } seal(callback) { this.hooks.seal.call(); // ... //完成了Chunk的构建和依赖、Chunk、module等各方面的优化 const chunk = this.addChunk(name); const entrypoint = new Entrypoint(name); entrypoint.setRuntimeChunk(chunk); entrypoint.addOrigin(null, name, preparedEntrypoint.request); this.namedChunkGroups.set(name, entrypoint); this.entrypoints.set(name, entrypoint); this.chunkGroups.push(entrypoint); GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); GraphHelpers.connectChunkAndModule(chunk, module); chunk.entryModule = module; chunk.name = name; // ... this.hooks.beforeHash.call(); this.createHash(); this.hooks.afterHash.call(); this.hooks.beforeModuleAssets.call(); this.createModuleAssets(); if (this.hooks.shouldGenerateChunkAssets.call() !== false) { this.hooks.beforeChunkAssets.call(); this.createChunkAssets(); } // ... } createHash() { // ... } // 生成 assets 资源并 保存到 Compilation.assets 中 给webpack写插件的时候会用到 createModuleAssets() { for (let i = 0; i < this.modules.length; i++) { const module = this.modules[i]; if (module.buildInfo.assets) { for (const assetName of Object.keys(module.buildInfo.assets)) { const fileName = this.getPath(assetName); this.assets[fileName] = module.buildInfo.assets[assetName]; this.hooks.moduleAsset.call(module, fileName); } } } } createChunkAssets() { asyncLib.forEach( this.chunks, (chunk, callback) = > { // manifest是数组结构,每个manifest元素都提供了 `render` 方法,提供后续的源码字符串生成服务。至于render方法何时初始化的,在`./lib/MainTemplate.js`中 let manifest = this.getRenderManifest() asyncLib.forEach( manifest, (fileManifest, callback) = > {... source = fileManifest.render() this.emitAsset(file, source, assetInfo) }, callback) }, callback) } }
class SingleEntryPlugin { apply(compiler) { compiler.hooks.compilation.tap( 'SingleEntryPlugin', (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory ) } ) compiler.hooks.make.tapAsync( 'SingleEntryPlugin', (compilation, callback) => { const { entry, name, context } = this const dep = SingleEntryPlugin.createDependency(entry, name) compilation.addEntry(context, dep, name, callback) } ) } static createDependency(entry, name) { const dep = new SingleEntryDependency(entry) dep.loc = { name } return dep } }
概括一下 make
阶段单入口打包的流程,大致为 4 步骤
- 执行
SingleEntryPlugin
(单入口调用SingleEntryPlugin
,多入口调用MultiEntryPlugin
,异步调用DynamicEntryPlugin
),EntryPlugin
方法中调用了Compilation.addEntry
方法,添加入口模块,开始编译&构建 addEntry
中调用_addModuleChain
,将模块添加到依赖列表中,并编译模块- 然后在
buildModule
方法中,调用了NormalModule.build
,创建模块之时,会调用runLoaders
,执行Loader
,利用acorn
编译生成AST
- 分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的
require
语法替换成webpack_require
来模拟模块化操作。
从源码的角度,思考一下, loader
为什么是自右向左执行的,loader
中有 pitch
也会从右到左执行的么?
runLoaders
方法调用 iteratePitchingLoaders
去递归查找执行有 pich
属性的 loader
;若存在多个 pitch
属性的 loader
则依次执行所有带 pitch
属性的 loader
,执行完后逆向执行所有带 pitch
属性的 normal
的 normal loader
后返回 result
,没有 pitch
属性的 loader
就不会再执行;若 loaders
中没有 pitch
属性的 loader
则逆向执行 loader;执行正常
loader 是在 iterateNormalLoaders
方法完成的,处理完所有 loader
后返回 result
。
出自文章你真的掌握了 loader 么?- loader 十问(https://juejin.im/post/5bc1a73df265da0a8d36b74f)