【webpack 核心庫】耗時 7 個小時,用近 50 張圖來學習 enhance-resolve 中的數據流動和插件調度機制
- 食用本文的文檔說明: =============
本篇文章 耗時 7個小時
左右才完工,篇幅涉及到大量的源碼及其分析的過程圖解和數據
,閱讀前,請保證自己有充分的時間,盡情的去享受吸收知識進入腦子的過程
。
因爲篇幅有限,希望你掌握以下前置知識:
-
已經學習過 `enhanced-resolve 工作流程和插拔式插件機制`,[點這裏複習:webpack 核心庫 enhanced-resolve 工作流程和插拔式插件機制](https://juejin.cn/post/7167978104881676319 "https://juejin.cn/post/7167978104881676319")
-
瞭解 `tabaple` 是一個`訂閱發佈`的設計模式(知道啥是訂閱發佈即可)
-
大致瞭解 node 中的模塊查找機制,如:
require(‘./xxx.js’);
require('./xxx');
require('xxx');
複製代碼
通過本文你將學到如下內容(或者帶着如下疑問去學習):
-
enhance-resolve
是如何在複雜的插件調用之間傳遞數據的? -
Resolver 和 ResolverFactory
的關係是什麼? -
Resolver
是如何設計實現的? -
軟鏈接和硬鏈接
是什麼?區別在哪裏? -
如何開發一個
enhance-resolve
的插件應用到 webpack 中? -
如何去一步步的
debug
一個開源庫?
1 webpack 和 enhance-resolve 的關係是什麼?
webpack 作爲一個強大的打包工具,其強大的不僅僅是插件機制,還有其核心包enhance-resolve
來實現模塊的路徑查找。功能上來說它可以增強Webpack的模塊解析能力
,使其更容易找到所需的模塊,從而提高 Webpack 的性能和可維護性
。從配置上來說它可以爲 Webpack 解析器添加額外的搜索路徑以及解析規則,讓 Webpack更好地解釋路徑和文件
,進而讓 webpack 更加專心的做模塊打包相關的事情。
瞭解完背景和需求以後,如果讓我們去實現一個 enhance-resolve 呢?
功能點:
-
首先解析器滿足模塊查找中的所有的規則 模塊:通用 JS 模塊 | 節點. js v14.21.3 文檔 (nodejs.org)[1]
-
要和 webpack 一樣,有強大的
插件加載機制和良好的配置功能
。
自己可以心中默默的想一下如何實現上述功能點呢?
- 接下來就根據上述功能點通過代碼去了解一下
enhance-resolve
=========================================
咱們上回太強了,3000 字圖文並茂的解析 webpack 核心庫 enhanced-resolve 工作流程和插拔式插件機制,真香 - 掘金 (juejin.cn)[2] 說到:
-
ResolverFactory.createResolver 根據
Resolver
類創建實例:myResolve
(吃了配置,吐出對象myResolve
) -
myResolve 上 註冊並訂閱
大量的 hook (槍支彈藥貯備好,一刻激發) -
調用
myResolver.resolve
方法開始進行 文件解析 的主流程 -
內部通過
resolve.doResolve
方法,開始調用第一個 hook:this.hooks.resolve
-
找到之前 訂閱 hook 的 plugin:
ParsePlugin
-
ParsePlugin
進行初步解析,然後 通過doResolve
執行下一個 hookparsed-resolve
,前期準備工作結束,鏈式調用開始,真正的解析文件的流程
也開始。
從上面的第 2 步開始整起,第 2 步註冊了哪些 hook 呢?接下來開始瞅瞅
2.1 細細回顧 myResolve
上註冊的 hooks
代碼跳轉到 lib/ResolverFactory.js
的 295
行左右,代碼如下:
//// pipeline ////
resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInternalResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawResolve");
resolver.ensureHook("normalResolve");
resolver.ensureHook("internal");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("resolveAsModule");
resolver.ensureHook("undescribedResolveInPackage");
resolver.ensureHook("resolveInPackage");
resolver.ensureHook("resolveInExistingDirectory");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("undescribedExistingDirectory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("finalFile");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
複製代碼
爲了便於理解,放出 ensureHook
的部分核心代碼,其主要作用就是創建一個 AsyncSeriesBailHook
異步串行保險型的 hook,(所謂的保險
你可以想象成流浪星球 2 中的飽和式救援
,1 個任務派出多個救援隊【訂閱多個 hook】,只要一個救援隊成功了【一個 hook 存在返回值】這次救援就算成功了【這個訂閱事件就算結束了】)
ensureHook(name) {
if (typeof name !== "string") {
return name;
}
name = toCamelCase(name);
const hook = this.hooks[name];
if (!hook) {
return (this.hooks[name] = new AsyncSeriesBailHook(
["request", "resolveContext"],
name
));
}
return hook;
}
複製代碼
PS: ensureHook
的作用是
可以看到作者在頭部特意寫了一個簡短的註釋 //// pipeline ////
,翻譯過來也就是流水線。
流水線是一種工業生產方式,它將一個大型工程分解成若干個小步驟
,每個步驟都有專門的工人或機器
來完成,從而提高生產效率。流水線的優勢在於可以提高生產效率,減少生產成本,提高產品質量,並且可以更快地完成大型工程
。在 IT 界就可以認爲是模塊間解耦,提高代碼可讀性和可維護性
。
到這裏流水線流程組裝完畢【可理解成爲每個工種分配了相關的任務】,那下一步就是要開始組裝每部分流程用到的工具集(plugins)
,【然後再爲每個工種分配不同的工具】。部分核心代碼如下:
// resolve
for (const { source, resolveOptions } of [
{ source: "resolve", resolveOptions: { fullySpecified } },
{ source: "internal-resolve", resolveOptions: { fullySpecified: false } }
]) {
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
source,
cachePredicate,
unsafeCache,
cacheWithContext,
`new-${source}`
)
);
plugins.push(
new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve")
);
} else {
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
}
}
// parsed-resolve
plugins.push(
new DescriptionFilePlugin(
"parsed-resolve",
descriptionFiles,
false,
"described-resolve"
)
);
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
...... 此處省略部分註冊插件邏輯
//// RESOLVER ////
for (const plugin of plugins) {
if (typeof plugin === "function") {
plugin.call(resolver, resolver);
} else {
plugin.apply(resolver);
}
}
複製代碼
一直到最後把根據用戶配置生成的相關的插件列表plugins
給註冊到 resolver
上,整個的resolver
的 hook 和 plugin 的綁定才成功結束。
本次調試代碼綁定的 總的插件的數量爲 41個
:
其中因爲NextPlugin
是流程推動性插件和業務邏輯無關,就過濾掉,還剩下 32個
:
2.2 開始調試正式流程吧 (流水線打開電源,跑起來了)
在 lib/Resolver.js
的 resolve
方法中是查找路徑開始的起點,首先就是把 用戶傳入的 路徑 path
和 要查找文件的路徑 request
賦值給 obj 對象 【此 obj 是核心對象,將在各個插件中流轉修改】。
然後就開始調用自身的 doResolve
方法,正式開始流程了。
- 從
resolve
hook 開始的流程,到結束 =============================
斷點到 doResolve
方法的 hook.callAsync
部分,看下相關的參數。
從圖中可以看出,此 hook 名爲 resolve
,入參有兩個:Array(2)[request,resolveContext]
,綁定此 hook 的插件只有一個 ParsePlugin
的插件,傳遞下去的參數是 request
對象:path
和 request
是重要的數據。
下一步就開始進入 ParsePlugin
插件看看它究竟做了什麼。
3.1 視察 ParsePlugin
工種的工作
ParsePlugin
其核心 apply
代碼如下:
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
// 調用 resolver 中的 parse 方法初步解析
const parsed = resolver.parse(/** @type {string} */ (request.request));
// 合併成新的 obj 對象
const obj = { ...request, ...parsed, ...this.requestOptions };
if (request.query && !parsed.query) {
obj.query = request.query;
}
if (request.fragment && !parsed.fragment) {
obj.fragment = request.fragment;
}
if (parsed && resolveContext.log) {
if (parsed.module) resolveContext.log("Parsed request is a module");
if (parsed.directory)
resolveContext.log("Parsed request is a directory");
}
// There is an edge-case where a request with # can be a path or a fragment -> try both
if (obj.request && !obj.query && obj.fragment) {
const directory = obj.fragment.endsWith("/");
const alternative = {
...obj,
directory,
request:
obj.request +
(obj.directory ? "/" : "") +
(directory ? obj.fragment.slice(0, -1) : obj.fragment),
fragment: ""
};
resolver.doResolve(
target,
alternative,
null,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
resolver.doResolve(target, obj, null, resolveContext, callback);
}
);
return;
}
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
複製代碼
經過斷點發現,obj
對象第一次進入這個 plugin
逛了一圈,然後最終走到了 resolver.doResolve(target, obj, null, resolveContext, callback);
這裏,處理完的數據如下:【思考一下吃了啥數據,吐出了啥數據?】
ParsePlugin
吃了 obj,以後對其進行初步解析,增加了如下屬性 【紅色是喫進去的,綠色是吐出來的】
然後下一個要執行 hook 是parsedResolve
,其綁定的業務插件是 DescriptionFilePlugin
,NextPlugin
插件屬於流程插件,可以忽略。
3.2 視察 DescriptionFilePlugin
工種的工作
當前流程的 DescriptionFilePlugin
插件的核心是在 DescriptionFileUtils.loadDescriptionFile
方法裏,
當看到 ['package.json']
的那一刻是不是可以聯想並猜測到:此插件的作用就是在實現查找當前的路徑
是否是一個 具有package.json
文件的模塊?繼續 debug loadDescriptionFile
方法,
看到這個路徑拼接,驗證了猜想是正確的,繼續 debug 發現,走到了此方法的 callback 函數里,執行了一個 cdUp
的方法。
我們不去看方法實現,僅僅看變更,變量從directory
變成了 dir
,數據從/Users/fujunkui/Desktop/github-project/enhanced-resolve/demo/test-find-file
變成了/Users/fujunkui/Desktop/github-project/enhanced-resolve/demo
,臥槽,還真是進入了上級目錄,cdUp
66666。
不出所料的話,他會一直 cdUp
知道進入到根目錄的, 查找 /package.json
爲止 【圖中,我把 enhance-resolve 項目的 package.json 文件給刪除了,不刪除的話找到這一級就停止了】 部分截圖
最後找呀找呀,就是找不到一個目錄具有package.json
文件,沒辦法只能走 callback
了。
結果就是這個插件一頓 cdUp 操作,啥都沒變,注意此處的 callback()
返回值爲空,他就要進入此 hook 的下一個插件了,NextPlugin
正式登場。
3.3 外賣小哥 NextPlugin
正式登場
NextPlugin
核心代碼如下:
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("NextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(target, request, null, resolveContext, callback);
});
}
複製代碼
直接調用 resolver.doResolve
把上一個 hook 的丟出的數據,給下一個 hook 使用,不做任何改變(像極了 辛苦幫商家送餐的外賣小哥,點贊)。
那就有請下一位 hook 閃亮登場:
好傢伙,下一個 hook 是 rawResolve
,讓我們來看看他的監聽者 都有誰,拉倒吧,還是 NextPlugin
外賣小哥,這就是外賣小哥點飯(外賣小哥送給外賣小哥)???
[3]
那就繼續吧,看看這個 rawResolve
的下一個 hook 是誰,監聽的插件都有誰?
下一個 hook 名叫 normalResolve
,竟然有 3 個插件監聽了此 hook,那麼開始表演吧。
3.4 視察 hook 名爲normalResolve
下面的三個工種(插件)的工作
3.4.1 第一位和第二位 靚仔都是 ConditionalPlugin
(翻譯爲中文就是:條件插件)
大致猜測一下條件插件:就是滿足了哪些條件纔會繼續執行下去。
兩者的區別在初始化的傳參裏:
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ module: true },
"resolve as module",
false,
"raw-module"
)
);
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ internal: true },
"resolve as internal import",
false,
"internal"
)
);
複製代碼
總體代碼是:
class ConditionalPlugin {
constructor(source, test, message, allowAlternatives, target) {
this.source = source;
this.test = test;
this.message = message;
this.allowAlternatives = allowAlternatives;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
const { test, message, allowAlternatives } = this;
const keys = Object.keys(test);
resolver
.getHook(this.source)
.tapAsync("ConditionalPlugin", (request, resolveContext, callback) => {
for (const prop of keys) {
if (request[prop] !== test[prop]) return callback();
}
resolver.doResolve(
target,
request,
message,
resolveContext,
allowAlternatives
? callback
: (err, result) => {
if (err) return callback(err);
// Don't allow other alternatives
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
};
複製代碼
執行結果如下:第一次 插件的 callback 結果是 空【下圖】,進入 第二個 插件,
第二個插件的 callback 結果是 空【下圖】, 進入 JoinRequestPlugin
插件
3.4.2 視察 JoinRequestPlugin
插件的工作
看名字就知道是幹啥的,任務比較簡單,就是把 path 和 request 合併成新的路徑 賦值給 path
(綠色圈中部分),
resolver.join(request.path, request.request),
複製代碼
這個 hook 的事情完成了,有請下一個 hook relative
,以及它的兩位監聽者們。
3.5 視察 hook 名爲relative
下面的兩個工種(插件)的工作
兜兜轉轉的又進入 DescriptionFilePlugin
插件了,但是 此時的參數和之前的不一樣了,但是好像也沒有什麼不同,最後還是 callback 爲空,灰頭土臉的走進下一個插件了。
繼續走到 NextPlugin
,然後被送到 describedRelative
的 hook,此 hook 的監聽者有:
3.5 視察 hook 名爲describedRelative
下面的兩個工種(條件插件)的工作
條件插件要滿足的第一個邏輯就是,不是文件夾,推測我們是滿足的,開始 debug。
plugins.push(
new ConditionalPlugin(
"described-relative",
{ directory: false },
null,
true,
"raw-file"
)
);
plugins.push(
new ConditionalPlugin(
"described-relative",
{ fullySpecified: false },
"as directory",
true,
"directory"
)
);
複製代碼
rawFile
,其相關的監聽者有 5 個。
3.6 視察 hook 名爲rawFile
下面的工種的工作
不滿足此插件,走進下一個插件TryNextPlugin
:
// raw-file
plugins.push(
new ConditionalPlugin(
"raw-file",
{ fullySpecified: true },
null,
false,
"file"
)
);
複製代碼
TryNextPlugin
(嘗試下一個插件) 的代碼如下:
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(
target,
request,
this.message,
resolveContext,
callback
);
});
}
複製代碼
個人感覺其實此處的邏輯更應該是嘗試下一個hook
,而不是插件
,所以改爲 TryNextHook
更好. 之所以這麼說看下面的代碼:
plugins.push(new TryNextPlugin("raw-file", "no extension", "file"));
複製代碼
上面代碼簡單理解爲,被查找的文件是 不帶擴展的文件,可以直接走到 名爲 file
的 hook 裏。此 hook 的監聽插件有:
那就繼續走 NextPlugin
插件的邏輯,然後走向了 finalFile
的 hook 【下圖】, 進入 FileExistsPlugin
插件的邏輯裏。
3.7 視察 hook 名爲finalFile
下面的工種FileExistsPlugin
插件的工作
代碼比較簡單:獲取查找路徑,直接判斷是不是文件即可。
發現不是文件,那就執行 callback 函數,此插件的 callback 函數是Resolver 中的 hook.callAsync
中的 callback 函數
然後 Resolver 中的 hook.callAsync
中的 callback 函數接受到的 err 和 result 都是 undefined,就又走了 doResolve
中接受的 callback 函數,那就要開始從現在這個 finalFile
向前找了,查找的過程要忽略掉 外賣小哥型插件 比如TryNextPlugin
和NextPlugin
。
finalFile
上一個是 file
的 hook 監聽 (NextPlugin
可忽略), file
的上一個是 raw-file
,觸發 raw-file
下的插件的監聽,接下來就是查找監聽了 hook 位 raw-file
的插件了。
這塊的代碼可能因爲都叫 callback,並且跳來跳去的有些難以理解,可以參考我下面簡化過的代碼。
let { AsyncSeriesBailHook } = require("tapable");
const hook1 = new AsyncSeriesBailHook(["request", "resolveContext"], "hook1");
const hook2 = new AsyncSeriesBailHook(["request", "resolveContext"], "hook2");
const hook1Tap1 = hook1.tapAsync(
"hook1Tap1",
(request, resolveContext, callback) => {
console.log("hook1Tap1", request, resolveContext);
return callback();
}
);
const hook1Tap2 = hook1.tapAsync(
"hook1Tap2",
(request, resolveContext, callback) => {
console.log("hook1Tap2", request, resolveContext);
return callback();
}
);
const hook2Tap1 = hook2.tapAsync(
"hook2Tap1",
(request, resolveContext, callback) => {
console.log("hook2Tap1", request, resolveContext);
return callback();
}
);
const hook2Tap2 = hook2.tapAsync(
"hook2Tap2",
(request, resolveContext, callback) => {
console.log("hook2Tap2", request, resolveContext);
return callback("err");
}
);
hook1.callAsync("111", "222", () => {
console.log("hook1 callback");
hook2.callAsync("333", "455", err => {
console.log("hook2 callback", err);
});
});
複製代碼
執行結果如下:
這塊的內容是定義了兩個異步的hook
, 然後在hook1 調用 callAsync
的時候,裏面傳遞了 hook2 的 callAsync
調用,這樣就會在調用完 hook1
的觸發事件,然後去接着調用 hook2
的觸發事件。
這樣是不是可以理解 多個 hook 之前傳遞 callback 的邏輯了?
那麼接下來就要找監聽了 hook 名爲 raw-file
的插件有哪些了,直接看 ResolverFactory
註冊時間得知 【下圖】,有 3 個插件監聽了。而現在的順序 又是按照監聽順序倒着執行 callback 的,那就應該是先執行 AppendPlugin
插件了,打上斷點,跑一下
3.8 回首掏,去視察 hook 名爲raw-file
下面的工種AppendPlugin
插件的工作
AppendPlugin
代碼較爲簡單,就是把傳入的 this.appending
和 request.path
進行拼接,生成新的 request.path
,
module.exports = class AppendPlugin {
constructor(source, appending, target) {
this.source = source;
this.appending = appending;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("AppendPlugin", (request, resolveContext, callback) => {
const obj = {
...request,
path: request.path + this.appending,
relativePath:
request.relativePath && request.relativePath + this.appending
};
resolver.doResolve(
target,
obj,
this.appending,
resolveContext,
callback
);
});
}
};
複製代碼
查找 this.appending
是在實例化時候傳入的,斷點得知。這個就是我們傳入的 extensions
配置
const myResolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".json", ".js", ".ts"]
});
複製代碼
然後斷點到此處,看喫進去了啥,吐出來了啥。
然後下一個 hook 是 file
,只有一個 NextPlugin
插件監聽了此 hook,用來推進流程【下圖】。
而 NextPlugin
插件是將流程 從 file
推向了 final-file
hook,走到 3.7 的流程,判斷一下帶有此後綴的文件是否存在,不存在的話,繼續 重複 raw-file
hook 的 AppendPlugin
的流程,此時的參數是 this.appending
是 .js
【下圖】
繼續 重複以上的操作:NextPlugin
插件是將流程 從 file
推向了 final-file
hook,然後 FileExistsPlugin
插件判斷到,此文件存在,推進流程到 existingFile
的 hook,此 hook 有 2 個插件監聽【下圖】。
3.9 文件存在了,下一步去視察 hook 名爲existingFile
下面的插件的工作
先去執行SymlinkPlugin
通過 fs.readlink
方法判斷其是否是符號鏈接下的文件,符號鏈接 symlink_什麼是符號鏈接或符號鏈接?如何爲 Windows 和 Linux 創建 Symlink?_cunjiu9486 的博客 - CSDN 博客 [4],
再補充一點 硬鏈接和軟鏈接的區別?- 掘金 (juejin.cn)[5]
關於符號鏈接這裏有特殊說明,假設你新建了
b.js
,刪除了當前目錄下的a.js
,當前目錄情況如下:
建立軟鏈接,進行測試:
其實軟鏈接,還區分絕對路徑和相對路徑的情況【下圖】,本次只考慮相對路徑,大家可以使用絕對路徑進行 debug.
我們進行軟鏈接的 debug,最後發現查找到 b.js 的路徑,那麼繼續 debug。
到此是發現了軟鏈接的源文件,那麼下一步肯定是判斷 此源文件是否是存在,又走到 existingFile
的 hook 【下圖】,重複 3.9 的步驟,又走 SymlinkPlugin
插件的邏輯(擔心軟鏈接的源文件還是軟鏈接),
繼續 debug SymlinkPlugin
,發現走到了 callback()
的情況【下圖】,那就是要進入下一個監聽者 (NextPlugin)了,
在 NextPlugin
中發現終於走到了最後的 hook resolved
,只有一個插件 ResultPlugin
進行監聽。
進入 ResultPlugin
插件內部,其主要是調用了 result
的 hook,
apply(resolver) {
this.source.tapAsync(
"ResultPlugin",
(request, resolverContext, callback) => {
const obj = { ...request };
if (resolverContext.log)
resolverContext.log("reporting result " + obj.path);
resolver.hooks.result.callAsync(obj, resolverContext, err => {
if (err) return callback(err);
if (typeof resolverContext.yield === "function") {
resolverContext.yield(obj);
callback(null, null);
} else {
callback(null, obj);
}
});
}
);
}
複製代碼
debug 一下那些插件監聽了此 hook,發現是空的,直接走到自身的 callback 函數里,
繼續 debug 此 callback 函數,就會發現這個 callback 在一層一層的向上傳遞值,接着傳到 Resolver 裏的 resolve 函數
裏, 經過 finishResolved
處理解析一次【下圖】,最後傳遞給 我們自身的 callback 函數里。
debug 停在我們自己監聽的 callback 函數里,至此完成整體流程。
4 完結撒花,回顧總結。
通過一步一步的 debug,會發現 enhance-resolve 這個庫,把 tapable 給用的出神入化,核心的處理邏輯都在 Resolver
上,而 ResolverFactory
則像是 流水線的 線長,借用Resolver
的能力,去指定流水線的流程,分配流水線每個流程應該協作的工種。
總的邏輯通下來,你會發現,所有的插件都是在對 obj 對象
做數據變更,每個插件都有自己的職責,互不干涉,互不影響,通過 NextPlugin
,這個外賣小哥插件,把 數據在各個 hook 流程之間進行流轉,進而建立起一套高效的流水線系統,耦合性低,定製化程度高,功能強大
。
這裏就不畫流程圖做總結了,偷個懶,因爲此文章耗時 7個小時
左右 (啊,我的眼鏡),從頭到尾 debug 下來,發現收穫不少,以後完全可以模仿此庫基於自己的業務流程,開發定製一套屬於自己的高效可定製化的可插拔插件的工程。
希望大家看完此文章會有所收穫,慢慢的開始自己的學習源碼之路。衝吧,兄弟們。
另外放出一個基於此庫開發的一個根據不同文件後綴進行條件編譯的插件:@fu1996/webapck-resolver-mode-plugin - npm (npmjs.com)[6]
關於本文
作者:付俊奎
https://juejin.cn/post/7204356282588676156
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/LL9VytDK8w9_qiRiRivtmw