Webpack系列-第一篇基礎雜記
Webpack系列-第二篇插件機制雜記
Webpack系列-第三篇流程雜記node
本文章我的理解, 只是爲了理清webpack流程, 沒有關注內部過多細節, 若有錯誤, 請輕噴~webpack
1.使用如下命令運行項目,./scripts/build.js
是你想要開始調試的地方git
node --inspect-brk ./scripts/build.js --inline --progress
2.打開chrome://inspect/#devices
便可調試github
入口處在bulid.js
,能夠看到其中的代碼是先實例化webpack,而後調用compiler的run方法
。web
function build(previousFileSizes) { let compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { ... }); }
webpack在node_moduls下面的\webpack\lib\webpack.js
(在此前面有入口參數合併),找到該文件能夠看到相關的代碼以下算法
const webpack = (options, callback) => { ...... let compiler; // 處理多個入口 if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === "object") { // webpack的默認參數 options = new WebpackOptionsDefaulter().process(options); console.log(options) // 見下圖 // 實例化compiler compiler = new Compiler(options.context); compiler.options = options; // 對webpack的運行環境處理 new NodeEnvironmentPlugin().apply(compiler); // 根據上篇的tabpable可知 這裏是爲了註冊插件 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 觸發兩個事件點 environment/afterEnviroment compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); // 設置compiler的屬性並調用默認配置的插件,同時觸發事件點entry-option compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { ...... compiler.run(callback); } return compiler; };
能夠看出options保存的就是本次webpack的一些配置參數,而其中的plugins屬性則是webpack中最重要的插件
。chrome
process(options, compiler) { let ExternalsPlugin; compiler.outputPath = options.output.path; compiler.recordsInputPath = options.recordsInputPath || options.recordsPath; compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath; compiler.name = options.name; compiler.dependencies = options.dependencies; if (typeof options.target === "string") { let JsonpTemplatePlugin; let FetchCompileWasmTemplatePlugin; let ReadFileCompileWasmTemplatePlugin; let NodeSourcePlugin; let NodeTargetPlugin; let NodeTemplatePlugin; switch (options.target) { case "web": JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin"); FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin"); NodeSourcePlugin = require("./node/NodeSourcePlugin"); new JsonpTemplatePlugin().apply(compiler); new FetchCompileWasmTemplatePlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new FunctionModulePlugin().apply(compiler); new NodeSourcePlugin(options.node).apply(compiler); new LoaderTargetPlugin(options.target).apply(compiler); break; case "webworker":...... ...... } } new JavascriptModulesPlugin().apply(compiler); new JsonModulesPlugin().apply(compiler); new WebAssemblyModulesPlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new EntryOptionPlugin().apply(compiler); // 觸發事件點entry-options並傳入參數 context和entry compiler.hooks.entryOption.call(options.context, options.entry); new CompatibilityPlugin().apply(compiler); ...... new ImportPlugin(options.module).apply(compiler); new SystemPlugin(options.module).apply(compiler); }
調用run時,會先在內部觸發beforeRun事件點
,而後再在讀取recodes
(關於records能夠參考該文檔)以前觸發run事件點,這兩個事件都是異步的形式,注意run方法是實際上整個webpack打包流程的入口
。能夠看到,最後調用的是compile
方法,同時傳入的是onCompiled函數數組
run(callback) { if (this.running) return callback(new ConcurrentCompilationError()); const finalCallback = (err, stats) => { ...... }; this.running = true; const onCompiled = (err, compilation) => { .... }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); }
compile方法主要上觸發beforeCompile、compile、make等事件點
,並實例化compilation
,這裏咱們能夠看到傳給compile的newCompilationParams
參數, 這個參數在後面相對流程中也是比較重要,能夠在這裏先看一下架構
compile(callback) { const params = this.newCompilationParams(); // 觸發事件點beforeCompile,並傳入參數CompilationParams this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); // 觸發事件點compile,並傳入參數CompilationParams this.hooks.compile.call(params); // 實例化compilation const compilation = this.newCompilation(params); // 觸發事件點make this.hooks.make.callAsync(compilation, err => { .... }); }); }
newCompilationParams返回的參數分別是兩個工廠函數和一個Set集合app
newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; }
從上面的compile方法看, compilation是經過newCompilation方法調用生成的,而後觸發事件點thisCompilation和compilation
,能夠看出compilation在這兩個事件點中最先當成參數傳入
,若是你在編寫插件的時候須要儘快使用該對象,則應該在該兩個事件中進行。
createCompilation() { return new Compilation(this); } newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; // 觸發事件點thisCompilation和compilation, 同時傳入參數compilation和params this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; }
下面是打印出來的compilation屬性
關於這裏爲何要有thisCompilation這個事件點和子編譯器(childCompiler)
,能夠參考該文章
總結起來就是:
子編譯器擁有完整的模塊解析和chunk生成階段,可是少了某些事件點,如"make", "compile", "emit", "after-emit", "invalid", "done", "this-compilation"。 也就是說咱們能夠利用子編譯器來獨立(於父編譯器)跑完一個核心構建流程,額外生成一些須要的模塊或者chunk。
從上面的compile方法知道, 實例化Compilation後就會觸發make事件點
了。
觸發了make時, 由於webpack在前面實例化SingleEntryPlugin或者MultleEntryPlugin
,SingleEntryPlugin則在其apply方法中註冊了一個make事件,
apply(compiler) { compiler.hooks.compilation.tap( "SingleEntryPlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory // 工廠函數,存在compilation的dependencyFactories集合 ); } ); compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); // 進入到addEntry compilation.addEntry(context, dep, name, callback); } ); }
事實上addEntry調用的是Comilation._addModuleChain
,acquire函數比較簡單,主要是處理module時若是任務太多,就將moduleFactory.create存入隊列等待
_addModuleChain(context, dependency, onModule, callback) { ...... // 取出對應的Factory const Dep = /** @type {DepConstructor} */ (dependency.constructor); const moduleFactory = this.dependencyFactories.get(Dep); ...... this.semaphore.acquire(() => { moduleFactory.create( { contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { ...... } ); }); }
moduleFactory.create則是收集一系列信息而後建立一個module傳入回調
回調函數主要上執行buildModule
方法
this.buildModule(module, false, null, null, err => { ...... afterBuild(); });
buildModule(module, optional, origin, dependencies, thisCallback) { // 處理回調函數 let callbackList = this._buildingModules.get(module); if (callbackList) { callbackList.push(thisCallback); return; } this._buildingModules.set(module, (callbackList = [thisCallback])); const callback = err => { this._buildingModules.delete(module); for (const cb of callbackList) { cb(err); } }; // 觸發buildModule事件點 this.hooks.buildModule.call(module); module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ...... } ); }
build方法中調用的是doBuild,doBuild又經過runLoaders獲取loader相關的信息並轉換成webpack須要的js文件,最後經過doBuild的回調函數調用parse方法,建立依賴Dependency並放入依賴數組
return this.doBuild(options, compilation, resolver, fs, err => { // 在createLoaderContext函數中觸發事件normal-module-loader const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); ..... const handleParseResult = result => { this._lastSuccessfulBuildMeta = this.buildMeta; this._initBuildHash(compilation); return callback(); }; try { // 調用parser.parse 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); } } ); if (result !== undefined) { // parse is sync handleParseResult(result); } } catch (e) { handleParseError(e); } });
在ast轉換過程當中也很容易獲得了須要依賴的哪些其餘模塊
。
最後執行了module.build的回調函數,觸發了事件點succeedModule
,並回到Compilation.buildModule函數的回調函數
module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ...... 觸發了事件點succeedModule this.hooks.succeedModule.call(module); return callback(); } ); this.buildModule(module, false, null, null, err => { ...... // 執行afterBuild afterBuild(); });
對於當前模塊,或許存在着多個依賴模塊。當前模塊會開闢一個依賴模塊的數組,在遍歷 AST 時,將 require() 中的模塊經過 addDependency() 添加到數組中。當前模塊構建完成後,webpack 調用 processModuleDependencies 開始遞歸處理依賴的 module
,接着就會重複以前的構建步驟。
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) { // 根據依賴數組(dependencies)建立依賴模塊對象 var factories = []; for (var i = 0; i < dependencies.length; i++) { var factory = _this.dependencyFactories.get(dependencies[i][0].constructor); factories[i] = [factory, dependencies[i]]; } ... // 與當前模塊構建步驟相同 }
最後, 全部的模塊都會被放入到Compilation的modules
裏面, 以下:
總結一下:
module 是 webpack 構建的核心實體,也是全部 module 的 父類,它有幾種不一樣子類:NormalModule , MultiModule , ContextModule , DelegatedModule 等,一個依賴對象(Dependency,還未被解析成模塊實例的依賴對象。好比咱們運行 webpack 時傳入的入口模塊,或者一個模塊依賴的其餘模塊,都會先生成一個 Dependency 對象。)通過對應的工廠對象(Factory)建立以後,就可以生成對應的模塊實例(Module)。
構建module後, 就會調用Compilation.seal, 該函數主要是觸發了事件點seal, 構建chunk
, 在全部 chunks 生成以後,webpack 會對 chunks 和 modules 進行一些優化相關的操做,好比分配id、排序等,而且觸發一系列相關的事件點
seal(callback) { // 觸發事件點seal this.hooks.seal.call(); // 優化 ...... this.hooks.afterOptimizeDependencies.call(this.modules); this.hooks.beforeChunks.call(); // 生成chunk for (const preparedEntrypoint of this._preparedEntrypoints) { const module = preparedEntrypoint.module; const name = preparedEntrypoint.name; // 整理每一個Module和chunk,每一個chunk對應一個輸出文件。 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.assignDepth(module); } this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice()); this.sortModules(this.modules); this.hooks.afterChunks.call(this.chunks); this.hooks.optimize.call(); ...... this.hooks.afterOptimizeModules.call(this.modules); ...... this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { ...... this.hooks.beforeChunkAssets.call(); this.createChunkAssets(); // 生成對應的Assets this.hooks.additionalAssets.callAsync(...) }); }
每一個 chunk 的生成就是找到須要包含的 modules。這裏大體描述一下 chunk 的生成算法:
1.webpack 先將 entry 中對應的 module 都生成一個新的 chunk
2.遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
3.若是一個依賴 module 是動態引入的模塊,那麼就會根據這個 module 建立一個新的 chunk,繼續遍歷依賴
4.重複上面的過程,直至獲得全部的 chunks
chunk屬性圖
在觸發這兩個事件點的中間時, 會調用Compilation.createCHunkAssets來建立assets
,
createChunkAssets() { ...... // 遍歷chunk for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; chunk.files = []; let source; let file; let filenameTemplate; try { // 調用何種Template const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; const manifest = template.getRenderManifest({ chunk, hash: this.hash, fullHash: this.fullHash, outputOptions, moduleTemplates: this.moduleTemplates, dependencyTemplates: this.dependencyTemplates }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }] for (const fileManifest of manifest) { ..... } ..... // 寫入assets對象 this.assets[file] = source; chunk.files.push(file); this.hooks.chunkAsset.call(chunk, file); alreadyWrittenFiles.set(file, { hash: usedHash, source, chunk }); } } catch (err) { ...... } } }
createChunkAssets會生成文件名和對應的文件內容,並放入Compilation.assets對象
, 這裏有四個Template 的子類,分別是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js , HotUpdateChunkTemplate.js
模塊封裝(引用自http://taobaofed.org/blog/201...)
模塊在封裝的時候和它在構建時同樣,都是調用各模塊類中的方法。封裝經過調用 module.source() 來進行各操做,好比說 require() 的替換。
MainTemplate.prototype.requireFn = "__webpack_require__"; MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) { var buf = []; // 每個module都有一個moduleId,在最後會替換。 buf.push("function " + this.requireFn + "(moduleId) {"); buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash))); buf.push("}"); buf.push(""); ... // 其他封裝操做 };
最後看看Compilation.assets對象
最後一步,webpack 調用 Compiler 中的 emitAssets() ,按照 output 中的配置項將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則須要在 emit 觸發後對自定義插件進行擴展。
webpack的內部核心仍是在於compilationcompilermodulechunk等對象或者實例。寫下這篇文章也有助於本身理清思路,學海無涯~~~
玩轉webpack(一):webpack的基本架構和構建流程
玩轉webpack(二):webpack的核心對象
細說 webpack 之流程篇