系列文章
Webpack系列-第一篇基礎雜記
Webpack系列-第二篇外掛機制雜記
Webpack系列-第三篇流程雜記
前言
本文章個人理解, 只是為了理清webpack流程, 沒有關注內部過多細節, 如有錯誤, 請輕噴~
除錯
1.使用以下命令執行專案,./scripts/build.js
是你想要開始除錯的地方
node --inspect-brk ./scripts/build.js --inline --progress
複製程式碼
2.開啟chrome://inspect/#devices
即可除錯
流程圖
入口
入口處在bulid.js
,可以看到其中的程式碼是先例項化webpack,然後呼叫compiler的run方法
。
function build(previousFileSizes) {
let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
...
});
}
複製程式碼
entry-option(compiler)
webpack.js
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中最重要的外掛
。
new WebpackOptionsApply().process
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(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(compiler)
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集合
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
複製程式碼
compilation(compiler)
從上面的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。
make(compiler)
從上面的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(compilation)
回撥函式主要上執行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轉換過程中也很容易得到了需要依賴的哪些其他模組
。
succeedModule(compilation)
最後執行了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)。
seal(compilation)
構建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屬性圖
beforeChunkAssets && additionalChunkAssets(Compilation)
在觸發這兩個事件點的中間時, 會呼叫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
- MainTemplate.js: 對應了在 entry 配置的入口 chunk 的渲染模板
- ChunkTemplate: 動態引入的非入口 chunk 的渲染模板
- ModuleTemplate.js: chunk 中的 module 的渲染模板
- HotUpdateChunkTemplate.js: 對熱替換模組的一個處理。
模組封裝(引用自http://taobaofed.org/blog/2016/09/09/webpack-flow/)
模組在封裝的時候和它在構建時一樣,都是呼叫各模組類中的方法。封裝通過呼叫 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物件
done(Compiler)
最後一步,webpack 呼叫 Compiler 中的 emitAssets() ,按照 output 中的配置項將檔案輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 emit 觸發後對自定義外掛進行擴充套件。
總結
webpack的內部核心還是在於compilation\compiler\module\chunk等物件或者例項。寫下這篇文章也有助於自己理清思路,學海無涯~~~
引用
玩轉webpack(一):webpack的基本架構和構建流程
玩轉webpack(二):webpack的核心物件
細說 webpack 之流程篇