webpack | 2020-06-29 19:12:29 637次 0次
Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展自 Tapable 类,以便注册和调用插件。大多数面向用户的插件,会先在 Compiler 上注册。
整体结构如下:
class Compiler extends Tapable { ... getInfrastructureLogger(name) { } //监听初始化 watch(watchOptions, handler) { } // 运行编译, 整个编译过程启动的入口 run(callback) { } // 作为子编译进程运行 runAsChild(callback) { } // 净化输入 purgeInputFileSystem() { } // 发布资源 emitAssets(compilation, callback) { } // 发布记录 ecordsOutputPath: path.join(__dirname, 'newRecords.json') emitRecords(callback) { } /** * 一些数据片段,用于储存多次构建过程中的module的标识 * 可以使用此文件来跟踪在每次构建之间的模块变化 * recordsInputPath: path.join(__dirname, 'records.json') */ readRecords(callback) { } // 创建子编译器 createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins ) { ... } // 是否子汇编 isChild() { return !!this.parentCompilation; } // 创建compilation createCompilation() { } // 一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息, //代表了一次资源的构建。 newCompilation(params) { } // 创建普通模块的工厂 createNormalModuleFactory() { } // 创建上下文模块的工厂 createContextModuleFactory() { } // 获取一个新的汇编参数对象 newCompilationParams() { } // 编译 compile(callback) { } }
一、 compiler.run
首先 this.hook 上注册了很多事件,这里可以参考下官方的 compiler api,里面有简要说明各个条件下触发机制。
//触发执行前的钩子 this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); //运行结束回调处理 //接着执行 run 钩子 this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); //读取之前的records this.readRecords(err => { if (err) return finalCallback(err); //执行编译 this.compile(onCompiled); }); }); });
这里先调用了 readRecords 方法,它是一些数据片段,用于储存多次构建过程中的 module 的标识,可以使用此文件来跟踪在每次构建之间的模块变化:
readRecords(callback) { // 配置的路径 recordsInputPath: path.join(__dirname, 'records.json') if (!this.recordsInputPath) { this.records = {}; return callback(); } //inputFileSystem是一个封装过的文件系统,扩展了fs的功能 this.inputFileSystem.stat(this.recordsInputPath, err => { //recordsInputPath的文件是否存在 存在则读取并解析,存到this.records中 if (err) return callback(); this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => { if (err) return callback(err); try { this.records = parseJson(content.toString("utf-8")); } catch (e) { ... } return callback(); }); }); }
接着执行 compile 方法,开始执行编译,这里最终调用了 run 中的 onCompiled 回调,主要用于输出构建资源,等下面再说:
compile(callback) { // 创建了compilation的初始参数 const params = this.newCompilationParams(); // 编译前 this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); //开始编译钩子 this.hooks.compile.call(params); // 每次编译都会创建一个新的compilation const compilation = this.newCompilation(params); // WebpackOptionsApply --> EntryOptionPlugin --> itemToPlugin --> // SingleEntryPlugin --> compiler.hooks.make.tapAsync这里被触发 --> compilation.addEntry this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); //模块完成构建 compilation.finish(err => { if (err) return callback(err); //编译(compilation)停止接收新模块时触发 compilation.seal(err => { if (err) return callback(err); //seal完成意味着编译完成,钩子触发 this.hooks.afterCompile.callAsync(compilation, err => { ... return callback(null, compilation); }); }); }); }); }); }
二、newCompilationParams
这里面发生两件事件,第一个创建一个普通模块的工厂函数 createNormalModuleFactory,再通过 NormalModuleFactory 实例生成。这里有个 ResolverFactory,使用 Hook 的映射类来创建,可能因为避免多次创建, HookMap 中是 Map 对象存储。
createNormalModuleFactory() { const normalModuleFactory = new NormalModuleFactory( this.options.context, //当前路径 this.resolverFactory, this.options.module || {} ); this.hooks.normalModuleFactory.call(normalModuleFactory); return normalModuleFactory; }
NormalModuleFactory 这个也是继承自 Tapable,这里面实例化了 RuleSet。
class NormalModuleFactory extends Tapable { ... //实例化 loader 插件,预置的和传入的配置合并 this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules)); ... //先注册这个方法 自身的 create 中会触发 this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => { //优先触发下面 resolver 回调 //开始构建模块 createdModule = new NormalModule(result); }); this.hooks.resolver.tap('NormalModuleFactory', () => (data, callback) => { //这里面触发时解析各种loader,然后处理最后生成正确顺序的loader组合 }); }
ContextModuleFactory 仍然是继承自 Tapable,这里面貌似都是兼容旧版插件写法,做的事情和上面这个方法是一样的。
// 创建上下文模块的工厂 createContextModuleFactory() { const contextModuleFactory = new ContextModuleFactory(this.resolverFactory); this.hooks.contextModuleFactory.call(contextModuleFactory); return contextModuleFactory; } class ContextModuleFactory extends Tapable { ... }
三、Compilation
compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,代表一次资源的构建。这里面会发现有 thisCompilation 和 compilation,因为在 createChildCompiler 子编译方法中官方注释说会复制所有的钩子,thisCompilation 其实是不会被复制的。
newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; this.hooks.thisCompilation.call(compilation, params); ... return compilation; }
首先 this.hook 上注册了很多事件,这里可以参考下官方的 compilation api
class Compilation extends Tapable { ... }
接着第一部分中的 this.hooks.make.callAsync 触发,这里先看下它的注册流程以及其他流程:
① new WebpackOptionsApply().process -- (webpack.js)
② new EntryOptionPlugin().apply(compiler); -- (WebpackOptionsApply.js)
compiler.hooks.entryOption.call(options.context, options.entry);
③ compiler.hooks.entryOption.tap -- (EntryOptionPlugins.js)
new SingleEntryPlugin().apply
④ compiler.hooks.make.tapAsync -- (SingleEntryPlugin.js)
compilation.addEntry(context, dep, name, callback);
这里最终调用了 addEntry:
Compilation.js addEntry(context, entry, name, callback) { //钩子执行 this.hooks.addEntry.call(entry, name); ... this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { ... } ) }
_addModuleChain 中继续通过 this.semaphore.acquire 进行一个并发量控制,默认值为 100,这里面进行 create 开始构建模块:
Compilation.js _addModuleChain(context, dependency, onModule, callback) { ... //SingleEntryDependency const Dep = (dependency.constructor); // SingleEntryPlugin中注册了这个normalModuleFactory。dependencyFactories 会在各个插件中存入东西 const moduleFactory = this.dependencyFactories.get(Dep); ... this.semaphore.acquire(() => { //开始创建 module moduleFactory.create(...) }); }
四、resolve
NormalModuleFactory 中的 create 中触发 this.hooks.factory.tap,然后这里面在 this.hooks.resolver 事件回调里面执行创建模块 NormalModule:
NormalModuleFactory.js this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => { let resolver = this.hooks.resolver.call(null); ... //该函数为解析构建所有 module 所需要的 loaders 组合及这个 module 的相关构建信息 resolver(result, (err, data) => { ... this.hooks.afterResolve.callAsync(data, (err, result) => { ... //创建模块的钩子被触发(源码中没发现哪里注册的) let createdModule = this.hooks.createModule.call(result); //被执行过 if (!createdModule) { ... //构建模块 createdModule = new NormalModule(result); } //(源码中没发现哪里注册的) createdModule = this.hooks.module.call(createdModule, result); ... }); }); });
resolver 时会触发第二小节中提到的 ResolverFactory 方法,里面除了上述说的之外还会使用 enhanced-resolve,能够处理各种文件路径和 loader 处理,并且还提供缓存功能提升效率。
resolver 内部再进行解析各种类型 loader,生成一个 loader 组合,执行在下一小节中会提到。之前写过一篇 loader 深度分析的文章,从使用到插件使用、编写、运行原理可以参考,源码中就不再细入分析了。loader深度分析
上一小节中 NormalModuleFactory 中的 create 中触发的上述系列事件成功后的回调中,拿到 module 实例之后,进入 moduleFactory.create 的回调。 在这之前 dependencyCache 会添加缓存模块。
Compilation.js //拿到module实例之后 进入这个回调中 在这之前dependencyCache会添加缓存模块 const addModuleResult = this.addModule(module); module = addModuleResult.module; //this.entries.push(module) 这里是主入口文件 onModule(module); dependency.module = module; module.addReason(null, dependency); ... if (addModuleResult.build) { this.buildModule(module, false, null, null, err => { ... afterBuild(); }); } else { ... } ... buildModule(module, optional, origin, dependencies, thisCallback) { ... this.hooks.buildModule.call(module); module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ... } ); }
接下来开始执行 buildModule 方法,并触发 buildModule 钩子函数,然后执行 module.build(就是 NormalModule 类中的 build 方法)。
NormalModule.js build(options, compilation, resolver, fs, callback) { this.buildTimestamp = Date.now(); this.built = true; this._source = null; this._sourceSize = null; this._ast = null; this._buildHash = ""; this.error = null; this.errors.length = 0; this.warnings.length = 0; this.buildMeta = {}; this.buildInfo = { cacheable: false, fileDependencies: new Set(), contextDependencies: new Set(), assets: undefined, assetsInfo: undefined }; return this.doBuild(options, compilation, resolver, fs, err => { ... }); }
在 build 中初始化一些参数,调用自身 doBuild 方法,上面提到 loader 的解析操作,到这里开始执行 runLoaders。
五、回调执行
runLoader 成功后执行 createSource 方法生成 _source,再返回 doBuild 的回调中,执行 this.parser.parse,这个 parser 来自:
NormalModuleFactory.js process.nextTick(() => { const type = settings.type; const resolveOptions = settings.resolve; callback(null, { ... parser: this.getParser(type, settings.parser), generator: this.getGenerator(type, settings.generator), resolveOptions }); });
NormalModule.js const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) { handleParseError(err); } else { handleParseResult(result); } } );
执行了 getParser(将当前模块解析为 ast 语法树)和 getGenerator(用于模版生成时提供方法)。
NormalModuleFactory.js getParser(type, parserOptions) { ... return (this.parserCache[ident] = this.createParser(type, parserOptions)); } createParser(type, parserOptions = {}) { const parser = this.hooks.createParser.for(type).call(parserOptions); if (!parser) { throw new Error(`No parser registered for ${type}`); } this.hooks.parser.for(type).call(parser, parserOptions); return parser; } getGenerator(type, generatorOptions) { let ident = type; ... return (this.generatorCache[ident] = this.createGenerator( type, generatorOptions )); } createGenerator(type, generatorOptions = {}) { const generator = this.hooks.createGenerator .for(type) .call(generatorOptions); if (!generator) { throw new Error(`No generator registered for ${type}`); } this.hooks.generator.for(type).call(generator, generatorOptions); return generator; }
createParser 触发一个事件,首先 WebpackOptionsApply 中实例化一个 JavascriptModulesPlugin 类,这个类中注册了事件 compiler.hooks.compilation.tap,在 Compiler 的 newCompilation 中触发,然后它的回调中又注册了:
JavascriptModulesPlugin.js normalModuleFactory.hooks.createParser .for("javascript/auto") .tap("JavascriptModulesPlugin", options => { return new Parser(options, "auto"); });
这里去实例化一个 Parser 对象,所以上面的 createParser 才会直接取到这个实例(这里的山路十八弯~)。
Parse.js class Parser extends Tapable { ... parse(source, initialState) { let ast; let comments; if (typeof source === "object" && source !== null) { ast = source; comments = source.comments; } else { comments = []; //自身的一个静态方法 生成ast ast = Parser.parse(source, { sourceType: this.sourceType, onComment: comments }); } ... //遍历 ast 收集依赖 if (this.hooks.program.call(ast, comments) === undefined) { //use strict检测 this.detectMode(ast.body); this.prewalkStatements(ast.body); this.blockPrewalkStatements(ast.body); this.walkStatements(ast.body); } ... return state; } ... static parse(code, options) { ... let ast; let error; let threw = false; try { // acornParser --> acorn.Parser | Babel ast = acornParser.parse(code, parserOptions); } catch (e) { error = e; threw = true; } ... return ast; } }
this.hooks.program.call 触发,事件定义在 HarmonyDetectionParserPlugin 和 UseStrictPlugin,坦白讲这块的逻辑没有理清楚,引用:上述执行结束后,会根据 import/export 的不同情况即模块间的相互依赖关系,在对应的 module.dependencies 上增加相应的依赖。
六、hash
parse 解析完成的回调中执行 handleParseResult,进行生成 hash:
NormalModule.js const handleParseResult = result => { this._lastSuccessfulBuildMeta = this.buildMeta; this._initBuildHash(compilation); return callback(); }; _initBuildHash(compilation) { //依赖 require("crypto") 库实现 const hash = createHash(compilation.outputOptions.hashFunction); if (this._source) { hash.update("source"); this._source.updateHash(hash); } //更新 hash 内容 hash.update("meta"); hash.update(JSON.stringify(this.buildMeta)); //获取 hash this._buildHash = /** @type {string} */ (hash.digest("hex")); }
七、afterBuild
再回到 this.buildModule 回调中执行了 afterBuild,某个模块被解析创建后,在 addModule() 中设置 dependencies 为 false 避免该模块重复解析创建依赖。
const afterBuild = () => { //首次解析 if (recursive && addModuleResult.dependencies) { this.processModuleDependencies(dependentModule, callback); } else { return callback(); } };
processModuleDependencies 中判断各种模块递归解析
processModuleDependencies(module, callback) { const dependencies = new Map(); const addDependency = dep => { ... }; const addDependenciesBlock = block => { if (block.dependencies) { iterationOfArrayCallback(block.dependencies, addDependency); } //懒加载 内部 import 木块 if (block.blocks) { iterationOfArrayCallback(block.blocks, addDependenciesBlock); } //内部变量 __resourceQuery if (block.variables) { iterationBlockVariable(block.variables, addDependency); } }; ... this.addModuleDependencies( module, sortedDependencies, this.bail, null, true, callback ); }
addModuleDependencies 和 _addModuleChain 很相似,里面调用了 factory.create,重新执行流程,最终生成一个模块对象:
初始化module -> add module -> module build -> afterBuild -> processModuleDependencies
0人赞