作者:崔靜
介紹
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 原始碼中 resolve 流程開始的入口在 factory 階段, factory 事件會觸發 NormalModuleFactory 中的函式。先放一張粗略的總體流程圖,在深入原始碼前現有一個大概的框架圖
接下來我們就從 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流程
- 獲取到 inline loader 的 request 部分。例如,針對如下寫法
import Styles from 'style-loader!css-loader?modules!./styles.css';
複製程式碼
會從中解析出 style-loader
和 css-loader
。由於此步驟只是為了解析出路徑,所以對於 loader 的配置部分並不關心。
-
得到 loader 型別的 resolver 處理例項,即
const loaderResolver = this.getResolver("loader");
-
對每一個 loader 用 loaderResolver 依次處理,得到執行檔案的路徑。
檔案流程
-
得到普通檔案的 resolver 處理例項,即程式碼
const normalResolver = this.getResolver("normal", data.resolveOptions);
-
用 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 例項的具體流程如下圖。
上圖中,首先根據不同 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 就結束了。
將這些事件一個一個串聯起來的關鍵部分在 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);
});
// ...
複製程式碼
上面程式碼整理一下,可以得到完整的事件流圖(下圖為簡化版本,完成版本附圖)
結合上面的圖和 demo,我們來一步一步看這個事件流中每一環都做了什麼。(ps:下面步驟中,會涉及到 request 引數,這個引數貫穿所有事件處理邏輯,儲存了整個 resolve 的資訊)
- UnsafeCachePlugin
增加一層快取,由於 webpack 處理打包的過程中,涉及到大量的 resolve 過程。所以需要增加一層快取,提高效率。webpack 預設會啟用 UnsafeCache。
-
ParsePlugin
初步解析路徑,判斷是否為 module/directory/file,結果儲存到 request 引數中。
-
DescriptionFilePlugin 和 NextPlugin
DescriptionFilePlugin 中會尋找描述檔案,預設會尋找 package.json。首先會在 request.path 這個目錄下尋找,如果沒有則按照路徑一層一層往上尋找。最後讀取到 package.json 的資訊和其所在的目錄/路徑資訊,存入 request 中。我們在 demo 的根目錄有 package.json 檔案,所以這裡會獲取到根目錄的檔案。
NextPlugin 起一個銜接的作用,內部邏輯就是直接呼叫 doResolve,然後觸發下一個事件。當 DescriptionFilePlugin 中未找到 package.json 檔案時,會進入 NextPlugin,然後讓事件流繼續。
-
AliasPlugin/AliasFieldPlugin
這一步開始處理別名,由於 AliasFieldPlugin 中依賴於 package.json 的配置,所以這一步放在了 DescriptionFilePlugin 之後。 除了我們在配置檔案中寫一些別名外,webpack 還會有一些自帶的 alias;每一個 alias 配置,都會註冊一個函式。這一步將執行所有的函式,一一對比。 若命中某一 alias 的配置或者 aliasField,那麼就會進入上圖紅色虛線的分支。用新的別名替換 request 引數內容,然後再次開始 resolve 過程。 沒有命中,則進入下一個處理函式 ModuleKindPlugin
- ModuleKindPlugin
根據 request.module
的值走不同的分支。如果是 module,則後續進入 rawModule 的邏輯。前面 ParsePlugin 中得到的結果中 request.module
為 false
,所以這裡返回 undefined,繼續進入下一個處理函式。
- JoinRequestPlugin
將 request 中 path 和 request 合併起來,將 request 中 relativePath 和 request 合併起來,得到兩個完整的路徑。在這個 demo 中會得到 /Users/didi/dist/webpackdemo/webpack-demos/demo01/a.js
和 ./demo01/a.js
- DescriptionFilePlugin
這時會再次進入 DescriptionFilePlugin 。不過與第一次進入時不同之處在於,此時的 request.path 變成了 /dir/demo/a.js`。由於 path 改變了,所以需要再次查詢一下 package.json
隨後觸發 describedRelative 事件,進入下一個流程
- FileKindPlugin
判斷是否為一個 directory,如果是則返回 undefined, 進入下一個 tryNextPlugin,這時會進入 directory 的分支。否則,則表明是一個檔案,進入 rawFile 事件。我們的 demo 中,這裡將走向 rawFile 分支。
- 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
分支,繼續事件流程。
- AliasPlugin/AliasFields/ConcorModulesPlugin/SymlinkPlugin
這時會再次進入到 Alias 的處理邏輯,注意在此步中 webpack 內部自帶的很多 Alias 不會再有。
與前面相同,這裡依然沒有 ConcorModulesPlugin
SymlinkPlugin 用來處理路徑中存在 link 的情況。由於 webpack 預設是按照真實的路徑來解析的,所以這裡會檢查路徑中每一段,如果遇到 link,則替換為真實路徑。由於 path 改變了,所以會再回到 relative
階段。
若路徑中沒有 link,則進入 FileExistsPlugin
- FileExistsPlugin
讀取 request.path
所在的檔案,看檔案是否存在。檔案存在則進入到 existingFile 事件。
- 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 分支流程。
- ModuleAppendPlugin/TryNextPlugin
ModuleAppendPlugin 和上面的 AppendPlugin 類似,新增字尾。 TryNextPlugin 進入 module 事件
- 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
。
- 與普通檔案 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
按照資料夾的方式來解析。
- DirectoryExisitsPlugin
確認 dir/demo/node_module/vue
是否存在。(ps: 針對 context 的 resolve 過程,到這裡如果資料夾存在,則就結束了。)
- 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
- UseFilePlugin
當我們 package.json 中沒有寫 browser、module、main 時,webpack 會自動去找目錄下的 index 檔案,request 變成如下
{
//...省略其他部分
relativePath: "./index",
path: 'dir/demo/node_modules/vue/index'
}
複製程式碼
然後觸發 undescribedRawFile 事件
- DescriptionFilePlugin/TryNextPlugin
針對新的 request.path ,重新尋找描述檔案,即 package.json
- 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 配置來減少檔案搜尋範圍:
- 使用 resolve.alias
我們日常開發專案中,常常會存在類似 common 這樣的目錄,common 目錄下的檔案,會被經常引用。比如 'common/index.js'。如果我們針對 common 目錄建立一個 alias 的話,在所有用到 'common/index.js' 的檔案中,可以寫 import xx from 'common/index.js'
。 由於 UnsafeCachePlugin 的存在,當 webpack 再次解析到 'common/index.js' 時,就可以直接使用快取。
不止如此,重點是解析鏈條變短,快取只是一部分吧
- 設定 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 的開銷。
- 對第三方模組設定 resolve.alias
對第三方的 module 進行 resolve 過程中,除了上面提到的 node_modules 目錄查詢過程,還會涉及到對 package.json 中配置的解析等。可以直接為其設定 alias 為執行檔案,來簡化整個 resolve 過程,如下:
resolve.alias: {
'vue': path.resolve(__dirname, './node_modules/vue/dist/vue.common.js')
}
複製程式碼
- 合理設定 resolve.extensions,減少檔案查詢
當我們的檔案沒有字尾時,AppendPlugin 會根據 resolve.extensions 中的值,依次新增字尾然後查詢檔案。為了減少檔案查詢,我們可以直接將檔案字尾寫上,或者設定 resolve.extensions 中的值,列表值儘量少,頻率高的檔案型別的字尾寫在前面。
明白了 resolve 的細節之後,再來看這些優化策略,便可以更好的瞭解其原因,做到“知其然知其所以然”。
附圖(resolve事件流完整版):