成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

webpack原理

trigkit4 / 2689人閱讀

摘要:原理查看所有文檔頁面前端開發(fā)文檔,獲取更多信息。初始化階段事件名解釋初始化參數(shù)從配置文件和語句中讀取與合并參數(shù),得出最終的參數(shù)。以上處理的相關(guān)配置如下編寫編寫的職責(zé)由上面的例子可以看出一個的職責(zé)是單一的,只需要完成一種轉(zhuǎn)換。

webpack原理
查看所有文檔頁面:前端開發(fā)文檔,獲取更多信息。
原文鏈接:webpack原理,原文廣告模態(tài)框遮擋,閱讀體驗(yàn)不好,所以整理成本文,方便查找。
工作原理概括 基本概念

在了解 Webpack 原理前,需要掌握以下幾個核心概念,以方便后面的理解:

Entry:入口,Webpack 執(zhí)行構(gòu)建的第一步將從 Entry 開始,可抽象成輸入。

Module:模塊,在 Webpack 里一切皆模塊,一個模塊對應(yīng)著一個文件。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模塊。

Chunk:代碼塊,一個 Chunk 由多個模塊組合而成,用于代碼合并與分割。

Loader:模塊轉(zhuǎn)換器,用于把模塊原內(nèi)容按照需求轉(zhuǎn)換成新內(nèi)容。

Plugin:擴(kuò)展插件,在 Webpack 構(gòu)建流程中的特定時機(jī)會廣播出對應(yīng)的事件,插件可以監(jiān)聽這些事件的發(fā)生,在特定時機(jī)做對應(yīng)的事情。

流程概括

Webpack 的運(yùn)行流程是一個串行的過程,從啟動到結(jié)束會依次執(zhí)行以下流程:

初始化參數(shù):從配置文件和 Shell 語句中讀取與合并參數(shù),得出最終的參數(shù);

開始編譯:用上一步得到的參數(shù)初始化 Compiler 對象,加載所有配置的插件,執(zhí)行對象的 run 方法開始執(zhí)行編譯;

確定入口:根據(jù)配置中的 entry 找出所有的入口文件;

編譯模塊:從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進(jìn)行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理;

完成模塊編譯:在經(jīng)過第4步使用 Loader 翻譯完所有模塊后,得到了每個模塊被翻譯后的最終內(nèi)容以及它們之間的依賴關(guān)系;

輸出資源:根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉(zhuǎn)換成一個多帶帶的文件加入到輸出列表,這步是可以修改輸出內(nèi)容的最后機(jī)會;

輸出完成:在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)。

在以上過程中,Webpack 會在特定的時間點(diǎn)廣播出特定的事件,插件在監(jiān)聽到感興趣的事件后會執(zhí)行特定的邏輯,并且插件可以調(diào)用 Webpack 提供的 API 改變 Webpack 的運(yùn)行結(jié)果。

流程細(xì)節(jié)

Webpack 的構(gòu)建流程可以分為以下三大階段:

初始化:啟動構(gòu)建,讀取與合并配置參數(shù),加載 Plugin,實(shí)例化 Compiler。

編譯:從 Entry 發(fā)出,針對每個 Module 串行調(diào)用對應(yīng)的 Loader 去翻譯文件內(nèi)容,再找到該 Module 依賴的 Module,遞歸地進(jìn)行編譯處理。

輸出:對編譯后的 Module 組合成 Chunk,把 Chunk 轉(zhuǎn)換成文件,輸出到文件系統(tǒng)。

如果只執(zhí)行一次構(gòu)建,以上階段將會按照順序各執(zhí)行一次。但在開啟監(jiān)聽模式下,流程將變?yōu)槿缦拢?/p>

在每個大階段中又會發(fā)生很多事件,Webpack 會把這些事件廣播出來供給 Plugin 使用,下面來一一介紹。

初始化階段
事件名 解釋
初始化參數(shù) 從配置文件和 Shell 語句中讀取與合并參數(shù),得出最終的參數(shù)。 這個過程中還會執(zhí)行配置文件中的插件實(shí)例化語句 new Plugin()
實(shí)例化 Compiler 用上一步得到的參數(shù)初始化 Compiler 實(shí)例,Compiler 負(fù)責(zé)文件監(jiān)聽和啟動編譯。Compiler 實(shí)例中包含了完整的 Webpack 配置,全局只有一個 Compiler 實(shí)例。
加載插件 依次調(diào)用插件的 apply 方法,讓插件可以監(jiān)聽后續(xù)的所有事件節(jié)點(diǎn)。同時給插件傳入 compiler 實(shí)例的引用,以方便插件通過 compiler 調(diào)用 Webpack 提供的 API。
environment 開始應(yīng)用 Node.js 風(fēng)格的文件系統(tǒng)到 compiler 對象,以方便后續(xù)的文件尋找和讀取。
entry-option 讀取配置的 Entrys,為每個 Entry 實(shí)例化一個對應(yīng)的 EntryPlugin,為后面該 Entry 的遞歸解析工作做準(zhǔn)備。
after-plugins 調(diào)用完所有內(nèi)置的和配置的插件的 apply 方法。
after-resolvers 根據(jù)配置初始化完 resolver,resolver 負(fù)責(zé)在文件系統(tǒng)中尋找指定路徑的文件。
空格 空格
空格 空格
空格 空格
編譯階段
事件名 解釋
run 啟動一次新的編譯。
watch-run run 類似,區(qū)別在于它是在監(jiān)聽模式下啟動的編譯,在這個事件中可以獲取到是哪些文件發(fā)生了變化導(dǎo)致重新啟動一次新的編譯。
compile 該事件是為了告訴插件一次新的編譯將要啟動,同時會給插件帶上 compiler 對象。
compilation 當(dāng) Webpack 以開發(fā)模式運(yùn)行時,每當(dāng)檢測到文件變化,一次新的 Compilation 將被創(chuàng)建。一個 Compilation 對象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。Compilation 對象也提供了很多事件回調(diào)供插件做擴(kuò)展。
make 一個新的 Compilation 創(chuàng)建完畢,即將從 Entry 開始讀取文件,根據(jù)文件類型和配置的 Loader 對文件進(jìn)行編譯,編譯完后再找出該文件依賴的文件,遞歸的編譯和解析。
after-compile 一次 Compilation 執(zhí)行完成。
invalid 當(dāng)遇到文件不存在、文件編譯錯誤等異常時會觸發(fā)該事件,該事件不會導(dǎo)致 Webpack 退出。
空格 空格
空格 空格

在編譯階段中,最重要的要數(shù) compilation 事件了,因?yàn)樵?compilation 階段調(diào)用了 Loader 完成了每個模塊的轉(zhuǎn)換操作,在 compilation 階段又包括很多小的事件,它們分別是:

事件名 解釋
build-module 使用對應(yīng)的 Loader 去轉(zhuǎn)換一個模塊。
normal-module-loader 在用 Loader 對一個模塊轉(zhuǎn)換完后,使用 acorn 解析轉(zhuǎn)換后的內(nèi)容,輸出對應(yīng)的抽象語法樹(AST),以方便 Webpack 后面對代碼的分析。
program 從配置的入口模塊開始,分析其 AST,當(dāng)遇到 require 等導(dǎo)入其它模塊語句時,便將其加入到依賴的模塊列表,同時對新找出的依賴模塊遞歸分析,最終搞清所有模塊的依賴關(guān)系。
seal 所有模塊及其依賴的模塊都通過 Loader 轉(zhuǎn)換完成后,根據(jù)依賴關(guān)系開始生成 Chunk。
輸出階段
事件名 解釋
should-emit 所有需要輸出的文件已經(jīng)生成好,詢問插件哪些文件需要輸出,哪些不需要。
emit 確定好要輸出哪些文件后,執(zhí)行文件輸出,可以在這里獲取和修改輸出內(nèi)容。
after-emit 文件輸出完畢。
done 成功完成一次完成的編譯和輸出流程。
failed 如果在編譯和輸出流程中遇到異常導(dǎo)致 Webpack 退出時,就會直接跳轉(zhuǎn)到本步驟,插件可以在本事件中獲取到具體的錯誤原因。

在輸出階段已經(jīng)得到了各個模塊經(jīng)過轉(zhuǎn)換后的結(jié)果和其依賴關(guān)系,并且把相關(guān)模塊組合在一起形成一個個 Chunk。 在輸出階段會根據(jù) Chunk 的類型,使用對應(yīng)的模版生成最終要要輸出的文件內(nèi)容。

輸出文件分析

雖然在前面的章節(jié)中你學(xué)會了如何使用 Webpack ,也大致知道其工作原理,可是你想過 Webpack 輸出的 bundle.js 是什么樣子的嗎? 為什么原來一個個的模塊文件被合并成了一個多帶帶的文件?為什么 bundle.js 能直接運(yùn)行在瀏覽器中? 本節(jié)將解釋清楚以上問題。

先來看看由 安裝與使用 中最簡單的項(xiàng)目構(gòu)建出的 bundle.js 文件內(nèi)容,代碼如下:

See the Pen bundle.js by whjin (@whjin) on CodePen.


以上看上去復(fù)雜的代碼其實(shí)是一個立即執(zhí)行函數(shù),可以簡寫為如下:

(function(modules) {

  // 模擬 require 語句
  function __webpack_require__() {
  }

  // 執(zhí)行存放所有模塊數(shù)組中的第0個模塊
  __webpack_require__(0);

})([/*存放所有模塊的數(shù)組*/])

bundle.js 能直接運(yùn)行在瀏覽器中的原因在于輸出的文件中通過 __webpack_require__ 函數(shù)定義了一個可以在瀏覽器中執(zhí)行的加載函數(shù)來模擬 Node.js 中的 require 語句。

原來一個個獨(dú)立的模塊文件被合并到了一個多帶帶的 bundle.js 的原因在于瀏覽器不能像 Node.js 那樣快速地去本地加載一個個模塊文件,而必須通過網(wǎng)絡(luò)請求去加載還未得到的文件。 如果模塊數(shù)量很多,加載時間會很長,因此把所有模塊都存放在了數(shù)組中,執(zhí)行一次網(wǎng)絡(luò)加載。

如果仔細(xì)分析 __webpack_require__ 函數(shù)的實(shí)現(xiàn),你還有發(fā)現(xiàn) Webpack 做了緩存優(yōu)化: 執(zhí)行加載過的模塊不會再執(zhí)行第二次,執(zhí)行結(jié)果會緩存在內(nèi)存中,當(dāng)某個模塊第二次被訪問時會直接去內(nèi)存中讀取被緩存的返回值。

分割代碼時的輸出

例如把源碼中的 main.js 修改為如下:

// 異步加載 show.js
import("./show").then((show) => {
  // 執(zhí)行 show 函數(shù)
  show("Webpack");
});

重新構(gòu)建后會輸出兩個文件,分別是執(zhí)行入口文件 bundle.js 和 異步加載文件 0.bundle.js

其中 0.bundle.js 內(nèi)容如下:

// 加載在本文件(0.bundle.js)中包含的模塊
webpackJsonp(
  // 在其它文件中存放著的模塊的 ID
  [0],
  // 本文件所包含的模塊
  [
    // show.js 所對應(yīng)的模塊
    (function (module, exports) {
      function show(content) {
        window.document.getElementById("app").innerText = "Hello," + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js 內(nèi)容如下:

See the Pen bundle.js by whjin (@whjin) on CodePen.


這里的 bundle.js 和上面所講的 bundle.js 非常相似,區(qū)別在于:

多了一個 __webpack_require__.e 用于加載被分割出去的,需要異步加載的 Chunk 對應(yīng)的文件;

多了一個 webpackJsonp 函數(shù)用于從異步加載的文件中安裝模塊。

在使用了 CommonsChunkPlugin 去提取公共代碼時輸出的文件和使用了異步加載時輸出的文件是一樣的,都會有 __webpack_require__.ewebpackJsonp。 原因在于提取公共代碼和異步加載本質(zhì)上都是代碼分割。

編寫 Loader

Loader 就像是一個翻譯員,能把源文件經(jīng)過轉(zhuǎn)化后輸出新的結(jié)果,并且一個文件還可以鏈?zhǔn)降慕?jīng)過多個翻譯員翻譯。

以處理 SCSS 文件為例:

SCSS 源代碼會先交給 sass-loader 把 SCSS 轉(zhuǎn)換成 CSS;

sass-loader 輸出的 CSS 交給 css-loader 處理,找出 CSS 中依賴的資源、壓縮 CSS 等;

css-loader 輸出的 CSS 交給 style-loader 處理,轉(zhuǎn)換成通過腳本加載的 JavaScript 代碼;

可以看出以上的處理過程需要有順序的鏈?zhǔn)綀?zhí)行,先 sass-loadercss-loaderstyle-loader。 以上處理的 Webpack 相關(guān)配置如下:

See the Pen 編寫 Loader by whjin (@whjin) on CodePen.


Loader 的職責(zé)

由上面的例子可以看出:一個 Loader 的職責(zé)是單一的,只需要完成一種轉(zhuǎn)換。 如果一個源文件需要經(jīng)歷多步轉(zhuǎn)換才能正常使用,就通過多個 Loader 去轉(zhuǎn)換。 在調(diào)用多個 Loader 去轉(zhuǎn)換一個文件時,每個 Loader 會鏈?zhǔn)降捻樞驁?zhí)行, 第一個 Loader 將會拿到需處理的原內(nèi)容,上一個 Loader 處理后的結(jié)果會傳給下一個接著處理,最后的 Loader 將處理后的最終結(jié)果返回給 Webpack。

所以,在你開發(fā)一個 Loader 時,請保持其職責(zé)的單一性,你只需關(guān)心輸入和輸出。

Loader 基礎(chǔ)

由于 Webpack 是運(yùn)行在 Node.js 之上的,一個 Loader 其實(shí)就是一個 Node.js 模塊,這個模塊需要導(dǎo)出一個函數(shù)。 這個導(dǎo)出的函數(shù)的工作就是獲得處理前的原內(nèi)容,對原內(nèi)容執(zhí)行處理后,返回處理后的內(nèi)容。

一個最簡單的 Loader 的源碼如下:

module.exports = function(source) {
  // source 為 compiler 傳遞給 Loader 的一個文件的原內(nèi)容
  // 該函數(shù)需要返回處理后的內(nèi)容,這里簡單起見,直接把原內(nèi)容返回了,相當(dāng)于該 Loader 沒有做任何轉(zhuǎn)換
  return source;
};

由于 Loader 運(yùn)行在 Node.js 中,你可以調(diào)用任何 Node.js 自帶的 API,或者安裝第三方模塊進(jìn)行調(diào)用:

const sass = require("node-sass");
module.exports = function(source) {
  return sass(source);
};
Loader 進(jìn)階

以上只是個最簡單的 Loader,Webpack 還提供一些 API 供 Loader 調(diào)用,下面來一一介紹。

獲得 Loader 的 options

在最上面處理 SCSS 文件的 Webpack 配置中,給 css-loader 傳了 options 參數(shù),以控制 css-loader。 如何在自己編寫的 Loader 中獲取到用戶傳入的 options 呢?需要這樣做:

const loaderUtils = require("loader-utils");
module.exports = function(source) {
  // 獲取到用戶給當(dāng)前 Loader 傳入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};
返回其它結(jié)果

上面的 Loader 都只是返回了原內(nèi)容轉(zhuǎn)換后的內(nèi)容,但有些場景下還需要返回除了內(nèi)容之外的東西。

例如以用 babel-loader 轉(zhuǎn)換 ES6 代碼為例,它還需要輸出轉(zhuǎn)換后的 ES5 代碼對應(yīng)的 Source Map,以方便調(diào)試源碼。 為了把 Source Map 也一起隨著 ES5 代碼返回給 Webpack,可以這樣寫:

module.exports = function(source) {
  // 通過 this.callback 告訴 Webpack 返回的結(jié)果
  this.callback(null, source, sourceMaps);
  // 當(dāng)你使用 this.callback 返回內(nèi)容時,該 Loader 必須返回 undefined,
  // 以讓 Webpack 知道該 Loader 返回的結(jié)果在 this.callback 中,而不是 return 中 
  return;
};

其中的 this.callback 是 Webpack 給 Loader 注入的 API,以方便 Loader 和 Webpack 之間通信。 this.callback 的詳細(xì)使用方法如下:

this.callback(
    // 當(dāng)無法轉(zhuǎn)換原內(nèi)容時,給 Webpack 返回一個 Error
    err: Error | null,
    // 原內(nèi)容轉(zhuǎn)換后的內(nèi)容
    content: string | Buffer,
    // 用于把轉(zhuǎn)換后的內(nèi)容得出原內(nèi)容的 Source Map,方便調(diào)試
    sourceMap?: SourceMap,
    // 如果本次轉(zhuǎn)換為原內(nèi)容生成了 AST 語法樹,可以把這個 AST 返回,
    // 以方便之后需要 AST 的 Loader 復(fù)用該 AST,以避免重復(fù)生成 AST,提升性能
    abstractSyntaxTree?: AST
);
Source Map 的生成很耗時,通常在開發(fā)環(huán)境下才會生成 Source Map,其它環(huán)境下不用生成,以加速構(gòu)建。 為此 Webpack 為 Loader 提供了 this.sourceMap API 去告訴 Loader 當(dāng)前構(gòu)建環(huán)境下用戶是否需要 Source Map。 如果你編寫的 Loader 會生成 Source Map,請考慮到這點(diǎn)。
同步與異步

Loader 有同步和異步之分,上面介紹的 Loader 都是同步的 Loader,因?yàn)樗鼈兊霓D(zhuǎn)換流程都是同步的,轉(zhuǎn)換完成后再返回結(jié)果。 但在有些場景下轉(zhuǎn)換的步驟只能是異步完成的,例如你需要通過網(wǎng)絡(luò)請求才能得出結(jié)果,如果采用同步的方式網(wǎng)絡(luò)請求就會阻塞整個構(gòu)建,導(dǎo)致構(gòu)建非常緩慢。

在轉(zhuǎn)換步驟是異步時,你可以這樣:

module.exports = function(source) {
    // 告訴 Webpack 本次轉(zhuǎn)換是異步的,Loader 會在 callback 中回調(diào)結(jié)果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通過 callback 返回異步執(zhí)行后的結(jié)果
        callback(err, result, sourceMaps, ast);
    });
};
處理二進(jìn)制數(shù)據(jù)

在默認(rèn)的情況下,Webpack 傳給 Loader 的原內(nèi)容都是 UTF-8 格式編碼的字符串。 但有些場景下 Loader 不是處理文本文件,而是處理二進(jìn)制文件,例如 file-loader,就需要 Webpack 給 Loader 傳入二進(jìn)制格式的數(shù)據(jù)。 為此,你需要這樣編寫 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 時,Webpack 傳給 Loader 的 source 是 Buffer 類型的
    source instanceof Buffer === true;
    // Loader 返回的類型也可以是 Buffer 類型的
    // 在 exports.raw !== true 時,Loader 也可以返回 Buffer 類型的結(jié)果
    return source;
};
// 通過 exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進(jìn)制數(shù)據(jù) 
module.exports.raw = true;

以上代碼中最關(guān)鍵的代碼是最后一行 module.exports.raw = true;,沒有該行 Loader 只能拿到字符串。

緩存加速

在有些情況下,有些轉(zhuǎn)換操作需要大量計算非常耗時,如果每次構(gòu)建都重新執(zhí)行重復(fù)的轉(zhuǎn)換操作,構(gòu)建將會變得非常緩慢。 為此,Webpack 會默認(rèn)緩存所有 Loader 的處理結(jié)果,也就是說在需要被處理的文件或者其依賴的文件沒有發(fā)生變化時, 是不會重新調(diào)用對應(yīng)的 Loader 去執(zhí)行轉(zhuǎn)換操作的。

如果你想讓 Webpack 不緩存該 Loader 的處理結(jié)果,可以這樣:

module.exports = function(source) {
  // 關(guān)閉該 Loader 的緩存功能
  this.cacheable(false);
  return source;
};
其它 Loader API

除了以上提到的在 Loader 中能調(diào)用的 Webpack API 外,還存在以下常用 API:

this.context:當(dāng)前處理文件的所在目錄,假如當(dāng)前 Loader 處理的文件是 /src/main.js,則 this.context 就等于 /src。

this.resource:當(dāng)前處理文件的完整請求路徑,包括 querystring,例如 /src/main.js?name=1。

this.resourcePath:當(dāng)前處理文件的路徑,例如 /src/main.js。

this.resourceQuery:當(dāng)前處理文件的 querystring。

this.target:等于 Webpack 配置中的 Target。

this.loadModule:但 Loader 在處理一個文件時,如果依賴其它文件的處理結(jié)果才能得出當(dāng)前文件的結(jié)果時, 就可以通過 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去獲得 request 對應(yīng)文件的處理結(jié)果。

this.resolve:像 require 語句一樣獲得指定文件的完整路徑,使用方法為 resolve(context: string, request: string, callback: function(err, result: string))。

this.addDependency:給當(dāng)前處理文件添加其依賴的文件,以便再其依賴的文件發(fā)生變化時,會重新調(diào)用 Loader 處理該文件。使用方法為 addDependency(file: string)。

this.addContextDependency:和 addDependency 類似,但 addContextDependency 是把整個目錄加入到當(dāng)前正在處理文件的依賴中。使用方法為 addContextDependency(directory: string)

this.clearDependencies:清除當(dāng)前正在處理文件的所有依賴,使用方法為 clearDependencies()

this.emitFile:輸出一個文件,使用方法為 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加載本地 Loader

在開發(fā) Loader 的過程中,為了測試編寫的 Loader 是否能正常工作,需要把它配置到 Webpack 中后,才可能會調(diào)用該 Loader。 在前面的章節(jié)中,使用的 Loader 都是通過 Npm 安裝的,要使用 Loader 時會直接使用 Loader 的名稱,代碼如下:

module.exports = {
  module: {
    rules: [
      {
        test: /.css/,
        use: ["style-loader"],
      },
    ]
  },
};

如果還采取以上的方法去使用本地開發(fā)的 Loader 將會很麻煩,因?yàn)槟阈枰_保編寫的 Loader 的源碼是在 node_modules 目錄下。 為此你需要先把編寫的 Loader 發(fā)布到 Npm 倉庫后再安裝到本地項(xiàng)目使用。

解決以上問題的便捷方法有兩種,分別如下:

Npm link

Npm link 專門用于開發(fā)和調(diào)試本地 Npm 模塊,能做到在不發(fā)布模塊的情況下,把本地的一個正在開發(fā)的模塊的源碼鏈接到項(xiàng)目的 node_modules 目錄下,讓項(xiàng)目可以直接使用本地的 Npm 模塊。 由于是通過軟鏈接的方式實(shí)現(xiàn)的,編輯了本地的 Npm 模塊代碼,在項(xiàng)目中也能使用到編輯后的代碼。

完成 Npm link 的步驟如下:

確保正在開發(fā)的本地 Npm 模塊(也就是正在開發(fā)的 Loader)的 package.json 已經(jīng)正確配置好;

在本地 Npm 模塊根目錄下執(zhí)行 npm link,把本地模塊注冊到全局;

在項(xiàng)目根目錄下執(zhí)行 npm link loader-name,把第2步注冊到全局的本地 Npm 模塊鏈接到項(xiàng)目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模塊名稱。

鏈接好 Loader 到項(xiàng)目后你就可以像使用一個真正的 Npm 模塊一樣使用本地的 Loader 了。

ResolveLoader

ResolveLoader 用于配置 Webpack 如何尋找 Loader。 默認(rèn)情況下只會去 node_modules 目錄下尋找,為了讓 Webpack 加載放在本地項(xiàng)目中的 Loader 需要修改 resolveLoader.modules。

假如本地的 Loader 在項(xiàng)目目錄中的 ./loaders/loader-name 中,則需要如下配置:

module.exports = {
  resolveLoader:{
    // 去哪些目錄下尋找 Loader,有先后順序之分
    modules: ["node_modules","./loaders/"],
  }
}

加上以上配置后, Webpack 會先去 node_modules 項(xiàng)目下尋找 Loader,如果找不到,會再去 ./loaders/ 目錄下尋找。

實(shí)戰(zhàn)

上面講了許多理論,接下來從實(shí)際出發(fā),來編寫一個解決實(shí)際問題的 Loader。

該 Loader 名叫 comment-require-loader,作用是把 JavaScript 代碼中的注釋語法:

// @require "../style/index.css"

轉(zhuǎn)換成:

require("../style/index.css");

該 Loader 的使用場景是去正確加載針對 Fis3 編寫的 JavaScript,這些 JavaScript 中存在通過注釋的方式加載依賴的 CSS 文件。

該 Loader 的使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        use: ["comment-require-loader"],
        // 針對采用了 fis3 CSS 導(dǎo)入語法的 JavaScript 文件通過 comment-require-loader 去轉(zhuǎn)換 
        include: [path.resolve(__dirname, "node_modules/imui")]
      }
    ]
  }
};

該 Loader 的實(shí)現(xiàn)非常簡單,完整代碼如下:

function replace(source) {
    // 使用正則把 // @require "../style/index.css" 轉(zhuǎn)換成 require("../style/index.css");  
    return source.replace(/(// *@require) +(("|").+("|")).*/, "require($2);");
}

module.exports = function (content) {
    return replace(content);
};
編寫 Plugin

Webpack 通過 Plugin 機(jī)制讓其更加靈活,以適應(yīng)各種應(yīng)用場景。 在 Webpack 運(yùn)行的生命周期中會廣播出許多事件,Plugin 可以監(jiān)聽這些事件,在合適的時機(jī)通過 Webpack 提供的 API 改變輸出結(jié)果。

一個最基礎(chǔ)的 Plugin 的代碼是這樣的:

class BasicPlugin{
  // 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置
  constructor(options){
  }

  // Webpack 會調(diào)用 BasicPlugin 實(shí)例的 apply 方法給插件實(shí)例傳入 compiler 對象
  apply(compiler){
    compiler.plugin("compilation",function(compilation) {
    })
  }
}

// 導(dǎo)出 Plugin
module.exports = BasicPlugin;

在使用這個 Plugin 時,相關(guān)配置代碼如下:

const BasicPlugin = require("./BasicPlugin.js");
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Webpack 啟動后,在讀取配置的過程中會先執(zhí)行 new BasicPlugin(options) 初始化一個 BasicPlugin 獲得其實(shí)例。 在初始化 compiler 對象后,再調(diào)用 basicPlugin.apply(compiler) 給插件實(shí)例傳入 compiler 對象。 插件實(shí)例在獲取到 compiler 對象后,就可以通過 compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽到 Webpack 廣播出來的事件。 并且可以通過 compiler 對象去操作 Webpack。

通過以上最簡單的 Plugin 相信你大概明白了 Plugin 的工作原理,但實(shí)際開發(fā)中還有很多細(xì)節(jié)需要注意,下面來詳細(xì)介紹。

CompilerCompilation

在開發(fā) Plugin 時最常用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋梁。 Compiler 和 Compilation 的含義如下:

Compiler 對象包含了 Webpack 環(huán)境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啟動時候被實(shí)例化,它是全局唯一的,可以簡單地把它理解為 Webpack 實(shí)例;

Compilation 對象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng) Webpack 以開發(fā)模式運(yùn)行時,每當(dāng)檢測到一個文件變化,一次新的 Compilation 將被創(chuàng)建。Compilation 對象也提供了很多事件回調(diào)供插件做擴(kuò)展。通過 Compilation 也能讀取到 Compiler 對象。

Compiler 和 Compilation 的區(qū)別在于:Compiler 代表了整個 Webpack 從啟動到關(guān)閉的生命周期,而 Compilation 只是代表了一次新的編譯。

事件流

Webpack 就像一條生產(chǎn)線,要經(jīng)過一系列處理流程后才能將源文件轉(zhuǎn)換成輸出結(jié)果。 這條生產(chǎn)線上的每個處理流程的職責(zé)都是單一的,多個流程之間有存在依賴關(guān)系,只有完成當(dāng)前處理后才能交給下一個流程去處理。 插件就像是一個插入到生產(chǎn)線中的一個功能,在特定的時機(jī)對生產(chǎn)線上的資源做處理。

Webpack 通過 Tapable 來組織這條復(fù)雜的生產(chǎn)線。 Webpack 在運(yùn)行過程中會廣播事件,插件只需要監(jiān)聽它所關(guān)心的事件,就能加入到這條生產(chǎn)線中,去改變生產(chǎn)線的運(yùn)作。 Webpack 的事件流機(jī)制保證了插件的有序性,使得整個系統(tǒng)擴(kuò)展性很好。

Webpack 的事件流機(jī)制應(yīng)用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。Compiler 和 Compilation 都繼承自 Tapable,可以直接在 Compiler 和 Compilation 對象上廣播和監(jiān)聽事件,方法如下:

/**
* 廣播出事件
* event-name 為事件名稱,注意不要和現(xiàn)有的事件重名
* params 為附帶的參數(shù)
*/
compiler.apply("event-name",params);

/**
* 監(jiān)聽名稱為 event-name 的事件,當(dāng) event-name 事件發(fā)生時,函數(shù)就會被執(zhí)行。
* 同時函數(shù)中的 params 參數(shù)為廣播事件時附帶的參數(shù)。
*/
compiler.plugin("event-name",function(params) {

});

同理,compilation.applycompilation.plugin 使用方法和上面一致。

在開發(fā)插件時,你可能會不知道該如何下手,因?yàn)槟悴恢涝摫O(jiān)聽哪個事件才能完成任務(wù)。

在開發(fā)插件時,還需要注意以下兩點(diǎn):

只要能拿到 Compiler 或 Compilation 對象,就能廣播出新的事件,所以在新開發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽使用。

傳給每個插件的 Compiler 和 Compilation 對象都是同一個引用。也就是說在一個插件中修改了 Compiler 或 Compilation 對象上的屬性,會影響到后面的插件。

有些事件是異步的,這些異步的事件會附帶兩個參數(shù),第二個參數(shù)為回調(diào)函數(shù),在插件處理完任務(wù)時需要調(diào)用回調(diào)函數(shù)通知 Webpack,才會進(jìn)入下一處理流程。例如:

 compiler.plugin("emit",function(compilation, callback) {
    // 支持處理邏輯

    // 處理完畢后執(zhí)行 callback 以通知 Webpack 
    // 如果不執(zhí)行 callback,運(yùn)行流程將會一直卡在這不往下執(zhí)行 
    callback();
  });
常用 API

插件可以用來修改輸出文件、增加輸出文件、甚至可以提升 Webpack 性能、等等,總之插件通過調(diào)用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面來介紹一些常用的 API。

讀取輸出資源、代碼塊、模塊及其依賴

有些插件可能需要讀取 Webpack 的處理結(jié)果,例如輸出資源、代碼塊、模塊及其依賴,以便做下一步處理。

emit 事件發(fā)生時,代表源文件的轉(zhuǎn)換和組裝已經(jīng)完成,在這里可以讀取到最終將輸出的資源、代碼塊、模塊及其依賴,并且可以修改輸出資源的內(nèi)容。 插件代碼如下:

See the Pen emit by whjin (@whjin) on CodePen.


監(jiān)聽文件變化

Webpack 會從配置的入口模塊出發(fā),依次找出所有的依賴模塊,當(dāng)入口模塊或者其依賴的模塊發(fā)生變化時, 就會觸發(fā)一次新的 Compilation。

在開發(fā)插件時經(jīng)常需要知道是哪個文件發(fā)生變化導(dǎo)致了新的 Compilation,為此可以使用如下代碼:

See the Pen Compilation by whjin (@whjin) on CodePen.


默認(rèn)情況下 Webpack 只會監(jiān)視入口和其依賴的模塊是否發(fā)生變化,在有些情況下項(xiàng)目可能需要引入新的文件,例如引入一個 HTML 文件。 由于 JavaScript 文件不會去導(dǎo)入 HTML 文件,Webpack 就不會監(jiān)聽 HTML 文件的變化,編輯 HTML 文件時就不會重新觸發(fā)新的 Compilation。 為了監(jiān)聽 HTML 文件的變化,我們需要把 HTML 文件加入到依賴列表中,為此可以使用如下代碼:

compiler.plugin("after-compile", (compilation, callback) => {
  // 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監(jiān)聽 HTML 模塊文件,在 HTML 模版文件發(fā)生變化時重新啟動一次編譯
    compilation.fileDependencies.push(filePath);
    callback();
});
修改輸出資源

有些場景下插件需要修改、增加、刪除輸出的資源,要做到這點(diǎn)需要監(jiān)聽 emit 事件,因?yàn)榘l(fā)生 emit 事件時所有模塊的轉(zhuǎn)換和代碼塊對應(yīng)的文件已經(jīng)生成好, 需要輸出的資源即將輸出,因此 emit 事件是修改 Webpack 輸出資源的最后時機(jī)。

所有需要輸出的資源會存放在 compilation.assets 中,compilation.assets 是一個鍵值對,鍵為需要輸出的文件名稱,值為文件對應(yīng)的內(nèi)容。

設(shè)置 compilation.assets 的代碼如下:

compiler.plugin("emit", (compilation, callback) => {
  // 設(shè)置名稱為 fileName 的輸出資源
  compilation.assets[fileName] = {
    // 返回文件內(nèi)容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二進(jìn)制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, "utf8");
    }
  };
  callback();
});

讀取 compilation.assets 的代碼如下:

compiler.plugin("emit", (compilation, callback) => {
  // 讀取名稱為 fileName 的輸出資源
  const asset = compilation.assets[fileName];
  // 獲取輸出資源的內(nèi)容
  asset.source();
  // 獲取輸出資源的文件大小
  asset.size();
  callback();
});
判斷 Webpack 使用了哪些插件

在開發(fā)一個插件時可能需要根據(jù)當(dāng)前配置是否使用了其它某個插件而做下一步?jīng)Q定,因此需要讀取 Webpack 當(dāng)前的插件配置情況。 以判斷當(dāng)前是否使用了 ExtractTextPlugin 為例,可以使用如下代碼:

// 判斷當(dāng)前配置使用使用了 ExtractTextPlugin,
// compiler 參數(shù)即為 Webpack 在 apply(compiler) 中傳入的參數(shù)
function hasExtractTextPlugin(compiler) {
  // 當(dāng)前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中尋找有沒有 ExtractTextPlugin 的實(shí)例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
實(shí)戰(zhàn)

下面我們舉一個實(shí)際的例子,帶你一步步去實(shí)現(xiàn)一個插件。

該插件的名稱取名叫 EndWebpackPlugin,作用是在 Webpack 即將退出時再附加一些額外的操作,例如在 Webpack 成功編譯和輸出了文件后執(zhí)行發(fā)布操作把輸出的文件上傳到服務(wù)器。 同時該插件還能區(qū)分 Webpack 構(gòu)建是否執(zhí)行成功。使用該插件時方法如下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 時傳入了兩個參數(shù),分別是在成功時的回調(diào)函數(shù)和失敗時的回調(diào)函數(shù);
    new EndWebpackPlugin(() => {
      // Webpack 構(gòu)建成功,并且文件輸出了后會執(zhí)行到這里,在這里可以做發(fā)布文件操作
    }, (err) => {
      // Webpack 構(gòu)建失敗,err 是導(dǎo)致錯誤的原因
      console.error(err);        
    })
  ]
}

要實(shí)現(xiàn)該插件,需要借助兩個事件:

done:在成功構(gòu)建并且輸出了文件后,Webpack 即將退出時發(fā)生;

failed:在構(gòu)建出現(xiàn)異常導(dǎo)致構(gòu)建失敗,Webpack 即將退出時發(fā)生;

實(shí)現(xiàn)該插件非常簡單,完整代碼如下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在構(gòu)造函數(shù)中傳入的回調(diào)函數(shù)
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin("done", (stats) => {
        // 在 done 事件中回調(diào) doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin("failed", (err) => {
        // 在 failed 事件中回調(diào) failCallback
        this.failCallback(err);
    });
  }
}
// 導(dǎo)出插件 
module.exports = EndWebpackPlugin;

從開發(fā)這個插件可以看出,找到合適的事件點(diǎn)去完成功能在開發(fā)插件時顯得尤為重要。 在 工作原理概括 中詳細(xì)介紹過 Webpack 在運(yùn)行過程中廣播出常用事件,你可以從中找到你需要的事件。

調(diào)試 Webpack

在編寫 Webpack 的 Plugin 和 Loader 時,可能執(zhí)行結(jié)果會和你預(yù)期的不一樣,就和你平時寫代碼遇到了奇怪的 Bug 一樣。 對于無法一眼看出問題的 Bug,通常需要調(diào)試程序源碼才能找出問題所在。

雖然可以通過 console.log 的方式完成調(diào)試,但這種方法非常不方便也不優(yōu)雅,本節(jié)將教你如何斷點(diǎn)調(diào)試 工作原理概括 中的插件代碼。 由于 Webpack 運(yùn)行在 Node.js 之上,調(diào)試 Webpack 就相對于調(diào)試 Node.js 程序。

在 Webstorm 中調(diào)試

Webstorm 集成了 Node.js 的調(diào)試工具,因此使用 Webstorm 調(diào)試 Webpack 非常簡單。

1. 設(shè)置斷點(diǎn)

在你認(rèn)為可能出現(xiàn)問題的地方設(shè)下斷點(diǎn),點(diǎn)擊編輯區(qū)代碼左側(cè)出現(xiàn)紅點(diǎn)表示設(shè)置了斷點(diǎn)。

2. 配置執(zhí)行入口

告訴 Webstorm 如何啟動 Webpack,由于 Webpack 實(shí)際上就是一個 Node.js 應(yīng)用,因此需要新建一個 Node.js 類型的執(zhí)行入口。

以上配置中有三點(diǎn)需要注意:

Name 設(shè)置成了 debug webpack,就像設(shè)置了一個別名,方便記憶和區(qū)分;

Working directory 設(shè)置為需要調(diào)試的插件所在的項(xiàng)目的根目錄;

JavaScript file 即 Node.js 的執(zhí)行入口文件,設(shè)置為 Webpack 的執(zhí)行入口文件 node_modules/webpack/bin/webpack.js。

3. 啟動調(diào)試

經(jīng)過以上兩步,準(zhǔn)備工作已經(jīng)完成,下面啟動調(diào)試,啟動時選中前面設(shè)置的 debug webpack。

4. 執(zhí)行到斷點(diǎn)

啟動后程序就會停在斷點(diǎn)所在的位置,在這里你可以方便的查看變量當(dāng)前的狀態(tài),找出問題。

原理總結(jié)

Webpack 是一個龐大的 Node.js 應(yīng)用,如果你閱讀過它的源碼,你會發(fā)現(xiàn)實(shí)現(xiàn)一個完整的 Webpack 需要編寫非常多的代碼。 但你無需了解所有的細(xì)節(jié),只需了解其整體架構(gòu)和部分細(xì)節(jié)即可。

對 Webpack 的使用者來說,它是一個簡單強(qiáng)大的工具; 對 Webpack 的開發(fā)者來說,它是一個擴(kuò)展性的高系統(tǒng)。

Webpack 之所以能成功,在于它把復(fù)雜的實(shí)現(xiàn)隱藏了起來,給用戶暴露出的只是一個簡單的工具,讓用戶能快速達(dá)成目的。 同時整體架構(gòu)設(shè)計合理,擴(kuò)展性高,開發(fā)擴(kuò)展難度不高,通過社區(qū)補(bǔ)足了大量缺失的功能,讓 Webpack 幾乎能勝任任何場景。

通過本章的學(xué)習(xí),希望你不僅能學(xué)會如何編寫 Webpack 擴(kuò)展,也能從中領(lǐng)悟到如何設(shè)計好的系統(tǒng)架構(gòu)。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/107811.html

相關(guān)文章

  • Webpack模塊化原理簡析

    摘要:模塊化原理簡析的核心原理一切皆模塊在中,,靜態(tài)資源文件等都可以視作模塊便于管理,利于重復(fù)利用按需加載進(jìn)行代碼分割,實(shí)現(xiàn)按需加載。模塊化原理以為例,分析構(gòu)建的模塊化方式。 webpack模塊化原理簡析 1.webpack的核心原理 一切皆模塊:在webpack中,css,html.js,靜態(tài)資源文件等都可以視作模塊;便于管理,利于重復(fù)利用; 按需加載:進(jìn)行代碼分割,實(shí)現(xiàn)按需加載。 2...

    tracy 評論0 收藏0
  • webpack原理及使用解析

    摘要:二打包原理和組成。標(biāo)識打包結(jié)束的導(dǎo)出文件路徑和文件名??梢詧?zhí)行范圍更廣的任務(wù)后續(xù)更新可以選擇和模式,內(nèi)部會進(jìn)行相應(yīng)的優(yōu)化。項(xiàng)目中使用本地調(diào)試,配置跨域請求配置服務(wù)器域名配置屬于模塊,配置服務(wù)器域名其中請求接口實(shí)例瀏覽器請求鏈接 webpack是JavaScript前端靜態(tài)資源打包器(module bundler)。 一、實(shí)例應(yīng)用①首先安裝webpack(可以全局安裝,也可以局部安裝) ...

    doodlewind 評論0 收藏0
  • webpack模塊化原理-commonjs

    摘要:函數(shù)首先會檢查是否緩存了已加載的模塊,如果有則直接返回緩存模塊的。調(diào)用完成后,模塊標(biāo)記為已加載。返回模塊的內(nèi)容。細(xì)心的你一定會發(fā)現(xiàn),文章到這里只介紹了對的實(shí)現(xiàn),那么是如何實(shí)現(xiàn)的呢歡迎閱讀本系列第二篇模塊化原理。 我們都知道,webpack作為一個構(gòu)建工具,解決了前端代碼缺少模塊化能力的問題。我們寫的代碼,經(jīng)過webpack構(gòu)建和包裝之后,能夠在瀏覽器以模塊化的方式運(yùn)行。這些能力,都是因...

    molyzzx 評論0 收藏0
  • webpack hot-module-replacement 原理&踩坑

    摘要:原理踩坑起因最近在做框架的熱更新,記錄一下的原理和小坑。文件系統(tǒng)接收更改并通知。運(yùn)行時通過請求這些更新。類似的問題還有很多,事件綁定手動插入并且沒有銷毀的定時器等,記得把這些副作用一起干掉。參考官方文檔原理分析與實(shí)現(xiàn) webpack hot-module-replacement 原理&踩坑 起因 最近在做san框架的熱更新,記錄一下webpack HMR的原理和小坑。 什么是HMR? ...

    elva 評論0 收藏0
  • webpack組織模塊的原理 - external模塊

    摘要:所以通常情況下當(dāng)你的庫需要依賴到例如,這樣的通用模塊時,我們可以不將它打包進(jìn),而是在的配置中聲明這就是在告訴請不要將這個模塊注入編譯后的文件里,對于我源代碼里出現(xiàn)的任何這個模塊的語句,請將它保留。 這篇文章討論Webpack打包library時經(jīng)常需要用到的一個選項(xiàng)external,它用于避免將一些很通用的模塊打包進(jìn)你發(fā)布的library里,而是選擇把它們聲明成external的模塊,...

    Lavender 評論0 收藏0
  • Omi原理-環(huán)境搭建

    摘要:相關(guān)依賴有和其余都是單元測試相關(guān)依賴注意,這里使用了。因?yàn)槭褂每蚣苤С趾褪褂檬菫榱嗽趩卧獪y試?yán)锩媸褂玫暮偷日Z法。腳本其中生成目錄的文件執(zhí)行單元測試編譯的編譯的在中,會根據(jù)去設(shè)置不同的入口文件。 環(huán)境搭建 Omi框架使用 Webpack + ES6 的方式去開發(fā);使用karma+jasmine來作為Omi的測試工具。 Karma介紹 Karma是一個基于Node.js的JavaScrip...

    lncwwn 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<