Webpack那些你不知道的事

隨着前端工程化的不斷髮展,構建工具也在不斷完善。Webpack藉由它強大的擴展能力及萬物皆模塊的概念,逐漸成爲前端構建工具中的小王子,隨着webpack4的不斷迭代,咱們享受着構建效率不斷提高帶來的快感,配置不斷減小的溫馨,也許你已經能夠熟練使用Webpack進行項目構建,可是在編寫本身的Plugin的時候不知道如何下手,也許你想閱讀Webpack源碼,可是當本身看的時候感到十分複雜而步履維艱。請不用擔憂,此篇文章會詳細的爲你講解Webpack中的插件機制原理,以及事件流機制原理工做流程原理,幫助你輕鬆探索Webpack中的那些未解之謎。javascript

在閱讀本文以前,咱們但願你已經掌握了Webpack的基本配置,可以獨立搭建一款基於Webpack的前端自動化構建體系,因此這篇文章不會教你如何配置或者使用Webpack,基本概念咱們就不作介紹了,直面主題,開始講解Webpack原理。css

Plugin

介紹

首先呢,說下plugin,我以爲plugin就是用來擴展webpack的功能的。不過相對於loader,它彷彿更加的「無所不能」且很是的靈活。下面咱們從plugin的配置入手,分析一下plugin的是如何嵌入到webpack中的,先動手作一個很是簡易的plugin,在實踐中學習。最後從這個小小的例子中,深刻到webpack的事件流,來看看這個驅動的webpack的"核心引擎" => tapable前端

先從基礎開始看看,怎麼配置一個plugin

plugins: [
    new WebpackPluginXXXXX({
        //...param
    }),
]
複製代碼

很是的簡單易懂,建立一個plugin的對象,而後放到數組中。。。
那麼plugin內部是怎樣的規則才能嵌入到webpack中執行呢?請看下面。vue

寫一個簡單plugin

官網文檔:教你如何寫插件 webpack.js.org/contribute/…java

先寫一個最最最簡單的plugin,讓他run起來。node

class MyPlugin {
  apply(compiler) {
    compiler.hooks.run.tap("myPlugin", compiler=> {
      console.log("我寫的插件正在運行!!!");
    });
  }
}
module.exports = { MyPlugin };
複製代碼

而後咱們新建個項目測試下react

注意:個人 node版本 10.16.0 ,webpack版本4.41.2webpack

而後放到配置文件中。git

const { MyPlugin } = require("./plugin/MyPlugin");
module.exports = {
  entry: {
    app: "./src/index.js"
  },
  plugins: [new MyPlugin()]
};
複製代碼

ok 看下效果。github

配置效果

發現我寫的已經在運行了~
經過官網學習和實踐,我得出這裏面最重要的是apply方法,這是與webpack內部運行接軌的方法。從apply中獲得 compiler對象, compiler 對象可在整個編譯生命週期訪問,經過compiler.hooks來訪問各類各樣的鉤子,好比run方法就是其中的hook之一,官網定義以下圖,經過tap方法來註冊到該事件中,來監聽事件響應。compiler爲 tapable的實例,tap就是 tapable中註冊同步執行的鉤子,類型爲 AsyncSeriesHook(下面會有對tapable,以及相應hook類型的詳解)。

運行效果

插件必定是class嗎?因而我又嘗試下兩種其餘寫法。

const MyPlugin2 = {
  apply(compiler) {
    compiler.hooks.run.tap("myPlugin", compilation => {
      console.log("我寫的第二個插件也正在運行!!!");
    });
  }
};
function MyPlugin3() {}
MyPlugin3.prototype.apply = function(compiler) {
  compiler.hooks.run.tap("myPlugin", compilation => {
    console.log("我寫的第三個插件也正在運行!!!");
  });
};
module.exports = { MyPlugin, MyPlugin2, MyPlugin3 };
複製代碼

而後在配置文件中添加以下,而後運行。

plugins: [new MyPlugin(), MyPlugin2, new MyPlugin3()]
複製代碼

結果發現,3個插件居然都能順利運行,不過確定是不推薦第二種。

ok,下面咱們來寫一個稍微有點實用價值的插件,這個功能是:在打包完成以後插入一段註釋。

const { ConcatSource } = require("webpack-sources");
class MyPlugin {
  apply(compiler) {
    //使用compilation hook 編譯(compilation)建立以後,執行插件。
    compiler.hooks.compilation.tap("BannerPlugin", compilation => {
      //優化全部 chunk 資源(asset)。資源(asset)會被存儲在 compilation.assets。
      // 每一個 Chunk 都有一個 files 屬性,指向這個 chunk 建立的全部文件。
      //附加資源(asset)被存儲在 compilation.additionalChunkAssets 中。
      compilation.hooks.optimizeChunkAssets.tap("BannerPlugin", chunks => {
        for (const chunk of chunks) {
          for (const file of chunk.files) {
            compilation.updateAsset(file, old => {         
              return new ConcatSource("/*!我在這裏加入一行註釋*/","\n", old);
            });
          }
        }
      });
    });
  }
}
module.exports = { MyPlugin };
複製代碼

而後就能夠在打包中看到效果了。

打包效果

ConcatSource 這個是一個webpack 合併資源的方法 查看更多=> www.npmjs.com/package/web…

在上面的例子中,咱們用了compiler的compilation鉤子和compilationoptimizeChunkAssets鉤子,咱們看下官方文檔。

官方文檔-compilation
在run鉤子中 Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。 Compilation 對象也提供了不少事件回調供插件作擴展。每當檢測到文件變化,一次新的 Compilation 將被建立。

當我想寫一些本身的插件的時候,看文檔是必不可少的,這時我對上圖中紅框的內容產生了疑問,這是什麼?要了解這些,就要深刻學習webpack的比較核心的庫,tapable,處理webpack複雜交通的指揮樞紐。

Tapable

通過了很長一段時間的學習,我發現我以前眼中高大上的tapable其實原理並不複雜,其設計模式是前端最經常使用的設計模式之一,觀察者模式。有點像nodejs的Events,註冊一個事件,而後到了適當的時候觸發,下面events,你們回顧下,一個作監聽,一個作觸發。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//on的第一個參數是事件名,以後emit能夠經過這個事件名,從而觸發這個方法。
//on的第二個參數是回掉函數,也就是此事件的執行方法
myEmitter.on('newListener', (param1,param2) => {
    console.log("newListener",param1,param2)
});
//emit的第一個參數是觸發的事件名
//emit的第二個之後的參數是回調函數的參數。
myEmitter.emit('newListener',111,222);
複製代碼

可是webpack的需求可能不只僅是一個Events能夠支撐的,必定有更復雜的需求,那麼這個升級版的Events到底提供了哪些功能呢?

咱們找到了npm 庫中的tapable,而後發現tapable庫暴露了不少Hook(鉤子)類,爲插件提供掛載的鉤子。

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

複製代碼

SyncHook

這個是最普通的同步hook,也具備表明性,由於全部的鉤子構造函數都採用一個可選參數,即做爲字符串數組的參數名列表。 tap爲綁定該hook的方法,第一個參數爲事件名稱,第二參數爲事件回調函數。

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

//綁定事件到webapck事件流
hook.tap('plugin1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3))  
hook.tap('plugin2', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) 

//執行綁定的事件
hook.call(1,2,3)
//1,2,3
//1,2,3
複製代碼

syncHook
Sync爲同步函數,下面咱們來分別看看這些函數的功能。

SyncHook:順序執行,最基礎也是最經常使用的hook。
SyncBailHook:該hook容許提早退出,當任何掛載的鉤子返回任何函數的時候,則下面的hook都將中止運行。
WaterfallHook:相似於 reduce,若是前一個 Hook 函數的結果 result !== undefined,則 result 會做爲後一個 Hook 函數的第一個參數。
LoopHook:不停的循環執行 Hook,直到全部函數結果 result === undefined。

下面舉個SyncBailHook的使用例子,其餘的就不在一一嘗試了,有興趣的同窗能夠本身嘗試下。

const hook1 = new SyncBailHook(["arg1"]);

hook1.tap("A", arg => {console.log(`A函數 param:${arg}`)});
hook1.tap("B", arg => arg);
hook1.tap("C", arg => {console.log(`C函數 param:${arg}`)});
hook1.tap("D", arg => {console.log(`D函數 param:${arg}`)});
  
hook1.call("sync");
//A函數 param:sync
複製代碼

上面的是同步的,下面咱們來看下異步hook。

AsyncHook

asyncHook

AsyncParalle* 表明異步並行執行鉤子
AsyncSeries* 表明異步串行執行鉤子

而上圖就是一個異步串行鉤子,下面咱們來詳細的說明一下它的使用方法

異步hook比同步接口增長了不少功能。
1.從消息發佈者方法來看能夠獲取到全部訂閱者執行結束後的回調。

hook.callAsync(name,callback)
複製代碼

2.從消息接收方來看增長了幾種監聽的方式

//同步方式執行
hook.tap(name)
//異步方式執行callback方式
hook.tapAsync(name,callback)
//異步方式執行Promise方式
hook.tapPromise(name)
複製代碼

學習了tapable的基本用法思考下面的運行,看看本身對tapable插件的理解~

const testHook = new AsyncSeriesHook(["name"]);

testHook.tap("plugin1", function(name) {
  console.log(name + "我是plugin1");
});
testHook.tapAsync("plugin2", function(name, cb) {
  console.log(name + "我是plugin2");
  setTimeout(() => {
    console.log("plugin2的異步回調開始執行");
    cb();
  }, 2000);
});
testHook.tapPromise("plugin3", function(name, cb) {
  console.log(name + "我是plugin3");
  return new Promise(resolve => setTimeout(resolve, 1000));
});
testHook.callAsync("hello", () => {
  console.log("over");
});
複製代碼

答案:
hello我是plugin1
hello我是plugin2
plugin2的異步回調開始執行
hello我是plugin3
over

1.首先AsyncSeriesHook是一個串行異步函數,支持三種監聽事件方法
2.tap爲同步方法,那麼首先打印出來的應該是 "hello我是plugin1"
3.接來下打印hello我是plugin2,走到第二個hook中,等待兩秒觸發回調打印plugin2的異步回調開始執行
4.緊接着走到第三個hook "hello我是plugin3",一秒以後Promise resolve方法執行
5.這時全部監聽者完成執行顯示 "over"

用插件解決實際問題

給你們介紹咱們這邊的一個小工具carefree,基於webpack插件和服務端Whistle的一套web真機測試解決方案,且套不依賴Wifi熱點~

carefree.jd.com/#/

經過對webpack事件流的理解和對tapable用法學習以後,在開發webpack插件的時候可能對各類hook的理解更深刻一點,下面咱們來看下webpack的功能流程解析,看下webpack是怎樣實現的


Webpack工做流程解析

Webpack的啓動方式有兩種:

  • 既能夠在Terminal終端中直接運行,這種方式最快捷,開箱即用。
  • 也能夠經過require('webpack')引入的方式執行,這種方式最靈活,咱們能夠控制Webpack啓動的時機,也能夠經過Webpack暴露出的鉤子在它的生命週期中作一些事情。

這裏咱們爲了方便對源碼進行調試和理解,使用了第二種方式來啓動Webpack注意這裏咱們使用的webpack版本爲5.0.0-beta.9,因此後面源碼也是這個版本),咱們在根目錄新建一個啓動文件index.js:

const webpack = require(webpack);
const config = require("./webpack.config.js");  // 咱們本身定義的webpack配置
const compiler = webpack(config);
// 因爲啓動webpack的時候沒有傳第二個參數callback,因此須要咱們手動執行run開始編譯
compiler.run((err, stats) => {
	if (err) {
		console.error(err);
	} else {
		console.log(stats);
	}
});
複製代碼

Webpack執行過程實際上是一個串行的過程,這裏先大概瞭解下。以下圖:

Webpack流程

咱們能夠看到整個運行流程可簡單分爲三個大階段,分別是初始化編譯輸出,那麼這裏詳細的介紹下這三個階段會發生什麼事件:

1、初始化階段:

一切從const compiler = webpack(config)開始。

webpack函數源碼(lib/webpack.js):

const webpack = (options, callback) => {
	// options參數就是本地配置文件的參數
	let compiler;
	// 初始化階段開始
	compiler = createCompiler(options);
    // 若是傳入callback函數,則自啓動,不然須要用戶手動執行run
	if (callback) {
		compiler.run((err, stats) => {
			compiler.close(err2 => {
				callback(err || err2, stats);
			});
		});
	}
	return compiler;
};
複製代碼

1.初始化參數:

Webpack最開始運行的時候,會先執行createCompiler並傳入用戶自定義配置參數options,而後會從咱們寫的配置文件和Shell中讀取與合併參數,得出最終的參數options

精簡僞代碼(lib/webpack.js):

const createCompiler = options => {
    // 初始化參數:將用戶本地的配置文件拼接上webpack內置的參數
    options = new WebpackOptionsDefaulter().process(options);
    ...
}
複製代碼

2.實例化Compiler:

用上一步獲得的參數初始化Compiler實例,CompilerWebpack的指揮官,負責並貫穿了整個打包生產線。在Compiler實例中包含了完整的Webpack環境信息,全局只有一個Compiler實例。

精簡僞代碼(lib/webpack.js):

const createCompiler = options => {
    ...
    // 用options參數實例化compiler,負責文件監聽和啓動編譯;
    const compiler = new Compiler(options.context);
    ...
}
複製代碼

3.掛載用戶自定義插件:

開始掛載咱們在配置文件中使用的plugins,這裏會判斷是否爲函數,若是是函數直接調用,反之則會調用對象的apply方法(這就是爲何Webpack官方限制咱們的插件只能用這兩種方式調用的緣由)。同時向插件傳入compiler實例的引用,以方便在插件內部經過Compiler調用Hook,使插件在任意事件節點執行,還能獲取Webpack環境的配置。

精簡僞代碼(lib/webpack.js):

const createCompiler = options => {
    ...
	// 掛載咱們本身配置的插件
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
    }
    ...
}
複製代碼

4.掛載內置插件和處理入口文件:

掛載完用戶自定義的插件以後,開始掛載Webpack內置的插件,將內置的插件註冊到不一樣的Hook上。

精簡僞代碼(lib/webpack.js):

const createCompiler = options => {
    ...
    // 掛載webpack內置插件
    compiler.options = new WebpackOptionsApply().process(options, compiler);
    ...
}
複製代碼

WebpackOptionsApply類的工做就是初始化內置插件,裏邊會判斷不少不一樣的狀況來加載不一樣的插件。

精簡僞代碼(lib/WebpackOptionsApply.js):

class WebpackOptionsApply extends OptionsApply {
	constructor() {
		super();
	}
	process(options, compiler) {
    // 當傳入的配置信息知足要求,處理與配置項相關的邏輯
    if(options.target) {
        new OnePlugin().apply(compiler);
    }
    if(options.devtool) {
        new AnotherPlugin().apply(compiler);
    }
	new JavascriptModulesPlugin().apply(compiler);
	new JsonModulesPlugin().apply(compiler);
	...
    // 註冊entryoption插件
    new EntryOptionPlugin().apply(compiler);
    // 觸發entry-option: 讀取entry的配置,找出全部入口文件,而後爲每一個入口文件掛上make的Hook
	compiler.hooks.entryOption.call(options.context, options.entry);
	...
    // 觸發afterPlugins: 調用完全部的內置和自定義插件的apply方法
    compiler.hooks.afterPlugins.call(compiler);
	...
    // 觸發afterResolvers:根據配置初始化resolver:resolver負責在文件系統中尋找指定路徑的文件
	compiler.hooks.afterResolvers.call(compiler);
	return options;
	}
}
複製代碼

而後這裏還會根據entry配置找出全部的入口文件,若是entry是數組說明是多入口,會循環遍歷每個入口處理,若是是函數,說明是異步加載入口,那麼使用異步加載的plugin處理,DynamicEntryPlugin其實就比EntryPlugin多了個使用Promise異步加載入口文件的操做。

精簡僞代碼(lib/EntryOptionPlugin.js):

module.exports = class EntryOptionPlugin {
	apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
			const applyEntryPlugins = (entry, name) => {
				if (typeof entry === "string") {
					new EntryPlugin(context, entry, name).apply(compiler);
				} else if (Array.isArray(entry)) {
					// 若是是多入口會遍歷全部入口
					for (const item of entry) {
						applyEntryPlugins(item, name);
					}
				}
			};

			if (typeof entry === "string" || Array.isArray(entry)) {
				applyEntryPlugins(entry, "main");
			} else if (typeof entry === "object") {
				for (const name of Object.keys(entry)) {
					applyEntryPlugins(entry[name], name);
				}
			} else if (typeof entry === "function") {
				// 若是是異步加載入口,則使用異步加載處理
				new DynamicEntryPlugin(context, entry).apply(compiler);
			}
			return true;
		});
	}
};
複製代碼

入口文件使用EntryPlugin進行處理,給每一個入口文件掛上compilation鉤子,而且給入口文件綁定上模塊工廠,而後還給每一個入口文件掛上make鉤子,等待編譯階段使用模塊工廠將入口文件及其依賴轉換爲JS模塊

精簡僞代碼(lib/EntryPlugin.js):

class EntryPlugin {
	constructor(context, entry, name) {
		this.context = context;
		this.entry = entry;
		this.name = name;
	}

	apply(compiler) {
		// 給每一個入口文件註冊compilation鉤子
		compiler.hooks.compilation.tap(
			"EntryPlugin",
			(compilation, { normalModuleFactory }) => {
				compilation.dependencyFactories.set(
					EntryDependency,
					normalModuleFactory
				);
			}
		);
		// 給每一個入口文件註冊make鉤子
		compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
			const { entry, name, context } = this;
			const dep = EntryPlugin.createDependency(entry, name);
			compilation.addEntry(context, dep, name, err => {
				callback(err);
			});
		});
	}
}
複製代碼

process函數執行完,Webpack將全部它關心的Hook消息都註冊完成,等待後續編譯過程當中挨個觸發。

以上就是Webpack的初始化階段,這個階段的主要任務是整合配置參數options,初始化Compiler對象,掛載全部的Plugin,註冊全部的Hook。另外還有一些小插曲好比引入了Node文件系統fs插件(主要是爲了接下來輸出打包文件作準備)、初始化resolver(在文件系統中尋找指定路徑的文件)。


2、編譯階段:

1.開跑:

執行compile.run讓編譯階段跑起來,run函數裏邊會定義一個編譯完成以後的回調函數,這個函數的做用就是將編譯後的內容生成文件。咱們能夠看到首先是判斷是否編譯成功,未成功則直接觸發done事件結束編譯。成功則開始打包文件。而後前後觸發了beforeRunrun事件,這兩個事件會綁定文件讀取對象和開啓緩存插件CachePlugin,而後會開始讀取入口文件,得到入口文件內容以後就開始執行this.compile()開始編譯了。

精簡僞代碼(lib/Compiler.js):

class Compiler {
	...
	// 整個run的過程
	run(callback) {
		const onCompiled = (err, compilation) => {
			// 編譯失敗
			if (this.hooks.shouldEmit.call(compilation) === false) {
				const stats = new Stats(compilation);
				stats.startTime = startTime;
				stats.endTime = Date.now();
				this.hooks.done.callAsync(stats, () => {
					this.hooks.afterDone.call(stats);
				});
				return;
			}
			process.nextTick(() => {
				// 輸出文件
				this.emitAssets(compilation, () => {
					// 觸發done事件
					this.hooks.done.callAsync(stats, () => {
						// 觸發咱們手動執行run函數的時候傳入的回調
						callback(stats);
						// 觸發afterDone事件
						this.hooks.afterDone.call(stats);
					});
				});
			});
		};
		// 觸發beforeRun事件,綁定文件讀取對象
		this.hooks.beforeRun.callAsync(this, () => {
			// 觸發run事件,會啓動編譯緩存插件CachePlugin,提升編譯效率
			this.hooks.run.callAsync(this, () => {
				// 讀取入口文件內容(readFile)
				this.readRecords(() => {
					// 開始編譯,並傳入編譯完成後的回調函數
					this.compile(onCompiled);
				})
			})
		})
	}
}
複製代碼

this.compile()會先初始化模塊工廠ModuleFactory並存入loader的配置,爲解析、轉換模塊作準備。而後會觸發compile事件告訴插件一次新的編譯即將開始。

精簡僞代碼(lib/Compiler.js):

class Compiler {
	newCompilationParams() {
		const params = {
			normalModuleFactory: this.createNormalModuleFactory(),
			contextModuleFactory: this.createContextModuleFactory()
		};
		return params;
	}
	compile(callback) {
		// 初始化模塊工廠
		const params = this.newCompilationParams();
		...
	}
}
複製代碼
2.初始化compilation:

等等,咱們好像還缺乏一位很是重要的對象,那就是Compilation了,前面咱們提到了Compiler,它是負責整條生產線,就像是一位指揮官,指揮着Webpack的各項工做,那麼Compilation就專一於一個產品的生產,咱們每編譯一次,都會從新初始化一個Compilation,它包含了當前編譯的環境信息,接着觸發make鉤子,真正開始編譯。

精簡僞代碼(lib/Compiler.js):

class Compiler {
	compile(callback) {
		...
		// 觸發compile事件告訴插件一次新的編譯將要啓動
		this.hooks.compile.call(params);
		// 初始化compilation,compilation對象表明了一次單一的版本構建和生成資源過程
		const compilation = this.newCompilation(params);
		// 觸發make事件,開始編譯
		this.hooks.make.callAsync(compilation, () => {
			...
		})
	}
	
	createCompilation() {
		return new Compilation(this);
	}

	newCompilation(params) {
		const compilation = this.createCompilation();
		compilation.name = this.name;
		compilation.records = this.records;
		this.hooks.thisCompilation.call(compilation, params);
		this.hooks.compilation.call(compilation, params);
		return compilation;
	}
}
複製代碼
3.開始編譯:

編譯階段此時進入主場,首先會把每個入口文件交給Compilation,而後執行addEntry

精簡僞代碼(lib/EntryPlugin.js):

compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
	const { entry, name, context } = this;  // 這裏的this是入口文件
	const dep = EntryPlugin.createDependency(entry, name);
	// 把入口文件交給compilation
	compilation.addEntry(context, dep, name, err => {
		callback(err);
	});
});
複製代碼

在addEntry中會執行addModuleChain處理每一個入口文件。而後會使用模塊工廠把入口文件轉換成模塊實例(NormalModule),這個過程調用鏈很是長,爲了方便理解,把執行過程進行了精簡。

精簡僞代碼(lib/EntryPlugin.js):

addEntry(context, entry) {
	...
	this._addModuleChain(context, entry);
}
_addModuleChain(context, dependency) {
	...
	const Dep = dependency.constructor;
	// 建立入口文件的模塊工廠
	const moduleFactory = this.dependencyFactories.get(Dep);
	// 建立入口模塊
	moduleFactory.create(
		{
			contextInfo: {
				issuer: "",
				compiler: this.compiler.name
			},
			context: context,
			dependencies: [dependency]
		},
		({ module }) => {
			// 建立完畢以後運行loader
			this.buildModule(module);
		}
	);
}
buildModule(module) {
	...
	// 開始編譯,build會執行dubuild來運行各個loader
	module.build(
		this.options,
		this,
		this.resolverFactory.get("normal", module.resolveOptions),
		this.inputFileSystem
	);
}
複製代碼

入口模塊構建完畢以後,會執行doBuild,其實doBuild就是選用合適的loader去加載resource。目的是爲了將這份resource轉換爲JS模塊(緣由是webpack只識別JS模塊)。最後返回加載後的源文件source,以便接下來繼續處理。

精簡僞代碼(lib/NormalModule.js):

const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback) {
	// runLoader從包'loader-runner'引入的方法
	runLoaders({
		resource: this.resource, // 這裏的resource多是js文件,多是css文件,多是img文件
		loaders: this.loaders
	}, (err, result) => {
		const source = result[0];
		const sourceMap = result.length >= 1 ? result[1] : null;
		const extraInfo = result.length >= 2 ? result[2] : null;
		...
	})
}
複製代碼

咱們處理完入口文件以後,還有入口文件的依賴以及依賴的依賴沒有處理,這個時候就須要使用Parser(acorn)將入口文件JS代碼轉換爲標準的AST(抽象語法樹),而後對這個AST進行分析,當遇到require或者import等導入其餘模塊的語句的時候,便將這個模塊加入到module.dependencies中,同時對新找出的依賴進行遞歸分析,最終弄清楚全部模塊的依賴關係,這樣就能生成一顆完整的依賴樹(依賴圖集moduleGraph)。

精簡僞代碼(lib/javascript/JavascriptParser.js):

parse(code, options) {
	// 調用第三方插件'acorn'解析js模塊
	let ast = acorn.parse(code);
	// 省略部分代碼
	if(this.hooks.program.call(ast, comments) === undefined) {
		this.detectStrictMode(ast.body);
		this.prewalkStatements(ast.body);
		this.blockPrewalkStatements(ast.body);
		// 這裏webpack會遍歷一次ast.body,其中會手機這個模塊的全部依賴項,最後寫入到'module.dependencies'中
		this.walkStatements(ast.body);
	}
}
複製代碼

至此,全部源碼已經編譯完成,而且保存在內存中(compilation.modules),等待打包並輸出。

以上就是Webpack的編譯階段,這個階段的主要任務是初始化模塊工廠,初始化compilation,而後調用loader進行編譯,轉換AST,生成依賴圖集。另外還有一些小插曲好比綁定文件讀取對象,調用了Cache插件(編譯緩存,提升編譯效率)。


3、輸出階段:

1.重組源碼:

make事件結束後,開始執行回調compilation.seal(),開始打包封裝模塊,這裏會執行compilation.createChunkAssets方法(在執行的時候會優先讀取cache中是否已經有了相同hash的資源,若是有,則直接返回內容,不然纔會繼續執行模塊生成的邏輯,並存入cache中)生成須要進行輸出的chunk資源。 這裏會先調用getRenderManifest獲取輸出列表,裏邊每一項都包含一個須要打包輸出的資源及信息。而後會將AST轉換回JS代碼並使用對應的模板進行拼接,而後把拼接好的內容根據文件名保存在Compilation.assets中,以備以後進行文件輸出。

精簡僞代碼(lib/Compilation.js):

createChunkAssets(callback) {
	// 獲取輸出列表,包含每個須要輸出的資源信息
	let manifest = this.getRenderManifest();
	for (const fileManifest of manifest) {
		// 將AST轉換回JS代碼而後根據模板拼接好代碼
		source = fileManifest.render();
		// 將最後的代碼內容放到compilation.assets中,準備生成文件
		this.emitAsset(file, source, assetInfo);
	}
}
複製代碼
2.輸出完成:

在seal執行結束後,全部模塊打包完畢並保存在內存中(Compilation.assets),是時候將它們輸出爲文件了。接下來就是一連串的callback回調,最後咱們到達了compiler.emitAssets方法體中,而後會先觸發emit事件,根據webpack.config.js文件的output配置的path屬性,將文件輸出到指定的文件夾。至此,你就能夠在dist中查看到打包後的文件了。

精簡僞代碼(lib/Compiler.js):

emitAssets(compilation, callback) {
	let outputPath;
	this.hooks.emit.callAsync(compilation, () => {
		// 找到輸出文件路徑
		outputPath = compilation.getPath(this.outputPath, {});
		// 將compilation.assets輸出到指定路徑
		mkdirp(this.outputFileSystem, outputPath, compilation.getAssets());
	})
}
複製代碼

以上就是Webpack的輸出階段,這個階段的主要任務是拿到轉換後的結果和依賴關係以後,將模塊組合成一個個Chunk,而後會根據Chunk類型使用對應的模板生成最終要輸出的文件內容,最後將內容輸出到硬盤裏。

webpack的工程化應用

webpack專一於模塊化編譯,現在衆多vue、react項目都是基於它進行打包編譯,你能夠爲你的團隊搭建一個針對vue、react技術棧且具備開發、測試、上線等工做流的腳手架,可是從零開始搭建可能須要花費你幾天的時間。 如今已經誕生了不少前端腳手架構建工具,這裏以Gaea-cli爲例,它是基於webpack、Node.js實現的腳手架搭建工具,也包含了開發、測試、打包等完整的前端工做流,具備合理的webpack默認配置,同時暴露出webpack配置文件讓用戶本身配置額外的插件。經過命令行初始化項目的時候還能選擇你喜歡的UI框架,好比NutUI、ElementUI等,也能夠經過配置不一樣的webpack插件來爲你的腳手架提供更多的功能,好比CareFree、圖片壓縮、PWA、Smock等,還能選擇是否須要支持Ts、Vuex、Eslint等配置。 這些構建工具均可以經過簡單選擇、零配置搭建起來一個vue、react腳手架,這樣你能夠專一在撰寫應用上,而沒必要花好幾天去糾結配置的問題了,趕快用起來吧,真香!

總結

Webpack的總體架構是一個插件的形勢,經過Tapable實現事件流,它的整個工做流程都是經過事件拼接起來的,而事件流可使插件在不一樣的流程階段執行,Webpack的事件流機制保證了插件的有序性,使得整個系統擴展性很好。

介於node.js單線程的壁壘,Webpack構建慢一直是它的短板(這也是Happypack之因此能大火的緣由,這裏Happypack是利用node.js原生的cluster模塊去開闢多進程進行構建,webpack4已經集成)。由於每個模塊,都會通過Loader -> js(String) -> AST -> js(String) 的過程,在Webapck裏,只有模塊!

現在前端項目都使用模塊化的思想來開發,Webpack也剛好是針對模塊化開發的自動化構建工具,再加上它強大的擴展性使它使用場景很是的普遍,不火也不行啊!

相關文章
相關標籤/搜索