在瞭解原理以前,咱們須要手動配置並運行過簡單的webpack,如:css
重要的幾個點:vue
基於node環境,js代碼有了操做計算機文件的權限,所以,webpack就是一堆js代碼,而後去折騰一堆文件。node
webpck能夠當作是一個工廠的流水線。webpack
從圖中能夠看到,整個過程是從左至右進行的,紫色的字所表明的,就是一個大的操做集, 操做集中能夠包含多個子操做,如 發酵 操做集中,有3個子操做,也就是說只有完成這3個子操做,才能結束發酵這個操做集,才能進入到下一個操做集:調配。web
如今,咱們把webpack帶入到流程圖中,藍色大字。算法
圖中的每個紫色操做集,在webpack中,都是一個hook的實例,hook是一個類,定義了一些方法如,添加plugin,運行全部plugin等等。hook有多種,同步or異步執行plugin,是否帶返回值等....segmentfault
例如 設計模式
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
var hook = new SyncHook() // 實例一個同步的hook
hook.addPlugin(plugin1)
hook.addPlugin(plugin2)
...
Complier.emit = function() {
hook.runAllPlugins()
}
Complier.emit() // 運行
複製代碼
圖中能夠發現,加粗的藍色爲Compliation。webpack中Complier負責整個的構建流程(準備、編譯、輸出等),而Compliation只負責其中的 編譯 過程。還有,Compliation只表明一次編譯,也就是說,每當文件有變更,就從新生成一個Compliation實例,即一個文件結構,對應一個Compliation。數組
一個Compilation對象包含了當前的模塊資源、編譯生成資源、變化的文件等。緩存
對於紫色操做集,webpack中是定義 死 的,因此按順序執行代碼就行
Complier.beforeRun() // 執行對應的hook中的一堆plugin
Complier.run() // 執行對應的hook中的一堆plugin
Complier.make() // 執行對應的hook中的一堆plugin執行一堆plugin
Compliation.buildModule() // 執行對應的hook中的一堆plugin執行一堆plugin
...
複製代碼
而每一個裏面的plugin是活的,且容許用戶自定義添加plugin,以前說了是hook類提供的plugin相關的方法,接下來說一下hook是怎麼玩的 ==> Tabable
tapable庫暴露了不少Hook(鉤子)類,爲插件提供掛載的鉤子。
咱們主要看這hook類,其餘的Hook能夠後自行研究
exports.SyncHook = require("./SyncHook");
複製代碼
繼承自Hook.js,這裏我就都寫一塊兒了,挑乾的
class SyncHook {
constructor() {
// 存放plugins
this.taps = []
}
// 添加plugin
tap(name, fn) {
item = Object.assign({}, {type: 'sync', name, fn})
// 添加plugin
this.taps.push(item)
// 實際上,在push以前,還有會註冊攔截器、對taps中的plugins進行排序等,這裏只簡單模擬下
}
// 執行plugins
// 這個call,走的是SyncHook.js中的compile()
call() {
// factory.setup(this, options);
// return factory.create(options);
// 實際上後面走的callTapsSeries、callTap
// 模擬
this.taps.forEach(tap => { // 逐個plugin執行
tap.fn()
})
}
}
複製代碼
因此,hook就是訂閱發佈設計模式,將plugin的回調函數存到數組中,再集中執行。
class Compiler {
constructor() {
super();
this.hooks = { // 一大堆hooks
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
environment: new SyncHook([]),
afterEnvironment: new SyncHook([]),
afterPlugins: new SyncHook(["compiler"]),
entryOption: new SyncBailHook(["context", "entry"])
}
}
run() {
// 這個callAsync就是上面hook類中的call,只不過是異步的
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
this.compile(); //
});
});
});
}
compile() {
this.hooks.beforeCompile.callAsync(err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync()
...
})
}
}
複製代碼
從上面咱們能夠看到了,this.hooks裏定義了所有的 操做子集 , 而run、compile、等都是操做集,每一個操做集裏面會運行多個子集,這就和以前的流程圖對應上了。
不過,上面都是執行hook的call方法,那麼,hook裏面的plugins是何時tap加進去的呢?接着看
因此
class MyPlugin {
apply(compiler) { // 接收傳過來的compiler實例
compiler.hooks.beforeRun.tap('MyPlugin', function() {
// 回調函數
console.log(3333)
file等操做...
})
// 能夠註冊多個不一樣的hook
compiler.hooks.afterCompile.tap('MyPlugin', function() {
// 回調函數
console.log(444)
file等操做...
})
}
}
class YouPlugin {
apply(compiler) { // 接收傳過來的compiler實例
compiler.hooks.compile.tap('YouPlugin', function() {
// 回調函數
console.log(444)
file等操做...
})
}
}
複製代碼
const webpack = (options) => {
let compiler = new Compiler()
// 循環plugins,把每一個子plugin添加到對應的compiler的操做集上
for (const plugin of options.plugins) {
plugin.apply(compiler); // 將plugin註冊添加到compiler對應中
}
// 註冊plugin結束
// 開始運行。。。
compiler.run()
}
複製代碼
const options = {
plugins: [
new MyPlugin(),
new YouPlugin()
]
}
webpack(options)
複製代碼
!!!想查看一些插件是何時加到hook中的,打開WebpackOptionsApply.js文件
到這裏,webpack的總體運行流程就差很少了,接下來,咱們逐個研究流程中的各個操做集細節~
主要研究如下這些
// Compiler.js
run() {
// NodeEnvironmentPlugin.js 中 beforeRun添加plugins
this.hooks.beforeRun.callAsync(this, err => {
// CachePlugin.js 中 beforeRun添加plugins
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
// 開始編譯,重要
this.compile(onCompiled);
});
});
});
// 按文件搜索this.hooks.beforeRun.tap 就能找到在哪一個js中添加的,其餘插件一樣這樣搜
}
複製代碼
// Compiler.js
compile() {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
// 新建一個 compilation
// compilation 裏面也定義了 this.hooks , 原理和 Compiler 同樣
const compilation = this.newCompilation(params);
// 執行make函數,重要的
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
}
複製代碼
一個新的compilation對象建立完畢, 即將從entry開始讀取文件,根據文件類型和編譯的loader對文件進行==編譯==,編譯完後再找出該文件依賴的文件,遞歸地編譯和解析
// 搜索lib目錄,compiler.hooks.make.tap
// 主要定位到文件SingleEntryPlugin.js,DllEntryPlugin.js,
// SingleEntryPlugin.js
apply(compiler) {
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
// 進入compilation.addEntry方法
compilation.addEntry(context, dep, name, callback);
}
);
}
複製代碼
// 主要執行的_addModuleChain方法
複製代碼
// 主要作了兩件事情。一是根據模塊的類型獲取對應的模塊工廠並建立模塊,二是構建模塊。
經過 *ModuleFactory.create方法建立模塊,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)
對模塊使用的loader進行加載。
調用 acorn 解析經 loader 處理後的源文件生成抽象語法樹 AST。遍歷 AST,構建該模塊所依賴的模塊
_addModuleChain() {
moduleFactory.create(rsu => { // 打開 NormalModuleFactory.js 查看 create 方法
// 收集一系列信息而後建立一個module傳入回調
// 執行this.buildModule方法方法,重要
this.buildModule() // 見下方
})
}
複製代碼
buildModule(module, optional, origin, dependencies, thisCallback) {
// 觸發buildModule事件點
this.hooks.buildModule.call(module);
//!!! 開始build,主要執行的是NormalModuleFactory生成的NormalModule中的build方法,中的build方法,打開NormalModule
module.build( // doBuild
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
......
}
);
複製代碼
build() {
// 先看執行的doBuild
return this.doBuild(options, compilation, resolver, fs, err => {
// 調用parse方法,建立依賴Dependency並放入依賴數組
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);
}
})
}
doBuild(options, compilation, resolver, fs, callback) {
runLoaders(rsu => { // 獲取loader相關的信息並轉換成webpack須要的js文件,原理見下方連接
callback()// 回build中執行ast(抽象語法樹,編譯過程當中常見結構,vue、babel原理都有)
})
}
複製代碼
runLoaders:segmentfault.com/a/119000001…
對於當前模塊,或許存在着多個依賴模塊。當前模塊會開闢一個依賴模塊的數組,在遍歷 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)。
複製代碼
buildModule後,就到了Seal封裝構建結果這一步驟
seal(callback) {
// 觸發事件點seal
this.hooks.seal.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.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
複製代碼
而後,對生成編譯後的源碼,合併,拆分,生成 hash 。 同時這是咱們在開發時進行代碼優化和功能添加的關鍵環節。
template.getRenderMainfest.render()
複製代碼
經過模板(MainTemplate、ChunkTemplate)把chunk生產 _webpack_requie() 的格式。
最後一步,webpack 調用 Compiler 中的 emitAssets() ,按照 output 中的配置項將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。
webpack很複雜,其中好多個點都須要深刻剖析,咱們須要按期的回看源碼,每一次都會有更深的理解。我借鑑了不少大佬的文章思路,理解尚淺,在通往大佬的過程當中,我會常常回來完善~~~