【webpack 核心庫】耗時 7 個小時,用近 50 張圖來學習 enhance-resolve 中的數據流動和插件調度機制

  1. 食用本文的文檔說明: =============

本篇文章 耗時 7個小時左右才完工,篇幅涉及到大量的源碼及其分析的過程圖解和數據,閱讀前,請保證自己有充分的時間,盡情的去享受吸收知識進入腦子的過程

因爲篇幅有限,希望你掌握以下前置知識:

  1. 已經學習過 `enhanced-resolve 工作流程和插拔式插件機制`[點這裏複習:webpack 核心庫 enhanced-resolve 工作流程和插拔式插件機制](https://juejin.cn/post/7167978104881676319 "https://juejin.cn/post/7167978104881676319")
  2. 瞭解 `tabaple` 是一個`訂閱發佈`的設計模式(知道啥是訂閱發佈即可)
  3. 大致瞭解 node 中的模塊查找機制,如:
require(‘./xxx.js’);
require('./xxx');
require('xxx');
複製代碼

通過本文你將學到如下內容(或者帶着如下疑問去學習):

  1. enhance-resolve是如何在複雜的插件調用之間傳遞數據的?

  2. Resolver 和 ResolverFactory的關係是什麼?

  3. Resolver是如何設計實現的?

  4. 軟鏈接和硬鏈接是什麼?區別在哪裏?

  5. 如何開發一個enhance-resolve的插件應用到 webpack 中?

  6. 如何去一步步的 debug 一個開源庫?

1 webpack 和 enhance-resolve 的關係是什麼?

webpack 作爲一個強大的打包工具,其強大的不僅僅是插件機制,還有其核心包enhance-resolve來實現模塊的路徑查找。功能上來說它可以增強Webpack的模塊解析能力,使其更容易找到所需的模塊,從而提高 Webpack 的性能和可維護性。從配置上來說它可以爲 Webpack 解析器添加額外的搜索路徑以及解析規則,讓 Webpack更好地解釋路徑和文件,進而讓 webpack 更加專心的做模塊打包相關的事情。

瞭解完背景和需求以後,如果讓我們去實現一個 enhance-resolve 呢?

功能點:

  1. 首先解析器滿足模塊查找中的所有的規則 模塊:通用 JS 模塊 | 節點. js v14.21.3 文檔 (nodejs.org)[1]

  2. 要和 webpack 一樣,有強大的插件加載機制和良好的配置功能

自己可以心中默默的想一下如何實現上述功能點呢?

  1. 接下來就根據上述功能點通過代碼去了解一下 enhance-resolve =========================================

咱們上回太強了,3000 字圖文並茂的解析 webpack 核心庫 enhanced-resolve 工作流程和插拔式插件機制,真香 - 掘金 (juejin.cn)[2] 說到:

  1. ResolverFactory.createResolver 根據 Resolver 類創建實例:myResolve (吃了配置,吐出對象myResolve)

  2. myResolve 上 註冊並訂閱 大量的 hook (槍支彈藥貯備好,一刻激發)

  3. 調用 myResolver.resolve 方法開始進行 文件解析 的主流程

  4. 內部通過 resolve.doResolve方法,開始調用第一個 hook: this.hooks.resolve

  5. 找到之前 訂閱 hook 的 plugin:ParsePlugin

  6. ParsePlugin 進行初步解析,然後 通過doResolve 執行下一個 hook parsed-resolve,前期準備工作結束,鏈式調用開始,真正的解析文件的流程也開始。

從上面的第 2 步開始整起,第 2 步註冊了哪些 hook 呢?接下來開始瞅瞅

2.1 細細回顧 myResolve 上註冊的 hooks

代碼跳轉到 lib/ResolverFactory.js295 行左右,代碼如下:

//// 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.jsresolve 方法中是查找路徑開始的起點,首先就是把 用戶傳入的 路徑 path 和 要查找文件的路徑 request 賦值給 obj 對象 【此 obj 是核心對象,將在各個插件中流轉修改】。

然後就開始調用自身的 doResolve 方法,正式開始流程了。

  1. resolve hook 開始的流程,到結束 =============================

斷點到 doResolve方法的 hook.callAsync 部分,看下相關的參數。

從圖中可以看出,此 hook 名爲 resolve,入參有兩個:Array(2)[request,resolveContext],綁定此 hook 的插件只有一個 ParsePlugin 的插件,傳遞下去的參數是 request 對象:pathrequest是重要的數據。

下一步就開始進入 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,其綁定的業務插件是 DescriptionFilePluginNextPlugin插件屬於流程插件,可以忽略。

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"
 )
);
複製代碼

確實滿足了不是文件夾的條件,推進到下一個 hook 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 向前找了,查找的過程要忽略掉 外賣小哥型插件 比如TryNextPluginNextPlugin

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.appendingrequest.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