webpack運行流程、源碼解析,Tabable原理

前言

在瞭解原理以前,咱們須要手動配置並運行過簡單的webpack,如:css

重要的幾個點:vue

  • entry: 入口文件
  • module: 模塊,一個文件是一個模塊
  • loader:文件轉換器,能讓你在js中引入各類其餘文件如 .txt .css
  • plugin:插件,最重要,實現各類功能
  • chunk:代碼塊,按需加載中的分塊文件

基於node環境,js代碼有了操做計算機文件的權限,所以,webpack就是一堆js代碼,而後去折騰一堆文件。node

工廠流水線

webpck能夠當作是一個工廠的流水線。webpack

一個飲料流水線的例子

從圖中能夠看到,整個過程是從左至右進行的,紫色的字所表明的,就是一個大的操做集, 操做集中能夠包含多個子操做,如 發酵 操做集中,有3個子操做,也就是說只有完成這3個子操做,才能結束發酵這個操做集,才能進入到下一個操做集:調配web

webpack 流程

如今,咱們把webpack帶入到流程圖中,藍色大字。算法

解析1: hook(鉤子)

圖中的每個紫色操做集,在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() // 運行
複製代碼

解析2:Complier,Compliation

圖中能夠發現,加粗的藍色爲Compliation。webpack中Complier負責整個的構建流程(準備、編譯、輸出等),而Compliation只負責其中的 編譯 過程。還有,Compliation只表明一次編譯,也就是說,每當文件有變更,就從新生成一個Compliation實例,即一個文件結構,對應一個Compliation。數組

一個Compilation對象包含了當前的模塊資源、編譯生成資源、變化的文件等。緩存

解析3:運行流程

對於紫色操做集,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

Tabable

tapable庫暴露了不少Hook(鉤子)類,爲插件提供掛載的鉤子。

咱們主要看這hook類,其餘的Hook能夠後自行研究

exports.SyncHook = require("./SyncHook");
複製代碼

SyncHook.js

繼承自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的回調函數存到數組中,再集中執行。

Compiler.js

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加進去的呢?接着看

因此

plugin

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等操做...
        })
        
    }
}
複製代碼

webpack.js

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的總體運行流程就差很少了,接下來,咱們逐個研究流程中的各個操做集細節~

源碼部分

注意:按文件搜索目標文件,好比想查看hooks.beforeRun的插件是何時註冊的,搜索lib文件夾hooks.beforeRun.tap,若按照代碼去找,須要花很長時間。。。

主要研究如下這些

  • Compiler.run()
  • Compiler.compile() 開始編譯
  • Compiler.make() 從入口分析依賴以及間接依賴模塊,建立模塊對象
  • Compilation.buildModule() 模塊構建
  • Compiler.normalModuleFactory() 構建
  • Compilation.seal() 構建結果封裝, 不可再更改
  • Compiler.afterCompile() 完成構建,緩存數據
  • Compiler.emit() 輸出到dist目錄

Compiler.run()

// 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.compile() 重要

// 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);
	        });
	    });
        });
    });
}
複製代碼

this.hooks.make 方法

一個新的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);
}
);
    
}

複製代碼

Compilation.addEntry

// 主要執行的_addModuleChain方法
複製代碼

_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

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 => {
            ......
        }
    );
複製代碼

NormalModule.js

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)。
複製代碼

Compilation.seal

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() 的格式。

Compiler.done

最後一步,webpack 調用 Compiler 中的 emitAssets() ,按照 output 中的配置項將文件輸出到了對應的 path 中,從而 webpack 整個打包過程結束。

本文連接

webpack很複雜,其中好多個點都須要深刻剖析,咱們須要按期的回看源碼,每一次都會有更深的理解。我借鑑了不少大佬的文章思路,理解尚淺,在通往大佬的過程當中,我會常常回來完善~~~

相關文章
相關標籤/搜索