webpack詳解

騰訊IVWEB團隊發表於2018-03-10

webpack是現代前端開發中最火的模組打包工具,只需要通過簡單的配置,便可以完成模組的載入和打包。那它是怎麼做到通過對一些外掛的配置,便可以輕鬆實現對程式碼的構建呢?

webpack的配置

const path = require('path');
module.exports = {
  entry: "./app/entry", // string | object | array
  // Webpack打包的入口
  output: {  // 定義webpack如何輸出的選項
    path: path.resolve(__dirname, "dist"), // string
    // 所有輸出檔案的目標路徑
    filename: "[chunkhash].js", // string
    // 「入口(entry chunk)」檔案命名模版
    publicPath: "/assets/", // string
    // 構建檔案的輸出目錄
    /* 其它高階配置 */
  },
  module: {  // 模組相關配置
    rules: [ // 配置模組loaders,解析規則
      {
        test: /\.jsx?$/,  // RegExp | string
        include: [ // 和test一樣,必須匹配選項
          path.resolve(__dirname, "app")
        ],
        exclude: [ // 必不匹配選項(優先順序高於test和include)
          path.resolve(__dirname, "app/demo-files")
        ],
        loader: "babel-loader", // 模組上下文解析
        options: { // loader的可選項
          presets: ["es2015"]
        },
      },
  },
  resolve: { //  解析模組的可選項
    modules: [ // 模組的查詢目錄
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    extensions: [".js", ".json", ".jsx", ".css"], // 用到的檔案的擴充套件
    alias: { // 模組別名列表
      "module": "new-module"
	  },
  },
  devtool: "source-map", // enum
  // 為瀏覽器開發者工具新增後設資料增強除錯
  plugins: [ // 附加外掛列表
    // ...
  ],
}
複製程式碼

從上面我們可以看到,webpack配置中需要理解幾個核心的概念EntryOutputLoadersPluginsChunk

  • Entry:指定webpack開始構建的入口模組,從該模組開始構建並計算出直接或間接依賴的模組或者庫
  • Output:告訴webpack如何命名輸出的檔案以及輸出的目錄
  • Loaders:由於webpack只能處理javascript,所以我們需要對一些非js檔案處理成webpack能夠處理的模組,比如sass檔案
  • Plugins:Loaders將各型別的檔案處理成webpack能夠處理的模組,plugins有著很強的能力。外掛的範圍包括,從打包優化和壓縮,一直到重新定義環境中的變數。但也是最複雜的一個。比如對js檔案進行壓縮優化的UglifyJsPlugin外掛
  • Chunk:coding split的產物,我們可以對一些程式碼打包成一個單獨的chunk,比如某些公共模組,去重,更好的利用快取。或者按需載入某些功能模組,優化載入時間。在webpack3及以前我們都利用CommonsChunkPlugin將一些公共程式碼分割成一個chunk,實現單獨載入。在webpack4 中CommonsChunkPlugin被廢棄,使用SplitChunksPlugin

webpack詳解

讀到這裡,或許你對webpack有一個大概的瞭解,那webpack 是怎麼執行的呢?我們都知道,webpack是高度複雜抽象的外掛集合,理解webpack的執行機制,對於我們日常定位構建錯誤以及寫一些外掛處理構建任務有很大的幫助。

不得不說的tapable

webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的例項。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括

  • plugin(name:string, handler:function)註冊外掛到Tapable物件中
  • apply(…pluginInstances: (AnyPlugin|function)[])呼叫外掛的定義,將事件監聽器註冊到Tapable例項登錄檔中
  • applyPlugins*(name:string, …)多種策略細緻地控制事件的觸發,包括applyPluginsAsyncapplyPluginsParallel等方法實現對事件觸發的控制,實現

(1)多個事件連續順序執行 (2)並行執行 (3)非同步執行 (4)一個接一個地執行外掛,前面的輸出是後一個外掛的輸入的瀑布流執行順序 (5)在允許時停止執行外掛,即某個外掛返回了一個undefined的值,即退出執行 我們可以看到,Tapable就像nodejs中EventEmitter,提供對事件的註冊on和觸發emit,理解它很重要,看個栗子:比如我們來寫一個外掛

function CustomPlugin() {}
CustomPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', pluginFunction);
}
複製程式碼

在webpack的生命週期中會適時的執行

this.apply*("emit",options)
複製程式碼

當然上面提到的Tapable都是1.0版本之前的,如果想深入學習,可以檢視Tapable 和 事件流 那1.0的Tapable又是什麼樣的呢?1.0版本發生了巨大的改變,不再是此前的通過plugin註冊事件,通過applyPlugins*觸發事件呼叫,那1.0的Tapable是什麼呢?

暴露出很多的鉤子,可以使用它們為外掛建立鉤子函式

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
複製程式碼

我們來看看 怎麼使用。

class Order {
    constructor() {
        this.hooks = { //hooks
            goods: new SyncHook(['goodsId', 'number']),
            consumer: new AsyncParallelHook(['userId', 'orderId'])
        }
    }

    queryGoods(goodsId, number) {
        this.hooks.goods.call(goodsId, number);
    }

    consumerInfoPromise(userId, orderId) {
        this.hooks.consumer.promise(userId, orderId).then(() => {
            //TODO
        })
    }

    consumerInfoAsync(userId, orderId) {
        this.hooks.consumer.callAsync(userId, orderId, (err, data) => {
            //TODO
        })
    }
}
複製程式碼

對於所有的hook的建構函式均接受一個可選的string型別的陣列

const hook = new SyncHook(["arg1", "arg2", "arg3"]);
複製程式碼
// 呼叫tap方法註冊一個consument
order.hooks.goods.tap('QueryPlugin', (goodsId, number) => {
    return fetchGoods(goodsId, number);
})
// 再新增一個
order.hooks.goods.tap('LoggerPlugin', (goodsId, number) => {
    logger(goodsId, number);
})

// 呼叫
order.queryGoods('10000000', 1)
複製程式碼

對於一個 SyncHook,我們通過tap來新增消費者,通過call來觸發鉤子的順序執行。

對於一個非sync*型別的鉤子,即async*型別的鉤子,我們還可以通過其它方式註冊消費者和呼叫

// 註冊一個sync 鉤子
order.hooks.consumer.tap('LoggerPlugin', (userId, orderId) => {
   logger(userId, orderId);
})

order.hooks.consumer.tapAsync('LoginCheckPlugin', (userId, orderId, callback) => {
    LoginCheck(userId, callback);
})

order.hooks.consumer.tapPromise('PayPlugin', (userId, orderId) => {
    return Promise.resolve();
})

// 呼叫
// 返回Promise
order.consumerInfoPromise('user007', '1024');

//回撥函式
order.consumerInfoAsync('user007', '1024')
複製程式碼

通過上面的栗子,你可能已經大致瞭解了Tapable的用法,它的用法

  • 外掛註冊數量
  • 外掛註冊的型別(sync, async, promise)
  • 呼叫的方式(sync, async, promise)
  • 例項鉤子的時候引數數量
  • 是否使用了interception

Tapable詳解

Alt text
對於Sync*型別的鉤子來說。

  • 註冊在該鉤子下面的外掛的執行順序都是順序執行。
  • 只能使用tap註冊,不能使用tapPromisetapAsync註冊
// 所有的鉤子都繼承於Hook
class Sync* extends Hook { 
	tapAsync() { // Sync*型別的鉤子不支援tapAsync
		throw new Error("tapAsync is not supported on a Sync*");
	}
	tapPromise() {// Sync*型別的鉤子不支援tapPromise
		throw new Error("tapPromise is not supported on a Sync*");
	}
	compile(options) { // 編譯程式碼來按照一定的策略執行Plugin
		factory.setup(this, options);
		return factory.create(options);
	}
}
複製程式碼

對於Async*型別鉤子

  • 支援taptapPromisetapAsync註冊
class AsyncParallelHook extends Hook {
	constructor(args) {
		super(args);
		this.call = this._call = undefined;
	}

	compile(options) {
		factory.setup(this, options);
		return factory.create(options);
	}
}
複製程式碼
class Hook {
	constructor(args) {
		if(!Array.isArray(args)) args = [];
		this._args = args; // 例項鉤子的時候的string型別的陣列
		this.taps = []; // 消費者
		this.interceptors = []; // interceptors
		this.call = this._call =  // 以sync型別方式來呼叫鉤子
		this._createCompileDelegate("call", "sync");
		this.promise = 
		this._promise = // 以promise方式
		this._createCompileDelegate("promise", "promise");
		this.callAsync = 
		this._callAsync = // 以async型別方式來呼叫
		this._createCompileDelegate("callAsync", "async");
		this._x = undefined; // 
	}

	_createCall(type) {
		return this.compile({
			taps: this.taps,
			interceptors: this.interceptors,
			args: this._args,
			type: type
		});
	}

	_createCompileDelegate(name, type) {
		const lazyCompileHook = (...args) => {
			this[name] = this._createCall(type);
			return this[name](...args);
		};
		return lazyCompileHook;
	}
	// 呼叫tap 型別註冊
	tap(options, fn) {
		// ...
		options = Object.assign({ type: "sync", fn: fn }, options);
		// ...
		this._insert(options);  // 新增到 this.taps中
	}
	// 註冊 async型別的鉤子
	tapAsync(options, fn) {
		// ...
		options = Object.assign({ type: "async", fn: fn }, options);
		// ...
		this._insert(options); // 新增到 this.taps中
	}
	註冊 promise型別鉤子
	tapPromise(options, fn) {
		// ...
		options = Object.assign({ type: "promise", fn: fn }, options);
		// ...
		this._insert(options); // 新增到 this.taps中
	}
	
}
複製程式碼

每次都是呼叫taptapSynctapPromise註冊不同型別的外掛鉤子,通過呼叫callcallAsyncpromise方式呼叫。其實呼叫的時候為了按照一定的執行策略執行,呼叫compile方法快速編譯出一個方法來執行這些外掛。

const factory = new Sync*CodeFactory();
class Sync* extends Hook { 
	// ...
	compile(options) { // 編譯程式碼來按照一定的策略執行Plugin
		factory.setup(this, options);
		return factory.create(options);
	}
}

class Sync*CodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}
複製程式碼

compile中呼叫HookCodeFactory#create方法編譯生成執行程式碼。


class HookCodeFactory {
	constructor(config) {
		this.config = config;
		this.options = undefined;
	}

	create(options) {
		this.init(options);
		switch(this.options.type) {
			case "sync":  // 編譯生成sync, 結果直接返回
				return new Function(this.args(), 
				"\"use strict\";\n" + this.header() + this.content({
					// ...
					onResult: result => `return ${result};\n`,
					// ...
				}));
			case "async": // async型別, 非同步執行,最後將呼叫外掛執行結果來呼叫callback,
				return new Function(this.args({
					after: "_callback"
				}), "\"use strict\";\n" + this.header() + this.content({
					// ...
					onResult: result => `_callback(null, ${result});\n`,
					onDone: () => "_callback();\n"
				}));
			case "promise": // 返回promise型別,將結果放在resolve中
				// ...
				code += "return new Promise((_resolve, _reject) => {\n";
				code += "var _sync = true;\n";
				code += this.header();
				code += this.content({
					// ...
					onResult: result => `_resolve(${result});\n`,
					onDone: () => "_resolve();\n"
				});
			    // ...
				return new Function(this.args(), code);
		}
	}
	// callTap 就是執行一些外掛,並將結果返回
	callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
		let code = "";
		let hasTapCached = false;
		// ...
		code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
		const tap = this.options.taps[tapIndex];
		switch(tap.type) {
			case "sync":
				// ...
				if(onResult) {
					code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
						before: tap.context ? "_context" : undefined
					})});\n`;
				} else {
					code += `_fn${tapIndex}(${this.args({
						before: tap.context ? "_context" : undefined
					})});\n`;
				}
				
				if(onResult) { // 結果透傳
					code += onResult(`_result${tapIndex}`);
				}
				if(onDone) { // 通知外掛執行完畢,可以執行下一個外掛
					code += onDone();
				}
				break;
			case "async": //非同步執行,外掛執行完後再將結果通過執行callback透傳
				let cbCode = "";
				if(onResult)
					cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
				else
					cbCode += `_err${tapIndex} => {\n`;
				cbCode += `if(_err${tapIndex}) {\n`;
				cbCode += onError(`_err${tapIndex}`);
				cbCode += "} else {\n";
				if(onResult) {
					cbCode += onResult(`_result${tapIndex}`);
				}
				
				cbCode += "}\n";
				cbCode += "}";
				code += `_fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined,
					after: cbCode //cbCode將結果透傳
				})});\n`;
				break;
			case "promise": // _fn${tapIndex} 就是第tapIndex 個外掛,它必須是個Promise型別的外掛
				code += `var _hasResult${tapIndex} = false;\n`;
				code += `_fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined
				})}).then(_result${tapIndex} => {\n`;
				code += `_hasResult${tapIndex} = true;\n`;
				if(onResult) {
					code += onResult(`_result${tapIndex}`);
				}
			// ...
				break;
		}
		return code;
	}
	// 按照外掛的註冊順序,按照順序遞迴呼叫執行外掛
	callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
		// ...
		const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
		const next = i => {
			// ...
			const done = () => next(i + 1);
			// ...
			return this.callTap(i, {
				// ...
				onResult: onResult && ((result) => {
					return onResult(i, result, done, doneBreak);
				}),
				// ...
			});
		};
		return next(0);
	}

	callTapsLooping({ onError, onDone, rethrowIfPossible }) {
		
		const syncOnly = this.options.taps.every(t => t.type === "sync");
		let code = "";
		if(!syncOnly) {
			code += "var _looper = () => {\n";
			code += "var _loopAsync = false;\n";
		}
		code += "var _loop;\n";
		code += "do {\n";
		code += "_loop = false;\n";
		// ...
		code += this.callTapsSeries({
			// ...
			onResult: (i, result, next, doneBreak) => { // 一旦某個外掛返回不為undefined,  即一隻呼叫某個外掛執行,如果為undefined,開始呼叫下一個
				let code = "";
				code += `if(${result} !== undefined) {\n`;
				code += "_loop = true;\n";
				if(!syncOnly)
					code += "if(_loopAsync) _looper();\n";
				code += doneBreak(true);
				code += `} else {\n`;
				code += next();
				code += `}\n`;
				return code;
			},
			// ...
		})
		code += "} while(_loop);\n";
		// ...
		return code;
	}
	// 並行呼叫外掛執行
	callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
		// ...
		// 遍歷註冊都所有外掛,並呼叫
		for(let i = 0; i < this.options.taps.length; i++) {
			// ...
			code += "if(_counter <= 0) break;\n";
			code += onTap(i, () => this.callTap(i, {
				// ...
				onResult: onResult && ((result) => {
					let code = "";
					code += "if(_counter > 0) {\n";
					code += onResult(i, result, done, doneBreak);
					code += "}\n";
					return code;
				}),
				// ...
			}), done, doneBreak);
		}
		// ...
		return code;
	}
}
複製程式碼

HookCodeFactory#create中呼叫到content方法,此方法將按照此鉤子的執行策略,呼叫不同的方法來執行編譯 生成最終的程式碼。

  • SyncHook中呼叫`callTapsSeries`編譯生成最終執行外掛的函式,`callTapsSeries`做的就是將外掛列表中外掛按照註冊順序遍歷執行。
    複製程式碼
class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}
複製程式碼
  • SyncBailHook中當一旦某個返回值結果不為undefined便結束執行列表中的外掛
 class SyncBailHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			// ...
			onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
			// ...
		});
	}
}
複製程式碼
  • SyncWaterfallHook中上一個外掛執行結果當作下一個外掛的入參
class SyncWaterfallHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			// ...
			onResult: (i, result, next) => {
				let code = "";
				code += `if(${result} !== undefined) {\n`;
				code += `${this._args[0]} = ${result};\n`;
				code += `}\n`;
				code += next();
				return code;
			},
			onDone: () => onResult(this._args[0]),
		});
	}
}
複製程式碼
  • AsyncParallelHook呼叫callTapsParallel並行執行外掛
class AsyncParallelHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone }) {
		return this.callTapsParallel({
			onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),
			onDone
		});
	}
}
複製程式碼

webpack流程篇

本文關於webpack 的流程講解是基於webpack4的。

webpack 入口檔案

從webpack專案的package.json檔案中我們找到了入口執行函式,在函式中引入webpack,那麼入口將會是lib/webpack.js,而如果在shell中執行,那麼將會走到./bin/webpack.js,我們就以lib/webpack.js為入口開始吧!

{
  "name": "webpack",
  "version": "4.1.1",
  ...
  "main": "lib/webpack.js",
  "web": "lib/webpack.web.js",
  "bin": "./bin/webpack.js",
  ...
  }
複製程式碼

webpack入口

const webpack = (options, callback) => {
    // ...
    // 驗證options正確性
    // 預處理options
    options = new WebpackOptionsDefaulter().process(options); // webpack4的預設配置
	compiler = new Compiler(options.context); // 例項Compiler
	// ...
    // 若options.watch === true && callback 則開啟watch執行緒
	compiler.watch(watchOptions, callback);
	compiler.run(callback);
	return compiler;
};
複製程式碼

webpack 的入口檔案其實就例項了Compiler並呼叫了run方法開啟了編譯,webpack的編譯都按照下面的鉤子呼叫順序執行。

  • before-run 清除快取
  • run 註冊快取資料鉤子
  • before-compile
  • compile 開始編譯
  • make 從入口分析依賴以及間接依賴模組,建立模組物件
  • build-module 模組構建
  • seal 構建結果封裝, 不可再更改
  • after-compile 完成構建,快取資料
  • emit 輸出到dist目錄

編譯&構建流程

webpack中負責構建和編譯都是Compilation

class Compilation extends Tapable {
	constructor(compiler) {
		super();
		this.hooks = {
			// hooks
		};
		// ...
		this.compiler = compiler;
		// ...
		// template
		this.mainTemplate = new MainTemplate(this.outputOptions);
		this.chunkTemplate = new ChunkTemplate(this.outputOptions);
		this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(
			this.outputOptions
		);
		this.runtimeTemplate = new RuntimeTemplate(
			this.outputOptions,
			this.requestShortener
		);
		this.moduleTemplates = {
			javascript: new ModuleTemplate(this.runtimeTemplate),
			webassembly: new ModuleTemplate(this.runtimeTemplate)
		};

		// 構建生成的資源
		this.chunks = [];
		this.chunkGroups = [];
		this.modules = [];
		this.additionalChunkAssets = [];
		this.assets = {};
		this.children = [];
		// ...
	}
	// 
	buildModule(module, optional, origin, dependencies, thisCallback) {
		// ...
		// 呼叫module.build方法進行編譯程式碼,build中 其實是利用acorn編譯生成AST
		this.hooks.buildModule.call(module);
		module.build(/**param*/);
	}
	// 將模組新增到列表中,並編譯模組
	_addModuleChain(context, dependency, onModule, callback) {
		    // ...
		    // moduleFactory.create建立模組,這裡會先利用loader處理檔案,然後生成模組物件
		    moduleFactory.create(
				{
					contextInfo: {
						issuer: "",
						compiler: this.compiler.name
					},
					context: context,
					dependencies: [dependency]
				},
				(err, module) => {
					const addModuleResult = this.addModule(module);
					module = addModuleResult.module;
					onModule(module);
					dependency.module = module;
					
					// ...
					// 呼叫buildModule編譯模組
					this.buildModule(module, false, null, null, err => {});
				}
		});
	}
	// 新增入口模組,開始編譯&構建
	addEntry(context, entry, name, callback) {
		// ...
		this._addModuleChain( // 呼叫_addModuleChain新增模組
			context,
			entry,
			module => {
				this.entries.push(module);
			},
			// ...
		);
	}

	
	seal(callback) {
		this.hooks.seal.call();

		// ...
		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.hooks.beforeHash.call();
		this.createHash();
		this.hooks.afterHash.call();
		this.hooks.beforeModuleAssets.call();
		this.createModuleAssets();
		if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
			this.hooks.beforeChunkAssets.call();
			this.createChunkAssets();
		}
		// ...
	}


	createHash() {
		// ...
	}
	
	// 生成 assets 資源並 儲存到 Compilation.assets 中 給webpack寫外掛的時候會用到
	createModuleAssets() {
		for (let i = 0; i < this.modules.length; i++) {
			const module = this.modules[i];
			if (module.buildInfo.assets) {
				for (const assetName of Object.keys(module.buildInfo.assets)) {
					const fileName = this.getPath(assetName);
					this.assets[fileName] = module.buildInfo.assets[assetName]; 
					this.hooks.moduleAsset.call(module, fileName);
				}
			}
		}
	}

	createChunkAssets() {
	 // ...
	}
}
複製程式碼

在webpack make鉤子中, tapAsync註冊了一個DllEntryPlugin, 就是將入口模組通過呼叫compilation.addEntry方法將所有的入口模組新增到編譯構建佇列中,開啟編譯流程。

compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
		compilation.addEntry(
			this.context,
			new DllEntryDependency(
				this.entries.map((e, idx) => {
					const dep = new SingleEntryDependency(e);
					dep.loc = `${this.name}:${idx}`;
					return dep;
				}),
				this.name
			),
			// ...
		);
	});
複製程式碼

隨後在addEntry 中呼叫_addModuleChain開始編譯。在_addModuleChain首先會生成模組,最後構建。

class NormalModuleFactory extends Tapable {
	// ...
	create(data, callback) {
		// ...
		this.hooks.beforeResolve.callAsync(
			{
				contextInfo,
				resolveOptions,
				context,
				request,
				dependencies
			},
			(err, result) => {
				if (err) return callback(err);

				// Ignored
				if (!result) return callback();
				// factory 鉤子會觸發 resolver 鉤子執行,而resolver鉤子中會利用acorn 處理js生成AST,再利用acorn處理前,會使用loader載入檔案
				const factory = this.hooks.factory.call(null);

				factory(result, (err, module) => {
					if (err) return callback(err);

					if (module && this.cachePredicate(module)) {
						for (const d of dependencies) {
							d.__NormalModuleFactoryCache = module;
						}
					}

					callback(null, module);
				});
			}
		);
	}
}
複製程式碼

在編譯完成後,呼叫compilation.seal方法封閉,生成資源,這些資源儲存在compilation.assets, compilation.chunk, 在給webpack寫外掛的時候會用到

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			beforeRun: new AsyncSeriesHook(["compilation"]),
			run: new AsyncSeriesHook(["compilation"]),
			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"]),
			// other hooks
		};
		// ...
	}

	run(callback) {
		const startTime = Date.now();

		const onCompiled = (err, compilation) => {
			// ...

			this.emitAssets(compilation, err => {
				if (err) return callback(err);

				if (compilation.hooks.needAdditionalPass.call()) {
					compilation.needAdditionalPass = true;

					const stats = new Stats(compilation);
					stats.startTime = startTime;
					stats.endTime = Date.now();
					this.hooks.done.callAsync(stats, err => {
						if (err) return callback(err);

						this.hooks.additionalPass.callAsync(err => {
							if (err) return callback(err);
							this.compile(onCompiled);
						});
					});
					return;
				}
				// ...
			});
		};

		this.hooks.beforeRun.callAsync(this, err => {
			if (err) return callback(err);
			this.hooks.run.callAsync(this, err => {
				if (err) return callback(err);

				this.readRecords(err => {
					if (err) return callback(err);

					this.compile(onCompiled);
				});
			});
		});
	}
	// 輸出檔案到構建目錄
	emitAssets(compilation, callback) {
		// ...
		this.hooks.emit.callAsync(compilation, err => {
			if (err) return callback(err);
			outputPath = compilation.getPath(this.outputPath);
			this.outputFileSystem.mkdirp(outputPath, emitFiles);
		});
	}
	
	newCompilationParams() {
		const params = {
			normalModuleFactory: this.createNormalModuleFactory(),
			contextModuleFactory: this.createContextModuleFactory(),
			compilationDependencies: new Set()
		};
		return params;
	}

	compile(callback) {
		const params = this.newCompilationParams();
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);
			this.hooks.compile.call(params);
			const compilation = this.newCompilation(params);

			this.hooks.make.callAsync(compilation, err => {
				if (err) return callback(err);
				compilation.finish();
				// make 鉤子執行後,呼叫seal生成資源
				compilation.seal(err => {
					if (err) return callback(err);
					this.hooks.afterCompile.callAsync(compilation, err => {
						if (err) return callback(err);
						// emit, 生成最終檔案
						return callback(null, compilation);
					});
				});
			});
		});
	}
}
複製程式碼

最後輸出

seal執行後,便會呼叫emit鉤子,根據webpack config檔案的output配置的path屬性,將檔案輸出到指定的path.


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章