webpack是一個強大的打包工具,擁有靈活、豐富的插件機制,網上關於如何使用webpack及webpack原理分析的技術文檔層出不窮。最近本身也在學習webpack的過程當中,記錄並分享一下,但願對你有點幫助。 本文主要探討,webpack的一次構建流程中,主要乾了哪些事兒。 (我們只研究研究構建的總體流程哈,細節不看🙈)javascript
已知,Webpack 源碼是一個插件的架構,不少功能都是經過諸多的內置插件實現的。Webpack爲此專門本身寫一個插件系統,叫 Tapable
主要提供了註冊和調用插件的功能。 一塊兒研究以前,但願你對 tapable
有所瞭解~java
閱讀源碼最直接的方式是在 chrome 中經過斷點在關鍵代碼上進行調試,咱們能夠用 node-inspector
進行這次debugger。node
"scripts": {
"build": "webpack --config webpack.prod.js",
"debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress",
},
複製代碼
執行npm run build && npm run debugwebpack
// 入口文件
import { helloWorld } from './helloworld.js';
document.write(helloWorld());
// helloworld.js
export function helloWorld() {
return 'bts';
}
// webpack.prod.js
module.exports = {
entry: {
index: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader',
},
]
},
};
複製代碼
先經過一張大圖總體梳理一下webpack的主體流程,再細節一點的稍後再介紹 web
流程圖中展現了些核心任務點,簡要說明下這些任務點作了事兒:yargs
解析 config
與 shell
中的配置項webpack
初始化過程,首先會根據第一步的 options
生成 compiler
對象,而後初始化 webpack
的內置插件及 options
配置run
表明編譯的開始,會構建 compilation
對象,用於存儲這一次編譯過程的全部數據make
執行真正的編譯構建過程,從入口文件開始,構建模塊,直到全部模塊建立結束seal
生成 chunks
,對 chunks
進行一系列的優化操做,並生成要輸出的代碼seal
結束後,Compilation
實例的全部工做到此也所有結束,意味着一次構建過程已經結束emit
被觸發以後,webpack
會遍歷 compilation.assets
, 生成全部文件,而後觸發任務點 done
,結束構建流程在學習其餘技術博客時都有相似上面的主體流程的分析,道理都懂,但不打斷點看的細節點,說服不了本身。如下是一些任務點的詳細動做,建議有興趣的小夥伴多打幾個debuggerchrome
強烈建議在每一個重要鉤子的回調函數中打debugger,否則可能跳着跳着就走遠了shell
webpack啓動入口,webpack-cli/bin/cli.jsnpm
const webpack = require("webpack");
// 使用yargs來解析命令行參數併合並配置文件中的參數(options),
// 而後調用lib/webpack.js實例化compile 並返回
let compiler;
try {
compiler = webpack(options);
} catch (err) {}
複製代碼
// lib/webpack.js
const webpack = (options, callback) => {
// 首先會檢查配置參數是否合法
// 建立Compiler
let compiler;
compiler = new Compiler(options.context);
compiler.options = new WebpackOptionsApply().process(options, compiler);
...
if (options.watch === true || ..) {
...
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
複製代碼
建立了 compiler
對象,compiler
能夠理解爲 webpack
編譯的調度中心,是一個編譯器實例,在 compiler
對象記錄了完整的 webpack
環境信息,在 webpack
的每一個進程中,compiler
只會生成一次。數組
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context", "entry"])
// 定義了不少不一樣類型的鉤子
};
// ...
}
}
複製代碼
能夠看到 Compiler
對象繼承自 Tapable
,初始化時定義了不少鉤子。babel
WebpackOptionsApply
類中會根據配置註冊對應的插件,其中有個比較重要的插件
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
複製代碼
EntryOptionPlugin插件中訂閱了compiler的entryOption鉤子,並依賴SingleEntryPlugin插件
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
return new SingleEntryPlugin(context, item, name);
});
}
};
複製代碼
SingleEntryPlugin
插件中訂閱了 compiler
的 make
鉤子,並在回調中等待執行 addEntry
,但此時 make
鉤子還並無被觸發哦
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
const normalModuleFactory = params.normalModuleFactory;
// 這裏記錄了 SingleEntryDependency 對應的工廠對象是 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);
}
);
}
複製代碼
初始化 compiler
後,根據 options
的 watch
判斷是否啓動了 watch
,若是啓動 watch
了就調用 compiler.watch
來監控構建文件,不然啓動 compiler.run
來構建文件,compiler.run
就是咱們這次編譯的入口方法,表明着要開始編譯了。
調用 compiler.run
方法來啓動構建
run(callback) {
const onCompiled = (err, compilation) => {
this.hooks.done.callAsync(stats, err => {
return finalCallback(null, stats);
});
};
// 執行訂閱了compiler.beforeRun鉤子插件的回調
this.hooks.beforeRun.callAsync(this, err => {
// 執行訂閱了compiler.run鉤子插件的回調
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled);
});
});
}
複製代碼
compiler.compile
開始真正執行咱們的構建流程,核心代碼以下
compile(callback) {
// 實例化核心工廠對象
const params = this.newCompilationParams();
// 執行訂閱了compiler.beforeCompile鉤子插件的回調
this.hooks.beforeCompile.callAsync(params, err => {
// 執行訂閱了compiler.compile鉤子插件的回調
this.hooks.compile.call(params);
// 建立這次編譯的Compilation對象
const compilation = this.newCompilation(params);
// 執行訂閱了compiler.make鉤子插件的回調
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
})
})
})
})
}
複製代碼
在compile
階段,Compiler
對象會開始實例化兩個核心的工廠對象,分別是 NormalModuleFactory
和 ContextModuleFactory
。工廠對象顧名思義就是用來建立實例的,它們後續用來建立 module
實例的,包括 NormalModule
以及 ContextModule
實例。
建立這次編譯的 Compilation
對象,核心代碼以下:
newCompilation(params) {
// 實例化Compilation對象
const compilation = new Compilation(this);
this.hooks.thisCompilation.call(compilation, params);
// 調用this.hooks.compilation通知感興趣的插件
this.hooks.compilation.call(compilation, params);
return compilation;
}
複製代碼
Compilation
對象是後續構建流程中最核心最重要的對象,它包含了一次構建過程當中全部的數據。也就是說一次構建過程對應一個 Compilation
實例。在建立 Compilation
實例時會觸發鉤子 compilaiion
和 thisCompilation
。
在Compilation對象中:
- modules 記錄了全部解析後的模塊
- chunks 記錄了全部chunk
- assets記錄了全部要生成的文件
上面這三個屬性已經包含了 Compilation
對象中大部分的信息,但目前也只是有個大體的概念,特別是 modules
中每一個模塊實例究竟是什麼東西,並不太清楚。先不糾結,畢竟此時 Compilation
對象剛剛生成。
當 Compilation
實例建立完成以後,webpack
的準備階段已經完成,下一步將開始 modules
的生成階段。
this.hooks.make.callAsync()
執行訂閱了 make
鉤子的插件的回調函數。回到上文,在初始化默認插件過程當中(WebpackOptionsApply類),SingleEntryPlugin
插件中訂閱了 compiler
的 make
鉤子,並在回調中等待執行 compilation.addEntry
方法。
compilation.addEntry
方法會觸發第一批 module
的解析,即咱們在 entry
中配置的入口文件 index.js
。在深刻 modules
的構建流程以前,咱們先對模塊實例 module
的概念有個瞭解。
Dependency
,能夠理解爲還未被解析成模塊實例的依賴對象。好比配置中的入口模塊,或者一個模塊依賴的其餘模塊,都會先生成一個Dependency
對象。每一個Dependency
都會有對應的工廠對象,好比咱們此次debuger的代碼,入口文件index.js
首先生成SingleEntryDependency
, 對應的工廠對象是NormalModuleFactory
。(前文說到SingleEntryPlugin
插件時有放代碼,有疑惑的同窗能夠往前翻翻看)
// 建立單入口依賴
const dep = SingleEntryPlugin.createDependency(entry, name);
// 正式進入構建階段
compilation.addEntry(context, dep, name, callback);
複製代碼
SingleEntryPlugin
插件訂閱的make
事件,將建立的單入口依賴傳入compilation.addEntry
方法,addEntry
主要執行_addModuleChain()
_addModuleChain(context, dependency, onModule, callback) {
...
// 根據依賴查找對應的工廠函數
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 調用工廠函數NormalModuleFactory的create來生成一個空的NormalModule對象
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
...
afterBuild();
})
})
}
複製代碼
_addModuleChain
中接收參數dependency
傳入的入口依賴,使用對應的工廠函數NormalModuleFactory.create
方法生成一個空的module
對象,回調中會把此module
存入compilation.modules
對象和dependencies.module
對象中,因爲是入口文件,也會存入compilation.entries
中。隨後執行buildModule
進入真正的構建module內容的過程。
buildModule
方法主要執行module.build()
,對應的是NormalModule.build()
// NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this.doBuild(options, compilation, resolver, fs, err => {
...
// 一下子講
}
}
複製代碼
先來看看doBuild
中作了什麼
doBuild(options, compilation, resolver, fs, callback) {
...
runLoaders(
{
resource: this.resource, // /src/index.js
loaders: this.loaders, // `babel-loader`
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
...
const source = result.result[0];
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
}
)
}
複製代碼
一句話說,doBuild
調用了相應的 loaders
,把咱們的模塊轉成標準的JS模塊。這裏,使用babel-loader
來編譯 index.js
,source
就是 babel-loader
編譯後的代碼。
// source
"debugger; import { helloWorld } from './helloworld.js';document.write(helloWorld());」
複製代碼
同時,還會生成this._source
對象,有name
和value
兩個字段,name
就是咱們的文件路徑,value
就是編譯後的JS代碼。模塊源碼最終是保存在 _source
屬性中,能夠經過 _source.source()
來獲得。回到剛剛的NormalModule
中的build
方法
build(options, compilation, resolver, fs, callback) {
...
return this.doBuild(options, compilation, resolver, fs, err => {
const result = this.parser.parse(
this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
}
);
}
}
複製代碼
通過 doBuild
以後,咱們的任何模塊都被轉成了標準的JS模塊。接下來就是調用Parser.parse
方法,將JS解析爲AST。
// Parser.js
const acorn = require("acorn");
const acornParser = acorn.Parser;
static parse(code, options) {
...
let ast = acornParser.parse(code, parserOptions);
return ast;
}
複製代碼
生成的AST結果以下:
解析成AST最大做用就是收集模塊依賴關係,webpack會遍歷AST對象,遇到不一樣類型的節點執行對應的函數。好比調試代碼中出現的import { helloWorld } from './helloworld.js'
或
const xxx = require('XXX')
的模塊引入語句,webpack會記錄下這些依賴項,並記錄在module.dependencies數組中。到這裏,入口module的解析過程就完成了,解析後的module你們有興趣能夠打印出來看下,這裏我只截圖了module.dependencies數組。
每一個 module 解析完成以後,都會觸發
Compilation
例對象的succeedModule鉤子,訂閱這個鉤子獲取到剛解析完的 module 對象。 隨後,webpack會遍歷module.dependencies數組,遞歸解析它的依賴模塊生成module,最終咱們會獲得項目所依賴的全部 modules。遍歷的邏輯在
afterBuild()
->
processModuleDependencies()
->
addModuleDependencies()
->
factory.create()
。
make
階段到此結束,接下去會觸發
compilation.seal
方法,進入下一個階段。
compilation.seal
方法主要生成chunks
,對chunks
進行一系列的優化操做,並生成要輸出的代碼。webpack
中的 chunk
,能夠理解爲配置在 entry
中的模塊,或者是動態引入的模塊。
chunk
內部的主要屬性是_modules
,用來記錄包含的全部模塊對象。因此要生成一個chunk
,就先要找到它包含的全部modules
。下面簡述一下chunk的生成過程:
entry
中對應的每一個 module
都生成一個新的 chunk
module.dependencies
,將其依賴的模塊也加入到上一步生成的chunk中下圖是咱們這次demo生成的this.chunks,_modules中有兩個模塊,分別是入口index模塊,與其依賴helloworld模塊。
在生成chunk的過程當中與過程後,webpack會對chunk和module進行一系列的優化操做,優化操做大都是由不一樣的插件去完成。可見compilation.seal
方法中,有大量的鉤子執行的代碼。
this.hooks.optimizeModulesBasic.call(this.modules);
this.hooks.optimizeModules.call(this.modules);
this.hooks.optimizeModulesAdvanced.call(this.modules);
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups);
...
複製代碼
例如,插件SplitChunksPlugin訂閱了compilation的optimizeChunksAdvanced鉤子。至此,咱們的modules和chunks都生成了,該去生成文件了。
首先須要生成最終的代碼,主要在compilation.seal
中調用了 compilation.createChunkAssets
方法。
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
const manifest = template.getRenderManifest({
...
})
...
for (const fileManifest of manifest) {
source = fileManifest.render();
}
...
this.emitAsset(file, source, assetInfo);
}
複製代碼
createChunkAssets
方法會遍歷chunks
,來渲染每個chunk生成代碼。其實,compilation
對象在實例化時,同時還會實例化三個對象,分別是MainTemplate
, ChunkTemplate
和ModuleTemplate
。這三個對象是用來渲染chunk
,獲得最終代碼模板的。它們之間的不一樣在於,MainTemplate
用來渲染入口 chunk,ChunkTemplate
用來渲染非入口 chunk,ModuleTemplate
用來渲染 chunk 中的模塊。
這裏, MainTemplate
和 ChunkTemplate
的 render
方法是用來生成不一樣的"包裝代碼"的,MainTemplate
對應的入口 chunk
須要帶有 webpack
的啓動代碼,因此會有一些函數的聲明和啓動。而包裝代碼中,每一個模塊的代碼是經過 ModuleTemplate
來渲染的,不過一樣只是生成」包裝代碼」來封裝真正的模塊代碼,而真正的模塊代碼,是經過模塊實例的 source
方法來提供。這麼說可能不是很好理解,直接看看最終生成文件中的代碼,以下:
emitAsset
將其存在
compilation.assets
中。當全部的 chunk 都渲染完成以後,assets 就是最終更要生成的文件列表。至此,
compilation
的
seal
方法結束,也表明着
compilation
實例的全部工做到此也所有結束,意味着一次構建過程已經結束,接下來只有文件生成的步驟了。
在 Compiler
開始生成文件前,鉤子 emit
會被執行,這是咱們修改最終文件的最後一個機會,生成的在此以後,咱們的文件就不能改動了。
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
複製代碼
webpack 會直接遍歷 compilation.assets 生成全部文件,而後觸發鉤子done,結束構建流程。
咱們將webpack核心的構建流程都過了一遍,但願在閱讀徹底文以後,對你們瞭解 webpack原理有所幫助~
本片文章代碼都是通過刪減更改處理的,都是爲了能更好的理解。能力有限,若是有不正確的地方歡迎你們指正,一塊兒交流學習。