webpack系列之三resolve

滴滴WebApp架構組發表於2019-02-20

作者:崔靜

介紹

webpack 的特點之一是處理一切模組,我們可以將邏輯拆分到不同的檔案中,然後通過模組化方案進行匯出和引入。現在 ES6 的 Module 則是大家最常用的模組化方案,所以你一定寫過 import './xxx' 或者 import 'something-in-nodemodules' 再或者 import '@/xxx'(@ 符號通過 webpack 配置中 alias 設定)。webpack 處理這些模組引入 import 的時候,有一個重要的步驟,就是如何正確的找到 './xxx''something-in-nodemodules' 或者 '@/xxx' 等等對應的是哪個檔案。這個步驟就是 resolve 的部分需要處理的邏輯。

其實不僅是針對原始碼中的模組需要 resolve,包括 loader 在內,webpack 的整體處理過程中,涉及到檔案路徑的,都離不開 resolve 的過程。

同時 webpack 在配置檔案中有一個 resolve 的配置,可以對 resolve 的過程進行適當的配置,比如設定副檔名,查詢搜尋的目錄等(更多的參考官方介紹)。

下面,將主要介紹針對普通檔案的 resolve 流程 和 loader 的 resolve 主流程。

resolve 主流程介紹

首先先準備一個簡單的 demo

import { A } from './a.js'
複製程式碼

然後針對這個 demo 來看主流程。在 webpack 系列之一總覽 文章中有一個 webpack 編譯總流程圖,圖中可以看到在 webpack 處理每一個檔案開始之前都會有一個 resolve 的過程,找到完整的檔案路徑資訊。

webpack編譯流程

webpack 原始碼中 resolve 流程開始的入口在 factory 階段, factory 事件會觸發 NormalModuleFactory 中的函式。先放一張粗略的總體流程圖,在深入原始碼前現有一個大概的框架圖

resolve總覽

接下來我們就從 NormalModuleFactory.js 檔案中開始看起

this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
    // 首先得到 resolver
	let resolver = this.hooks.resolver.call(null);

	// Ignored
	if (!resolver) return callback();

   // 執行
	resolver(result, (err, data) => {
		if (err) return callback(err);

		// Ignored
		if (!data) return callback();

		// direct module
		if (typeof data.source === "function") return callback(null, data);

		this.hooks.afterResolve.callAsync(data, (err, result) => {
			//... resolve結束後流程,此處省略
		});
	});
});
複製程式碼

第一步獲得 resolver 邏輯比較簡單,觸發 resolver 事件(SyncWaterfallHook型別的Hook,關於Hook的型別,可以參考上一篇文章),同時 NormalModuleFactory 中註冊了 resolver 事件。下面是 resolver 事件的程式碼,可以看到返回了一個函式。

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  //...先展示省略具體內容,後面會詳細解釋。
})
複製程式碼

因此 this.hooks.resolver.call(null); 結束後,將得到一個函式。然後接下來就是執行該函式獲得 resolver 結果。 resolver 函式中,從整體看分為兩大主要流程 loader 和 檔案。

loader流程

  1. 獲取到 inline loader 的 request 部分。例如,針對如下寫法
import Styles from 'style-loader!css-loader?modules!./styles.css';
複製程式碼

會從中解析出 style-loadercss-loader。由於此步驟只是為了解析出路徑,所以對於 loader 的配置部分並不關心。

  1. 得到 loader 型別的 resolver 處理例項,即 const loaderResolver = this.getResolver("loader");

  2. 對每一個 loader 用 loaderResolver 依次處理,得到執行檔案的路徑。

檔案流程

  1. 得到普通檔案的 resolver 處理例項,即程式碼 const normalResolver = this.getResolver("normal", data.resolveOptions);

  2. 用 normalResolver 處理檔案,得到最終檔案絕對路徑

下面是具體的 resolver 程式碼:

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
	const contextInfo = data.contextInfo;
	const context = data.context;
	const request = data.request;
   
   // ... 省略部分和 loader 處理相關的程式碼
   // 處理 inline loaders,拿到 loader request 部分(loader 的名稱或者 loader 的路徑,由於這裡不關係 loader 的配置等其他細節,所以直接將開頭的 -!, 和 ! 直接替換掉,將多個 ! 替換成一個,方便後面處理)
	let elements = request
		.replace(/^-?!+/, "")
		.replace(/!!+/g, "!")
		.split("!");
	let resource = elements.pop();
	// 提取出具體的 loader
	elements = elements.map(identToLoaderRequest);

	const loaderResolver = this.getResolver("loader");
	const normalResolver = this.getResolver("normal", data.resolveOptions);

	asyncLib.parallel(
		[
			callback =>
				this.resolveRequestArray(
					contextInfo,
					context,
					elements,
					loaderResolver,
					callback
				),
			callback => {
				if (resource === "" || resource[0] === "?") {
					return callback(null, {
						resource
					});
				}

				normalResolver.resolve(
					contextInfo,
					context,
					resource,
					{},
					(err, resource, resourceResolveData) => {
						if (err) return callback(err);
						callback(null, {
							resourceResolveData,
							resource
						});
					}
				);
			}
		],
		(err, results) => {
		  // ... reslover callback
		})
	)
})
		
複製程式碼

結合上面的步驟和程式碼看,其實 loader 類和普通檔案型別(後面稱為 normal 類),大致流程是相似的。我們先看獲取不同型別的 resolver 例項部分。

獲取不同型別 resolver 處理例項

getResolver 函式,會呼叫到 webpack/lib/ResolverFactory.js 中的 get 方法。該方法中獲取 resolver 例項的具體流程如下圖。

獲取resolver例項

上圖中,首先根據不同 type 獲取 options 。那麼這些 options 配置都存在哪裡呢?

webpack中options配置

webpack 直接對外暴露的 resolve 的配置,在配置檔案中 resolve 和 resolveLoader 部分,詳細的欄位見官網。但是其內部會有一個預設的配置,在 webpack.js 入口處理函式中,初始化了所有的預設配置

// ...
if (Array.isArray(options)) {
	compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
	options = new WebpackOptionsDefaulter().process(options);
	compiler = new Compiler(options.context);
	compiler.options = options;
// ...
複製程式碼

WebpackOptionsDefaulter() 中,配置了很多關於 resolve 和 resolveLoader 的配置。process 方法將我們寫的 webpack 的配置 和預設的配置合併。

// WebpackOptionsDefaulter.js 檔案
//...
this.set("resolve", "call", value => Object.assign({}, value));
this.set("resolve.unsafeCache", true); // 預設開啟快取
this.set("resolve.modules", ["node_modules"]); // 預設從 node_modules 中查詢
// ...
複製程式碼

webpack.js 中,接下來有一句

new WebpackOptionsApply().process(options, compiler);
複製程式碼

其中 process 過程裡會注入關於 normal/context/loader 的預設配置的獲取函式。

  compiler.resolverFactory.hooks.resolveOptions
  	.for("normal")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem
  			},
  			options.resolve,
  			resolveOptions
  		);
  	});
  compiler.resolverFactory.hooks.resolveOptions
  	.for("context")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem,
  				resolveToContext: true
  			},
  			options.resolve,
  			resolveOptions
  		);
  	});
  compiler.resolverFactory.hooks.resolveOptions
  	.for("loader")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem
  			},
  			options.resolveLoader,
  			resolveOptions
  		);
  	});
複製程式碼

options 介紹到此先結束,我們繼續沿著上面流程圖往下看。當獲取到 resolver 例項後,就開始 resolver 的過程:根據型別的不同,會有 normalResolver 和 loaderResolver,同時在 normalResolver 中會區分檔案和 module。

webpack 中有很多針對路徑的配置,例如 alias, extensions, modules 等等,node.js 中的 require 已經無法滿足 webpack 對路徑的解析的要求。因此,webpack 封裝出一個單獨的庫 enhanced-resolve,專門用來處理各種路徑的解析,仍然採用了 webpack 的外掛模式來組織程式碼。 接下來會深入到這個庫中,依次介紹普通檔案、module 和 loader 的處理過程(webpack 中還有一個 context 的 resolve 過程,由於其過程沒太多特別之處,放在 module 過程中一起介紹)。先看普通檔案的處理過程。

普通檔案的 resolve 過程

普通檔案 resolver 處理入口為 webpack 中 normalResolver.resolve 方法,而整個 resolve 過程可以看成事件的串聯,當所有串聯在一起的事件執行完之後,resolve 就結束了。

resolve事件環

將這些事件一個一個串聯起來的關鍵部分在 doResolve 和每個事件的處理函式中。這裡以 doResolve 和呼叫的 UnsafePlugin 為例,看一下銜接的過程。

// 第一個引數 hook,函式中用到的 hook 是通過引數傳進來的。
doResolve(hook, request, message, resolveContext, callback) {
	// ...
	// 生成 context 棧。
	const stackLine = hook.name + ": (" + request.path + ") " +
		(request.request || "") + (request.query || "") +
		(request.directory ? " directory" : "") +
		(request.module ? " module" : "");

	let newStack;
	if(resolveContext.stack) {
		newStack = new Set(resolveContext.stack);
		if(resolveContext.stack.has(stackLine)) {
			// Prevent recursion
			const recursionError = new Error("Recursion in resolving\nStack:\n  " + Array.from(newStack).join("\n  "));
			recursionError.recursion = true;
			if(resolveContext.log) resolveContext.log("abort resolving because of recursion");
			return callback(recursionError);
		}
		newStack.add(stackLine);
	} else {
		newStack = new Set([stackLine]);
	}
	// 簡單的demo中這裡沒有事件註冊,先忽略
	this.hooks.resolveStep.call(hook, request);

    // 如果該hook有註冊過事件,則調觸發該 hook
	if(hook.isUsed()) {
		const innerContext = createInnerContext({
			log: resolveContext.log,
			missing: resolveContext.missing,
			stack: newStack
		}, message);
		return hook.callAsync(request, innerContext, (err, result) => {
			if(err) return callback(err);
			if(result) return callback(null, result);
			callback();
		});
	} else {
		callback();
	}
}
複製程式碼

呼叫到 hook.callAsync 時,進入 UnsafeCachePlugin,然後看 UnsafeCachePlugin 中部分實現:

class UnsafeCachePlugin {
	constructor(source, filterPredicate, cache, withContext, target) {
		this.source = source;
       // ... 省略部分
		this.target = target;
	}
	apply(resolver) {
	   // ensureHook 主要邏輯:如果 resolver 已經有對應的 hook 則返回;如果沒有,則會給 resolver 增加一個 this.target 型別的 hook
		const target = resolver.ensureHook(this.target);
		// getHook 會根據 this.source 字串獲取對應的 hook
		resolver.getHook(this.source).tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
		   //... 先省略 UnsafeCache 中其他邏輯,只看銜接部分
			// 繼續呼叫 doResolve,但是注意這裡的 target 
			resolver.doResolve(target, request, null, resolveContext, (err, result) => {
				if(err) return callback(err);
				if(result) return callback(null, this.cache[cacheId] = result);
				callback();
			});
		});
	}
}
複製程式碼

UnsafeCachePlugin 分為兩部分:事件註冊(new 和 執行apply) 和事件執行(resolver.getHook(this.source).tapAsync 的回撥部分)。事件註冊階段發在 webpack 獲取不同型別 resolve 處理例項時(前面獲取不同型別 resolver 處理例項小節中,getResolver 的時候),這時會傳入一個 source 值(字串型別)和一個 target 值(字串型別),程式碼如下

// source 值為 resolve,target 值為 new-resolve
new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve")`
//...然後會呼叫 apply 方法
複製程式碼

apply 中,將 UnsafeCachePlugin 的處理邏輯註冊為 source 事件的回撥,同時確保 target 事件的存在(如果沒有則註冊一個)。

事件執行階段,完成 UnsafeCachePlugin 本身的邏輯之後,遞迴呼叫 resolver.doResolve(target, ...),這時第一個引數為 UnsafeCachePlugin 中的 target 事件。如此,再進入到 doResolve 之後,再觸發 target 的事件,這樣就形成了事件流。而整體的呼叫過程,簡化來看整體邏輯就是:

doResolve(target1) 
  -> target1 事件(srouce:target1, target: target2) 
  -> 遞迴呼叫doResolve(target2) 
     -> target2 事件(srouce:target2, target: target3) 
     -> 遞迴呼叫doResolve(target3) 
        -> target3 事件(srouce:target3, target: target4) 
        ...
        ->遇到遞迴結束標識,結束遞迴
     
複製程式碼

通過對 doResolve 的遞迴呼叫,事件之間就銜接了起來,形成完整的處事件流,最終得到 resolve 結果。在 ResolverFactory.js 檔案的 createResolver 方法中各個 plugin 的註冊方法,決定了整個 resolve 的事件流。

exports.createResolver = function(options) {
    // ...
	// 根據 options 中條件的不同,加入各種 plugin
	if(unsafeCache) {
		plugins.push(new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve"));
		plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
	} else {
		plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
	}
    // ... plugin 加入的程式碼
	plugins.forEach(plugin => {
		plugin.apply(resolver);
	});
	// ...
複製程式碼

上面程式碼整理一下,可以得到完整的事件流圖(下圖為簡化版本,完成版本附圖)

resolve事件流簡版

結合上面的圖和 demo,我們來一步一步看這個事件流中每一環都做了什麼。(ps:下面步驟中,會涉及到 request 引數,這個引數貫穿所有事件處理邏輯,儲存了整個 resolve 的資訊)

  1. UnsafeCachePlugin

增加一層快取,由於 webpack 處理打包的過程中,涉及到大量的 resolve 過程。所以需要增加一層快取,提高效率。webpack 預設會啟用 UnsafeCache。

  1. ParsePlugin

    初步解析路徑,判斷是否為 module/directory/file,結果儲存到 request 引數中。

  2. DescriptionFilePlugin 和 NextPlugin

    DescriptionFilePlugin 中會尋找描述檔案,預設會尋找 package.json。首先會在 request.path 這個目錄下尋找,如果沒有則按照路徑一層一層往上尋找。最後讀取到 package.json 的資訊和其所在的目錄/路徑資訊,存入 request 中。我們在 demo 的根目錄有 package.json 檔案,所以這裡會獲取到根目錄的檔案。

    NextPlugin 起一個銜接的作用,內部邏輯就是直接呼叫 doResolve,然後觸發下一個事件。當 DescriptionFilePlugin 中未找到 package.json 檔案時,會進入 NextPlugin,然後讓事件流繼續。

  3. AliasPlugin/AliasFieldPlugin

這一步開始處理別名,由於 AliasFieldPlugin 中依賴於 package.json 的配置,所以這一步放在了 DescriptionFilePlugin 之後。 除了我們在配置檔案中寫一些別名外,webpack 還會有一些自帶的 alias;每一個 alias 配置,都會註冊一個函式。這一步將執行所有的函式,一一對比。 若命中某一 alias 的配置或者 aliasField,那麼就會進入上圖紅色虛線的分支。用新的別名替換 request 引數內容,然後再次開始 resolve 過程。 沒有命中,則進入下一個處理函式 ModuleKindPlugin

  1. ModuleKindPlugin

根據 request.module 的值走不同的分支。如果是 module,則後續進入 rawModule 的邏輯。前面 ParsePlugin 中得到的結果中 request.modulefalse,所以這裡返回 undefined,繼續進入下一個處理函式。

  1. JoinRequestPlugin

將 request 中 path 和 request 合併起來,將 request 中 relativePath 和 request 合併起來,得到兩個完整的路徑。在這個 demo 中會得到 /Users/didi/dist/webpackdemo/webpack-demos/demo01/a.js./demo01/a.js

  1. DescriptionFilePlugin

這時會再次進入 DescriptionFilePlugin 。不過與第一次進入時不同之處在於,此時的 request.path 變成了 /dir/demo/a.js`。由於 path 改變了,所以需要再次查詢一下 package.json

隨後觸發 describedRelative 事件,進入下一個流程

  1. FileKindPlugin

判斷是否為一個 directory,如果是則返回 undefined, 進入下一個 tryNextPlugin,這時會進入 directory 的分支。否則,則表明是一個檔案,進入 rawFile 事件。我們的 demo 中,這裡將走向 rawFile 分支。

  1. TryNextPlugin/ConcordExtensionsPlugin/AppendPlugin

由於 webpack 中預設的 enforceExtension 值為 true,所以這裡會進入 TryNextPlugin,同時 enableConcord 為 false,不會有 ConcordExtensionsPlugin。

TryNextPlugin 和 NextPlugin 類似,起一個銜接的作用,內部邏輯就是直接呼叫 doResolve,然後觸發下一個事件。所以在這個階段會直接走到觸發 file 事件的分支。 當 TryNextPlugin 有返回,且返回為 undefined 。這時意味著沒有找到 request.path 所對應的檔案,那麼會繼續執行後續的 AppendPlugin。

AppendPlugin 主要邏輯:webpack 會設定 resolve.extensions 引數(配置中設定或者使用 webpack 預設的),AppendPlugin 會給 request.path 和 request.relativePath 逐一新增這些字尾,然後進入 file 分支,繼續事件流程。

  1. AliasPlugin/AliasFields/ConcorModulesPlugin/SymlinkPlugin

這時會再次進入到 Alias 的處理邏輯,注意在此步中 webpack 內部自帶的很多 Alias 不會再有。 與前面相同,這裡依然沒有 ConcorModulesPlugin SymlinkPlugin 用來處理路徑中存在 link 的情況。由於 webpack 預設是按照真實的路徑來解析的,所以這裡會檢查路徑中每一段,如果遇到 link,則替換為真實路徑。由於 path 改變了,所以會再回到 relative 階段。 若路徑中沒有 link,則進入 FileExistsPlugin

  1. FileExistsPlugin

讀取 request.path 所在的檔案,看檔案是否存在。檔案存在則進入到 existingFile 事件。

  1. NextPlugin/ResultPlugin

通過 NextPlugin 銜接,再進入 Resolved 事件。然後執行 ResultPlugin,到此 resolve 整個流程就結束了,request 儲存了 resolve 的結果。

module 的 resolve 過程

在 webpack 中,我們除了會 import 一個檔案以外,還會 import 一個模組,比如 import Vue from 'vue'。那麼這時候,webpack 就需要正確找到 vue 所對應的入口檔案在哪裡。針對 vue,ParsePlugin 結果中 request.module = true,隨後在 ModuleKindPlugin 就會進入上面圖中 rawModule 的分支。我們就以 import Vue from 'vue' 為 demo,看一下 rawModule 分支流程。

  1. ModuleAppendPlugin/TryNextPlugin

ModuleAppendPlugin 和上面的 AppendPlugin 類似,新增字尾。 TryNextPlugin 進入 module 事件

  1. ModulesInHierachicDirectoriesPlugin/ModulesInRootPlugin

ModulesInHierachicDirectoriesPlugin 中會依次在 request.path 的每一層目錄中尋找 node_modules。例如 request.path = 'dir/demo' 那麼尋找 node_modules 的過程為:

dir/demo/node_modules
dir/node_modules
/node_modules
複製程式碼

如果 dir/demo/node_modules 存在,則修改 request.path 和 request.request

const obj = Object.assign({}, request, {
  	path: addr, // node_module 所在的路徑
  	request: "./" + request.request
  });
複製程式碼

對於 ModulesInRootPlugin,則預設為在根目錄下尋找,直接進行替換

  const obj = Object.assign({}, request, {
  	path: this.path,
  	request: "./" + request.request
  });
複製程式碼

隨後,由於改變了 request.path 和 request.request,所以重新回到 resolve 開始的階段。但是這時 request.request 從一個 module 變成了一個普通檔案型別./vue

  1. 與普通檔案 resolve 過程分叉點

按照普通檔案的方式查詢 dir/demo/node_module/vue 的過程與前文中普通檔案 resolve 過程類似,經歷上一節中 1-7 的步驟,然後觸發 describedRelative 事件(這個事件下注冊了兩個函式 FileKindPlugin 和 TryNextPlugin)。 首先進入 FileKindPlugin 的邏輯,由於 dir/demo/node_module/vue 不是一個檔案地址,所以在第 8 步 FileKindPlugin 中最終會返回 undefined。 這時候會進入下一個處理事件 TryNextPlugin,然後觸發 directory 事件,把 dir/demo/node_module/vue 按照資料夾的方式來解析。

  1. DirectoryExisitsPlugin

確認 dir/demo/node_module/vue 是否存在。(ps: 針對 context 的 resolve 過程,到這裡如果資料夾存在,則就結束了。)

  1. MainFieldPlugin

webpack 預設的 mainField 為 ['browser', 'module', 'main']。這裡會按照順序,在 dir/demo/node_module/vue/package.json 中找對應欄位。 vue 的 package.json 中定義了

{
  "module": "dist/vue.runtime.esm.js"
}
複製程式碼

所以找到該欄位後,會將 request.request 的值替換為 ./dist/vue.runtime.esm.js。之後又回到 resolve 節點,開始新一輪,尋找一個普通檔案 ./dist/vue.runtime.esm.js 的過程。 當 MainFieldPlugin 執行完,都沒有結果時,會進入 UseFilePlugin

  1. UseFilePlugin

當我們 package.json 中沒有寫 browser、module、main 時,webpack 會自動去找目錄下的 index 檔案,request 變成如下

{
  //...省略其他部分
  relativePath: "./index",
  path: 'dir/demo/node_modules/vue/index'
}
複製程式碼

然後觸發 undescribedRawFile 事件

  1. DescriptionFilePlugin/TryNextPlugin

針對新的 request.path ,重新尋找描述檔案,即 package.json

  1. AppendPlugin

依次為 'dir/demo/node_modules/vue/index' 新增字尾名,然後尋找該檔案是否存在。與前文中 file 之後的流程相同。直到最後找到存在的檔案,整個針對 module 的 resolve 過程就結束了。

loader 的 resolve 過程

loader 的 resolve 過程和 module 的過程類似,我們以 url-loader 為例,入口在 NormalModuleFactory.js 中 resolveRequestArray 函式。這裡會執行 resolver.resolve,這裡的 resolver 為之前得到的 loaderResolver,resolve 過程開始時 request 引數如下:

{
  context: {
    compiler: undefined,
    issuer: "/dir/demos/main.js"
  },
  path: "/dir/demos"
  request: "url-loader"
}
複製程式碼

在 ParsePlugin 中,request: "url-loader" 會被解析為 module。隨後過程中整個和 module 執行流程相同。

到此 webpack 中關於 resolve 流程就結束了。除此之外 webpack 還有不少的細節處理,鑑於篇幅有限這裡就不展開細細討論了,大家可以結合文章看 webpack 程式碼時去細細品味。

從原理到優化

webpack 中每涉及到一個檔案,就會經過 resolve 的過程。而 resolve 過程中其中針對一些不確定的因素,比如字尾名,node_modules 路徑等,會存在探索的過程,從而使得整個 resolve 的鏈條很長。很多針對 webpack 的優化,都會提到利用 resolve 配置來減少檔案搜尋範圍:

  1. 使用 resolve.alias

我們日常開發專案中,常常會存在類似 common 這樣的目錄,common 目錄下的檔案,會被經常引用。比如 'common/index.js'。如果我們針對 common 目錄建立一個 alias 的話,在所有用到 'common/index.js' 的檔案中,可以寫 import xx from 'common/index.js'。 由於 UnsafeCachePlugin 的存在,當 webpack 再次解析到 'common/index.js' 時,就可以直接使用快取。

不止如此,重點是解析鏈條變短,快取只是一部分吧

  1. 設定 resolve.modules

resolve.modules 的預設值為 ['node_modules'],所以在對 module 的 resolve 過程中,會依次查詢 ./node_modules、../node_modules、../../node_modules 等,即沿著路徑一層一層往上找,直到找到 node_modules。可以直接設定

resolve.modules:[path.resolve(__dirname, 'node_modules')]
複製程式碼

如此會進入 ModulesInRootPlugin 而不是 ModulesInHierachicDirectoriesPlugin,避免了層層尋找 node_modules 的開銷。

  1. 對第三方模組設定 resolve.alias

對第三方的 module 進行 resolve 過程中,除了上面提到的 node_modules 目錄查詢過程,還會涉及到對 package.json 中配置的解析等。可以直接為其設定 alias 為執行檔案,來簡化整個 resolve 過程,如下:

resolve.alias: {
    'vue': path.resolve(__dirname, './node_modules/vue/dist/vue.common.js')
}
複製程式碼
  1. 合理設定 resolve.extensions,減少檔案查詢

當我們的檔案沒有字尾時,AppendPlugin 會根據 resolve.extensions 中的值,依次新增字尾然後查詢檔案。為了減少檔案查詢,我們可以直接將檔案字尾寫上,或者設定 resolve.extensions 中的值,列表值儘量少頻率高的檔案型別的字尾寫在前面

明白了 resolve 的細節之後,再來看這些優化策略,便可以更好的瞭解其原因,做到“知其然知其所以然”。

附圖(resolve事件流完整版):

resolve事件流完整版

相關文章