原文发布在个人 GitHub 欢迎 star
收藏。webpack
本文源码来源:webpack-4git
在开始编译流程以前,webpack
会处理用户配置,首先进行校验,经过后与默认配置合并或者是调用相应函数进行处理,输出最终的 options
。以后实例化 Compiler
,并传入 options
,并为注册的 plugins
注入 compiler
实例。若是配置了 watch
选项,则添加监听,在资源变化的时候会从新进行编译流程;不然直接进入编译流程:github
const webpack = (options, callback) => { // 校验 validateSchema(webpackOptionsSchema, options); /* ... */ // 处理、合并 options options = new WebpackOptionsDefaulter().process(options); // 实例化 Compiler const compiler = new Compiler(options.context); /* ... */ // 注册插件 if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } // 开始编译流程 if (watch) { compiler.watch(watchOptions, callback); } else { compiler.run((err, stats) => { compiler.close(err2 => { callback(err || err2, stats); }); }); } }
在 compiler.run()
的过程当中,会调用 compile
方法。在 compile
方法中会实例化 Compilation
,模块的编译构建流程都由 Compilation
控制。实例化 Compilation
后紧接着触发 compilation
和 make
事件,从 make
开始主编译流程。这里有一个很细节的点,其实也与 webpack
的插件机制有关,webpack
的插件机制当然让其有很好的扩展性,但对于阅读源码来讲会把人绕晕,众多的钩子,注册钩子函数的代码与触发的地方很难让人找到其联系之处,只能经过编辑器的全局搜索去找到注册的钩子函数。在处理 options
的时候,会调用到 EntryPlugin
插件,其内部会注册 compilation
与 make
事件:web
apply(compiler) { compiler.hooks.compilation.tap( "EntryPlugin", (compilation, { normalModuleFactory }) => { // 设置 DependencyFactories,在添加 moduleChain 的时候会用上 compilation.dependencyFactories.set( EntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = EntryPlugin.createDependency(entry, name); // 编译文件模块的入口 compilation.addEntry(context, dep, name, err => { callback(err); }); }); }
在 make
事件触发后,会调用 compilation.addEntry
从入口文件开始,根据 dep
类型判断使用哪种 moduleFactory
,而且将入口添加到 _preparedEntrypoints
属性中,其结构以下:数组
const slot = { name: name, // entry 的 name request: null, // entry 的 request module: null // 对 entry 文件解析成的最终 module,在 afterBuild 中对其赋值 }; // ... this._preparedEntrypoints.push(slot);
调用 _addModuleChian
将模块加入编译链条,会传入一个 dependency
对象,存有 entry
的 name
和 request
等信息。request
中,存储的是文件的路径信息:app
addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); // ... this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { // ... } ); }
在 _addModuleChain
的回调中会调用到 xxxModuleFactory
的 create
方法。以 normalModuleFactory
的 create
方法为例,先是触发 beforeResolve
事件,而后触发 normalModuleFactory
的 factory
事件,并调用返回的 factory
方法:async
create(data, callback) { // ... this.hooks.beforeResolve.callAsync({ ... }, (err, result) => { // ... const factory = this.hooks.factory.call(null); // ... factory(result, (err, module) => { // ... }); } ); }
在 factory
方法中,会触发 resolver
事件,并返回一个函数,经过调用此函数执行文件路径及相关 loader
路径的解析,完成后触发 afterResolve
事件,并在其回调中生成一个 module
实例,并将 resolve
的结果存入其中:编辑器
// factory 事件注册 this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => { let resolver = this.hooks.resolver.call(null); // ... resolver(result, (err, data) => { // ... this.hooks.afterResolve.callAsync(data, (err, result) => { // ... let createdModule = this.hooks.createModule.call(result); if (!createdModule) { if (!result.request) { return callback(new Error("Empty dependency (no request)")); } // normalModule 实例 createdModule = new NormalModule(result); } createdModule = this.hooks.module.call(createdModule, result); return callback(null, createdModule); }); }); }); // resolve 事件注册 this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => { // ... // loader 和 normal 文件的 resolve 有所区别 const loaderResolver = this.getResolver("loader"); const normalResolver = this.getResolver("normal", data.resolveOptions); // resolve loader & normal path });
回到 normalModuleFactory.create
的 callback
中,会依次执行 addModule
---> addReason
---> buildModule
---> afterBuild
。函数
module
实例添加到全局 Compilation.modules
数组中和 _modules
对象中;module
添加 reason
,便是哪一个 module
依赖了该 module
;module
;module
;调用 buildModule
,最终会走到相应 module
实例的 build
方法中,前面咱们的 module
是 NormalModule
的实例,因此咱们来看看 NormalModule
的 build
方法:ui
// NormalModule 的 build 方法 build(options, compilation, resolver, fs, callback) { // ... return this.doBuild(options, compilation, resolver, fs, err => { // doBuild 回调 // ... try { // 进行 AST 的转换 const result = this.parser.parse( // 若是在 runLoaders 的时候已经解析成 AST 则使用 _ast,不然传入 JS 源代码 this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) handleParseError(err); else handleParseResult(result); } ); if (result !== undefined) handleParseResult(result); } catch (e) { handleParseError(e); } }); } // doBuild 方法 doBuild(options, compilation, resolver, fs, callback) { // ... runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // ... // createSource 将 loaders 处理的结果转换为字符串 this._source = this.createSource( this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); this._sourceSize = null; // 若是已经转换为 AST,则存在 _ast 中 this._ast = typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined ? extraInfo.webpackAST : null; return callback(); } ); }
在 duBuild
方法中,会进行 loaders
的转换。在 this.parser.parse
方法中,会将转换后的源码进行 AST
转换,并在 import/export
等地方添加相应的 Dependency
,也能够理解为某种占位符,在后续的解析中,会根据相应的模板等进行替换生成最终文件。buildModule
结束后,执行其回调,并调用 afterBuild
方法,在 afterBuild
方法中,调用了 processModuleDependencies
方法处理依赖:
processModuleDependencies(module, callback) { const dependencies = new Map(); // 省略对 dependencies 作的一些处理 const sortedDependencies = []; // 将依赖解析成下面的结构 for (const pair1 of dependencies) { for (const pair2 of pair1[1]) { sortedDependencies.push({ factory: pair1[0], dependencies: pair2[1] }); } } // 添加模块依赖的解析 this.addModuleDependencies( module, sortedDependencies, this.bail, null, true, callback ); } // addModuleDependencies 遍历依赖,并从新进行 module 的编译 addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) { const start = this.profile && Date.now(); const currentProfile = this.profile && {}; asyncLib.forEach( dependencies, (item, callback) => { const dependencies = item.dependencies; // ... semaphore.acquire(() => { const factory = item.factory; // create factory.create( { // ... }, (err, dependentModule) => { // ... const iterationDependencies = depend => { for (let index = 0; index < depend.length; index++) { // ... // addReason dependentModule.addReason(module, dep); } }; // addModule const addModuleResult = this.addModule( dependentModule, cacheGroup ); // ... if (addModuleResult.build) { // buildModule this.buildModule( dependentModule, isOptional(), module, dependencies, err => { // ... // afterBuild afterBuild(); } ); } // ... } ); }); }, err => { // ... } ); }
能够看到,在 addModuleDependencies
方法中,对前一个 module
的依赖进行遍历,又从新执行 factory.create
---> addModule
---> buildModule
---> afterBuild
,进行编译。当全部的依赖都编译完,便完成了 module
的编译过程。回到 make
事件的回调中,此时全部须要编译的 module
都已经通过上面的步骤处理完毕,接下来会调用 compilation.seal
进行 chunk
图的生成工做:
this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); // seal 阶段,组合 chunk,生成 chunkGroup compilation.seal(err => { if (err) return callback(err); // 在其回调中触发 afterCompile 事件,编译过程结束 this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); });
未完待续...