webpack構建流程分析

前言

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 解析 configshell 中的配置項
  • webpack 初始化過程,首先會根據第一步的 options 生成 compiler 對象,而後初始化 webpack 的內置插件及 options 配置
  • run 表明編譯的開始,會構建 compilation 對象,用於存儲這一次編譯過程的全部數據
  • make 執行真正的編譯構建過程,從入口文件開始,構建模塊,直到全部模塊建立結束
  • seal 生成 chunks,對 chunks 進行一系列的優化操做,並生成要輸出的代碼
  • seal 結束後,Compilation 實例的全部工做到此也所有結束,意味着一次構建過程已經結束
  • emit 被觸發以後,webpack 會遍歷 compilation.assets, 生成全部文件,而後觸發任務點 done,結束構建流程

構建流程

在學習其餘技術博客時都有相似上面的主體流程的分析,道理都懂,但不打斷點看的細節點,說服不了本身。如下是一些任務點的詳細動做,建議有興趣的小夥伴多打幾個debuggerchrome

強烈建議在每一個重要鉤子的回調函數中打debugger,否則可能跳着跳着就走遠了shell

webpack準備階段

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 對象,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

初始化默認插件和Options配置

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 插件中訂閱了 compilermake 鉤子,並在回調中等待執行 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);
        }
    );
}
複製代碼

run

初始化 compiler 後,根據 optionswatch 判斷是否啓動了 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 對象會開始實例化兩個核心的工廠對象,分別是 NormalModuleFactoryContextModuleFactory。工廠對象顧名思義就是用來建立實例的,它們後續用來建立 module 實例的,包括 NormalModule 以及 ContextModule 實例。

Compilation

建立這次編譯的 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 實例時會觸發鉤子 compilaiionthisCompilation

在Compilation對象中:

  • modules 記錄了全部解析後的模塊
  • chunks 記錄了全部chunk
  • assets記錄了全部要生成的文件

上面這三個屬性已經包含了 Compilation 對象中大部分的信息,但目前也只是有個大體的概念,特別是 modules 中每一個模塊實例究竟是什麼東西,並不太清楚。先不糾結,畢竟此時 Compilation 對象剛剛生成。

make

Compilation 實例建立完成以後,webpack 的準備階段已經完成,下一步將開始 modules 的生成階段。

this.hooks.make.callAsync() 執行訂閱了 make 鉤子的插件的回調函數。回到上文,在初始化默認插件過程當中(WebpackOptionsApply類),SingleEntryPlugin 插件中訂閱了 compilermake 鉤子,並在回調中等待執行 compilation.addEntry 方法。

生成modules

compilation.addEntry 方法會觸發第一批 module 的解析,即咱們在 entry 中配置的入口文件 index.js。在深刻 modules 的構建流程以前,咱們先對模塊實例 module 的概念有個瞭解。

modules

一個依賴對象(Dependency)通過對應的工廠對象(Factory)建立以後,就可以生成對應的模塊實例(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

_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

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.jssource就是 babel-loader 編譯後的代碼。

// source
"debugger; import { helloWorld } from './helloworld.js';document.write(helloWorld());」
複製代碼

同時,還會生成this._source對象,有namevalue兩個字段,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方法,進入下一個階段。

生成chunks

compilation.seal 方法主要生成chunks,對chunks進行一系列的優化操做,並生成要輸出的代碼。webpack 中的 chunk ,能夠理解爲配置在 entry 中的模塊,或者是動態引入的模塊。

chunk內部的主要屬性是_modules,用來記錄包含的全部模塊對象。因此要生成一個chunk,就先要找到它包含的全部modules。下面簡述一下chunk的生成過程:

  • 先把 entry 中對應的每一個 module 都生成一個新的 chunk
  • 遍歷module.dependencies,將其依賴的模塊也加入到上一步生成的chunk中
  • 若某個module是動態引入的,爲其建立一個新的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, ChunkTemplateModuleTemplate。這三個對象是用來渲染chunk,獲得最終代碼模板的。它們之間的不一樣在於,MainTemplate用來渲染入口 chunk,ChunkTemplate用來渲染非入口 chunk,ModuleTemplate用來渲染 chunk 中的模塊。

這裏, MainTemplateChunkTemplaterender 方法是用來生成不一樣的"包裝代碼"的,MainTemplate 對應的入口 chunk 須要帶有 webpack 的啓動代碼,因此會有一些函數的聲明和啓動。而包裝代碼中,每一個模塊的代碼是經過 ModuleTemplate 來渲染的,不過一樣只是生成」包裝代碼」來封裝真正的模塊代碼,而真正的模塊代碼,是經過模塊實例的 source 方法來提供。這麼說可能不是很好理解,直接看看最終生成文件中的代碼,以下:

每一個chunk的源碼生成以後,會調用 emitAsset 將其存在 compilation.assets 中。當全部的 chunk 都渲染完成以後,assets 就是最終更要生成的文件列表。至此, compilationseal 方法結束,也表明着 compilation 實例的全部工做到此也所有結束,意味着一次構建過程已經結束,接下來只有文件生成的步驟了。

emit

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原理有所幫助~

本片文章代碼都是通過刪減更改處理的,都是爲了能更好的理解。能力有限,若是有不正確的地方歡迎你們指正,一塊兒交流學習。

相關文章
相關標籤/搜索