webpack道理

webpack道理

檢察統統文檔頁面:
前端開闢文檔,獵取更多信息。

原文鏈接:
webpack道理,原文廣告模態框遮擋,瀏覽體驗不好,所以整頓成本文,輕易查找。

事變道理歸納綜合

基本觀點

在相識 Webpack 道理前,須要掌握以下幾个中心觀點,以輕易背面的邃曉:

  • Entry:進口,Webpack 實行構建的第一步將從 Entry 最先,可籠統成輸入。
  • Module:模塊,在 Webpack 里統統皆模塊,一個模塊對應着一個文件。Webpack 會從設置的 Entry 最先遞歸找出統統依靠的模塊。
  • Chunk:代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼兼并與支解。
  • Loader:模塊轉換器,用於把模塊原內容依據需求轉換成新內容。
  • Plugin:擴大插件,在 Webpack 構建流程中的特定時機遇播送出對應的事宜,插件能夠監聽這些事宜的發作,在特定機遇做對應的事變。

流程歸納綜合

Webpack 的運轉流程是一個串行的歷程,從啟動到終了會順次實行以下流程:

  1. 初始化參數:從設置文件和 Shell 語句中讀取與兼并參數,得出終究的參數;
  2. 最先編譯:用上一步取得的參數初始化 Compiler 對象,加載統統設置的插件,實行對象的 run 要領最先實行編譯;
  3. 肯定進口:依據設置中的 entry 找出統統的進口文件;
  4. 編譯模塊:從進口文件動身,挪用統統設置的 Loader 對模塊舉行翻譯,再找出該模塊依靠的模塊,再遞歸本步驟直到統統進口依靠的文件都經由了本步驟的處置懲罰;
  5. 完成模塊編譯:在經由第4步運用 Loader 翻譯完統統模塊后,取得了每一個模塊被翻譯后的終究內容以及它們之間的依靠關聯;
  6. 輸出資本:依據進口和模塊之間的依靠關聯,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個零丁的文件加入到輸出列表,這步是能夠修正輸出內容的末了機遇;
  7. 輸出完成:在肯定好輸出內容后,依據設置肯定輸出的途徑和文件名,把文件內容寫入到文件體系。

在以上歷程當中,Webpack 會在特定的時刻點播送出特定的事宜,插件在監聽到感興趣的事宜後會實行特定的邏輯,而且插件能夠挪用 Webpack 供應的 API 轉變 Webpack 的運轉效果。

流程細節

Webpack 的構建流程能夠分為以下三大階段:

  1. 初始化:啟動構建,讀取與兼并設置參數,加載 Plugin,實例化 Compiler。
  2. 編譯:從 Entry 發出,針對每一個 Module 串行挪用對應的 Loader 去翻譯文件內容,再找到該 Module 依靠的 Module,遞歸地舉行編譯處置懲罰。
  3. 輸出:對編譯后的 Module 組合成 Chunk,把 Chunk 轉換成文件,輸出到文件體系。

如果只實行一次構建,以上階段將會依據遞次各實行一次。但在開啟監聽形式下,流程將變成以下:

《webpack道理》

在每一個大階段中又會發作很多事宜,Webpack 會把這些事宜播送出來供應 Plugin 運用,下面來逐一引見。

初始化階段

事宜名詮釋
初始化參數從設置文件和 Shell 語句中讀取與兼并參數,得出終究的參數。 這個歷程當中還會實行設置文件中的插件實例化語句 new Plugin()
實例化 Compiler用上一步取得的參數初始化 Compiler 實例,Compiler 擔任文件監聽和啟動編譯。Compiler 實例中包含了完全的 Webpack 設置,全局只要一個 Compiler 實例。
加載插件順次挪用插件的 apply 要領,讓插件能夠監聽後續的統統事宜節點。同時給插件傳入 compiler 實例的援用,以輕易插件經由過程 compiler 挪用 Webpack 供應的 API。
environment最先運用 Node.js 作風的文件體繫到 compiler 對象,以輕易後續的文件尋覓和讀取。
entry-option讀取設置的 Entrys,為每一個 Entry 實例化一個對應的 EntryPlugin,為背面該 Entry 的遞歸剖析事變做準備。
after-plugins挪用完統統內置的和設置的插件的 apply 要領。
after-resolvers依據設置初始化完 resolverresolver 擔任在文件體系中尋覓指定途徑的文件。
空格空格
空格空格
空格空格

編譯階段

事宜名詮釋
run啟動一次新的編譯。
watch-runrun 類似,辨別在於它是在監聽形式下啟動的編譯,在這個事宜中能夠獵取到是哪些文件發作了變化致使從新啟動一次新的編譯。
compile該事宜是為了關照插件一次新的編譯將要啟動,同時會給插件帶上 compiler 對象。
compilationWebpack 以開闢形式運轉時,每當檢測到文件變化,一次新的 Compilation 將被建立。一個 Compilation 對象包含了當前的模塊資本、編譯天生資本、變化的文件等。Compilation 對象也供應了很多事宜回調供插件做擴大。
make一個新的 Compilation 建立終了,行將從 Entry 最先讀取文件,依據文件範例和設置的 Loader 對文件舉行編譯,編譯完后再找出該文件依靠的文件,遞歸的編譯和剖析。
after-compile一次 Compilation 實行完成。
invalid當碰到文件不存在、文件編譯毛病等異常時會觸發該事宜,該事宜不會致使 Webpack 退出。
空格空格
空格空格

在編譯階段中,最主要的要數 compilation 事宜了,因為在 compilation 階段挪用了 Loader 完成了每一個模塊的轉換操縱,在 compilation 階段又包含很多小的事宜,它們離別是:

事宜名詮釋
build-module運用對應的 Loader 去轉換一個模塊。
normal-module-loader在用 Loader 對一個模塊轉換完后,運用 acorn 剖析轉換后的內容,輸出對應的籠統語法樹(AST),以輕易 Webpack 背面臨代碼的剖析。
program從設置的進口模塊最先,剖析其 AST,當碰到 require 等導入別的模塊語句時,便將其加入到依靠的模塊列表,同時對新找出的依靠模塊遞歸剖析,終究搞清統統模塊的依靠關聯。
seal統統模塊及其依靠的模塊都經由過程 Loader 轉換完成后,依據依靠關聯最先天生 Chunk。

輸出階段

事宜名詮釋
should-emit統統須要輸出的文件已天生好,訊問插件哪些文件須要輸出,哪些不須要。
emit肯定好要輸出哪些文件后,實行文件輸出,能夠在這裏獵取和修正輸出內容。
after-emit文件輸出終了。
done勝利完成一次完成的編譯和輸出流程。
failed如果在編譯和輸出流程中碰到異常致使 Webpack 退出時,就會直接跳轉到本步驟,插件能夠在本事宜中獵取到細緻的毛病緣由。

在輸出階段已取得了各個模塊經由轉換后的效果和其依靠關聯,而且把相干模塊組合在一同構成一個個 Chunk。 在輸出階段會依據 Chunk 的範例,運用對應的模版天生終究要要輸出的文件內容。

輸出文件剖析

雖然在前面的章節中你學會了怎樣運用 Webpack ,也大抵曉得其事變道理,但是你想過 Webpack 輸出的 bundle.js 是什麼模樣的嗎? 為何本來一個個的模塊文件被兼并成了一個零丁的文件?為何 bundle.js 能直接運轉在瀏覽器中? 本節將詮釋清晰以上題目。

先來看看由 裝置與運用 中最簡樸的項目構建出的 bundle.js 文件內容,代碼以下:

<p data-height=”565″ data-theme-id=”0″ data-slug-hash=”NMQzxz” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”bundle.js” class=”codepen”>See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async src=”https://static.codepen.io/ass…;></script>

以上看上去龐雜的代碼現實上是一個馬上實行函數,能夠簡寫為以下:

(function(modules) {

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

  // 實行寄存統統模塊數組中的第0個模塊
  __webpack_require__(0);

})([/*寄存統統模塊的數組*/])

bundle.js 能直接運轉在瀏覽器中的緣由在於輸出的文件中經由過程 __webpack_require__ 函數定義了一個能夠在瀏覽器中實行的加載函數來模仿 Node.js 中的 require 語句。

本來一個個自力的模塊文件被兼并到了一個零丁的 bundle.js 的緣由在於瀏覽器不能像 Node.js 那樣疾速地去當地加載一個個模塊文件,而必需經由過程收集要求去加載還未取得的文件。 如果模塊數目很多,加載時刻會很長,因而把統統模塊都寄存在了數組中,實行一次收集加載。

如果仔細剖析 __webpack_require__ 函數的完成,你另有發明 Webpack 做了緩存優化: 實行加載過的模塊不會再實行第二次,實行效果會緩存在內存中,當某個模塊第二次被接見時會直接去內存中讀取被緩存的返回值。

支解代碼時的輸出

比方把源碼中的 main.js 修正為以下:

// 異步加載 show.js
import('./show').then((show) => {
  // 實行 show 函數
  show('Webpack');
});

從新構建後會輸出兩個文件,離別是實行進口文件 bundle.js 和 異步加載文件 0.bundle.js

个中 0.bundle.js 內容以下:

// 加載在本文件(0.bundle.js)中包含的模塊
webpackJsonp(
  // 在別的文件中寄存着的模塊的 ID
  [0],
  // 本文件所包含的模塊
  [
    // show.js 所對應的模塊
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js 內容以下:

<p data-height=”565″ data-theme-id=”0″ data-slug-hash=”yjmRyG” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”bundle.js” class=”codepen”>See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async src=”https://static.codepen.io/ass…;></script>

這裏的 bundle.js 和上面所講的 bundle.js 異常類似,辨別在於:

  • 多了一個 __webpack_require__.e 用於加載被支解出去的,須要異步加載的 Chunk 對應的文件;
  • 多了一個 webpackJsonp 函數用於從異步加載的文件中裝置模塊。

在運用了 CommonsChunkPlugin 去提取大眾代碼時輸出的文件和運用了異步加載時輸出的文件是一樣的,都邑有 __webpack_require__.ewebpackJsonp。 緣由在於提取大眾代碼和異步加載本質上都是代碼支解。

編寫 Loader

Loader 就像是一個通譯員,能把源文件經由轉化后輸出新的效果,而且一個文件還能夠鏈式的經由多個通譯員翻譯。

以處置懲罰 SCSS 文件為例:

  • SCSS 源代碼會先交給 sass-loader 把 SCSS 轉換成 CSS;
  • sass-loader 輸出的 CSS 交給 css-loader 處置懲罰,找出 CSS 中依靠的資本、緊縮 CSS 等;
  • css-loader 輸出的 CSS 交給 style-loader 處置懲罰,轉換成經由過程劇本加載的 JavaScript 代碼;

能夠看出以上的處置懲罰歷程須要有遞次的鏈式實行,先 sass-loadercss-loaderstyle-loader。 以上處置懲罰的 Webpack 相干設置以下:

<p data-height=”365″ data-theme-id=”0″ data-slug-hash=”YLmbeQ” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”編寫 Loader” class=”codepen”>See the Pen 編寫 Loader by whjin (@whjin) on CodePen.</p>
<script async src=”https://static.codepen.io/ass…;></script>

Loader 的職責

由上面的例子能夠看出:一個 Loader 的職責是單一的,只須要完成一種轉換。 如果一個源文件須要閱歷多步轉換才一般運用,就經由過程多個 Loader 去轉換。 在挪用多個 Loader 去轉換一個文件時,每一個 Loader 會鏈式的遞次實行, 第一個 Loader 將會拿到需處置懲罰的原內容,上一個 Loader 處置懲罰后的效果會傳給下一個接着處置懲罰,末了的 Loader 將處置懲罰后的終究效果返回給 Webpack。

所以,在你開闢一個 Loader 時,請堅持其職責的單一性,你只需體貼輸入和輸出。

Loader 基本

因為 Webpack 是運轉在 Node.js 之上的,一個 Loader 實在就是一個 Node.js 模塊,這個模塊須要導出一個函數。 這個導出的函數的事變就是取得處置懲罰前的原內容,對原內容實行處置懲罰后,返回處置懲罰后的內容。

一個最簡樸的 Loader 的源碼以下:

module.exports = function(source) {
  // source 為 compiler 傳遞給 Loader 的一個文件的原內容
  // 該函數須要返回處置懲罰后的內容,這裏簡樸起見,直接把原內容返回了,相當於該 Loader 沒有做任何轉換
  return source;
};

因為 Loader 運轉在 Node.js 中,你能夠挪用任何 Node.js 自帶的 API,或許裝置第三方模塊舉行挪用:

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};

Loader 進階

以上只是個最簡樸的 Loader,Webpack 還供應一些 API 供 Loader 挪用,下面來逐一引見。

取得 Loader 的 options

在最上面處置懲罰 SCSS 文件的 Webpack 設置中,給 css-loader 傳了 options 參數,以掌握 css-loader。 怎樣在本身編寫的 Loader 中獵取到用戶傳入的 options 呢?須要如許做:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 獵取到用戶給當前 Loader 傳入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

返回別的效果

上面的 Loader 都只是返回了原內容轉換后的內容,但有些場景下還須要返回除了內容以外的東西。

比方以用 babel-loader 轉換 ES6 代碼為例,它還須要輸出轉換后的 ES5 代碼對應的 Source Map,以輕易調試源碼。 為了把 Source Map 也一同跟着 ES5 代碼返回給 Webpack,能夠如許寫:

module.exports = function(source) {
  // 經由過程 this.callback 關照 Webpack 返回的效果
  this.callback(null, source, sourceMaps);
  // 當你運用 this.callback 返回內容時,該 Loader 必需返回 undefined,
  // 以讓 Webpack 曉得該 Loader 返回的效果在 this.callback 中,而不是 return 中 
  return;
};

个中的 this.callback 是 Webpack 給 Loader 注入的 API,以輕易 Loader 和 Webpack 之間通訊。 this.callback 的細緻運用要領以下:

this.callback(
    // 當沒法轉換原內容時,給 Webpack 返回一個 Error
    err: Error | null,
    // 原內容轉換后的內容
    content: string | Buffer,
    // 用於把轉換后的內容得出原內容的 Source Map,輕易調試
    sourceMap?: SourceMap,
    // 如果本次轉換為原內容天生了 AST 語法樹,能夠把這個 AST 返回,
    // 以輕易以後須要 AST 的 Loader 復用該 AST,以防止反覆天生 AST,提拔機能
    abstractSyntaxTree?: AST
);

Source Map 的天生很耗時,一般在開闢環境下才會天生 Source Map,別的環境下不必天生,以加快構建。 為此 Webpack 為 Loader 供應了
this.sourceMap API 去關照 Loader 當前構建環境下用戶是不是須要 Source Map。 如果你編寫的 Loader 會天生 Source Map,請考慮到這點。

同步與異步

Loader 有同步和異步之分,上面引見的 Loader 都是同步的 Loader,因為它們的轉換流程都是同步的,轉換完成后再返回效果。 但在有些場景下轉換的步驟只能是異步完成的,比方你須要經由過程收集要求才得出效果,如果採納同步的體式格局收集要求就會壅塞全部構建,致使構建異常遲緩。

在轉換步驟是異步時,你能夠如許:

module.exports = function(source) {
    // 關照 Webpack 本次轉換是異步的,Loader 會在 callback 中回調效果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 經由過程 callback 返回異步實行后的效果
        callback(err, result, sourceMaps, ast);
    });
};

處置懲罰二進制數據

在默許的狀況下,Webpack 傳給 Loader 的原內容都是 UTF-8 花樣編碼的字符串。 但有些場景下 Loader 不是處置懲罰文本文件,而是處置懲罰二進制文件,比方 file-loader,就須要 Webpack 給 Loader 傳入二進制花樣的數據。 為此,你須要如許編寫 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 時,Webpack 傳給 Loader 的 source 是 Buffer 範例的
    source instanceof Buffer === true;
    // Loader 返回的範例也能夠是 Buffer 範例的
    // 在 exports.raw !== true 時,Loader 也能夠返回 Buffer 範例的效果
    return source;
};
// 經由過程 exports.raw 屬性關照 Webpack 該 Loader 是不是須要二進制數據 
module.exports.raw = true;

以上代碼中最癥結的代碼是末了一行 module.exports.raw = true;,沒有該行 Loader 只能拿到字符串。

緩存加快

在有些狀況下,有些轉換操縱須要大批盤算異常耗時,如果每次構定都從新實行反覆的轉換操縱,構建將會變得異常遲緩。 為此,Webpack 會默許緩存統統 Loader 的處置懲罰效果,也就是說在須要被處置懲罰的文件或許其依靠的文件沒有發作變化時, 是不會從新挪用對應的 Loader 去實行轉換操縱的。

如果你想讓 Webpack 不緩存該 Loader 的處置懲罰效果,能夠如許:

module.exports = function(source) {
  // 封閉該 Loader 的緩存功用
  this.cacheable(false);
  return source;
};

別的 Loader API

除了以上提到的在 Loader 中能挪用的 Webpack API 外,還存在以下常常使用 API:

  • this.context:當前處置懲罰文件的地點目次,如果當前 Loader 處置懲罰的文件是 /src/main.js,則 this.context 就即是 /src
  • this.resource:當前處置懲罰文件的完全要求途徑,包含 querystring,比方 /src/main.js?name=1
  • this.resourcePath:當前處置懲罰文件的途徑,比方 /src/main.js
  • this.resourceQuery:當前處置懲罰文件的 querystring
  • this.target:即是 Webpack 設置中的 Target。
  • this.loadModule:但 Loader 在處置懲罰一個文件時,如果依靠別的文件的處置懲罰效果才得出當前文件的效果時, 就可以夠經由過程 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去取得 request 對應文件的處置懲罰效果。
  • this.resolve:像 require 語句一樣取得指定文件的完全途徑,運用要領為 resolve(context: string, request: string, callback: function(err, result: string))
  • this.addDependency:給當前處置懲罰文件增添其依靠的文件,以便再其依靠的文件發作變化時,會從新挪用 Loader 處置懲罰該文件。運用要領為 addDependency(file: string)
  • this.addContextDependency:和 addDependency 類似,但 addContextDependency 是把全部目次加入到當前正在處置懲罰文件的依靠中。運用要領為 addContextDependency(directory: string)
  • this.clearDependencies:消滅當前正在處置懲罰文件的統統依靠,運用要領為 clearDependencies()
  • this.emitFile:輸出一個文件,運用要領為 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加載當地 Loader

在開闢 Loader 的歷程當中,為了測試編寫的 Loader 是不是能一般事變,須要把它設置到 Webpack 中后,才能夠會挪用該 Loader。 在前面的章節中,運用的 Loader 都是經由過程 Npm 裝置的,要運用 Loader 時會直接運用 Loader 的稱號,代碼以下:

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

如果還採用以上的要領去運用當地開闢的 Loader 將會很貧苦,因為你須要確保編寫的 Loader 的源碼是在 node_modules 目次下。 為此你須要先把編寫的 Loader 宣布到 Npm 堆棧后再裝置到當地項目運用。

處理以上題目標便利要領有兩種,離別以下:

Npm link

Npm link 特地用於開闢和調試當地 Npm 模塊,能做到在不宣布模塊的狀況下,把當地的一個正在開闢的模塊的源碼鏈接到項目標 node_modules 目次下,讓項目能夠直接運用當地的 Npm 模塊。 因為是經由過程軟鏈接的體式格局完成的,編輯了當地的 Npm 模塊代碼,在項目中也能運用到編輯后的代碼。

完成 Npm link 的步驟以下:

  • 確保正在開闢的當地 Npm 模塊(也就是正在開闢的 Loader)的 package.json 已準確設置好;
  • 在當地 Npm 模塊根目次下實行 npm link,把當地模塊註冊到全局;
  • 在項目根目次下實行 npm link loader-name,把第2步註冊到全局的當地 Npm 模塊鏈接到項目標 node_moduels 下,个中的 loader-name 是指在第1步中的 package.json 文件中設置的模塊稱號。

鏈接好 Loader 到項目后你就可以夠像運用一個真正的 Npm 模塊一樣運用當地的 Loader 了。

ResolveLoader

ResolveLoader 用於設置 Webpack 怎樣尋覓 Loader。 默許狀況下只會去 node_modules 目次下尋覓,為了讓 Webpack 加載放在當地項目中的 Loader 須要修正 resolveLoader.modules

如果當地的 Loader 在項目目次中的 ./loaders/loader-name 中,則須要以下設置:


module.exports = {
  resolveLoader:{
    // 去哪些目次下尋覓 Loader,有先後遞次之分
    modules: ['node_modules','./loaders/'],
  }
}

加上以上設置后, Webpack 會先去 node_modules 項面前目今尋覓 Loader,如果找不到,會再去 ./loaders/ 目次下尋覓。

實戰

上面講了很多理論,接下來從現實動身,來編寫一個處理現實題目標 Loader。

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

// @require '../style/index.css'

轉換成:

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

該 Loader 的運用場景是去準確加載針對 Fis3 編寫的 JavaScript,這些 JavaScript 中存在經由過程解釋的體式格局加載依靠的 CSS 文件。

該 Loader 的運用要領以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 針對採納了 fis3 CSS 導入語法的 JavaScript 文件經由過程 comment-require-loader 去轉換 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};

該 Loader 的完成異常簡樸,完全代碼以下:

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

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

編寫 Plugin

Webpack 經由過程 Plugin 機制讓其越發天真,以順應種種運用場景。 在 Webpack 運轉的生命周期中會播送出很多事宜,Plugin 能夠監聽這些事宜,在適宜的機遇經由過程 Webpack 供應的 API 轉變輸出效果。

一個最基本的 Plugin 的代碼是如許的:

class BasicPlugin{
  // 在構造函數中獵取用戶給該插件傳入的設置
  constructor(options){
  }

  // Webpack 會挪用 BasicPlugin 實例的 apply 要領給插件實例傳入 compiler 對象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 導出 Plugin
module.exports = BasicPlugin;

在運用這個 Plugin 時,相干設置代碼以下:

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

Webpack 啟動后,在讀取設置的歷程當中會先實行 new BasicPlugin(options) 初始化一個 BasicPlugin 取得實在例。 在初始化 compiler 對象后,再挪用 basicPlugin.apply(compiler) 給插件實例傳入 compiler 對象。 插件實例在獵取到 compiler 對象后,就可以夠經由過程 compiler.plugin(事宜稱號, 回調函數) 監聽到 Webpack 播送出來的事宜。 而且能夠經由過程 compiler 對象去操縱 Webpack。

經由過程以上最簡樸的 Plugin 相信你也許邃曉了 Plugin 的事變道理,但現實開闢中另有很多細節須要注重,下面來細緻引見。

CompilerCompilation

在開闢 Plugin 時最常常使用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋樑。 Compiler 和 Compilation 的寄義以下:

  • Compiler 對象包含了 Webpack 環境統統的的設置信息,包含 optionsloadersplugins 這些信息,這個對象在 Webpack 啟動時刻被實例化,它是全局唯一的,能夠簡樸地把它邃曉為 Webpack 實例;
  • Compilation 對象包含了當前的模塊資本、編譯天生資本、變化的文件等。當 Webpack 以開闢形式運轉時,每當檢測到一個文件變化,一次新的 Compilation 將被建立。Compilation 對象也供應了很多事宜回調供插件做擴大。經由過程 Compilation 也能讀取到 Compiler 對象。

Compiler 和 Compilation 的辨別在於:Compiler 代表了全部 Webpack 從啟動到封閉的生命周期,而 Compilation 只是代表了一次新的編譯。

事宜流

Webpack 就像一條臨盆線,要經由一系列處置懲罰流程后才將源文件轉換成輸出效果。 這條臨盆線上的每一個處置懲罰流程的職責都是單一的,多個流程之間有存在依靠關聯,只要完成當前處置懲罰后才交給下一個流程去處置懲罰。 插件就像是一個插進去到臨盆線中的一個功用,在特定的機遇對臨盆線上的資本做處置懲罰。

Webpack 經由過程 Tapable 來構造這條龐雜的臨盆線。 Webpack 在運轉歷程當中會播送事宜,插件只須要監聽它所體貼的事宜,就可以加入到這條臨盆線中,去轉變臨盆線的運作。 Webpack 的事宜流機制保證了插件的有序性,使得全部體系擴大性很好。

Webpack 的事宜流機制運用了觀察者形式,和 Node.js 中的 EventEmitter 異常類似。Compiler 和 Compilation 都繼續自 Tapable,能夠直接在 Compiler 和 Compilation 對象上播送和監聽事宜,要領以下:

/**
* 播送出事宜
* event-name 為事宜稱號,注重不要和現有的事宜重名
* params 為附帶的參數
*/
compiler.apply('event-name',params);

/**
* 監聽稱號為 event-name 的事宜,當 event-name 事宜發作時,函數就會被實行。
* 同時函數中的 params 參數為播送事宜時附帶的參數。
*/
compiler.plugin('event-name',function(params) {

});

同理,compilation.applycompilation.plugin 運用要領和上面一致。

在開闢插件時,你能夠會不曉得該怎樣動手,因為你不曉得該監聽哪一個事宜才完成使命。

在開闢插件時,還須要注重以下兩點:

  • 只要能拿到 Compiler 或 Compilation 對象,就可以播送出新的事宜,所以在新開闢的插件中也能播送出事宜,給別的插件監聽運用。
  • 傳給每一個插件的 Compiler 和 Compilation 對象都是同一個援用。也就是說在一個插件中修正了 Compiler 或 Compilation 對象上的屬性,會影響到背面的插件。
  • 有些事宜是異步的,這些異步的事宜會附帶兩個參數,第二個參數為回調函數,在插件處置懲罰完使命時須要挪用回調函數關照 Webpack,才會進入下一處置懲罰流程。比方:
 compiler.plugin('emit',function(compilation, callback) {
    // 支撐處置懲罰邏輯

    // 處置懲罰終了后實行 callback 以關照 Webpack 
    // 如果不實行 callback,運轉流程將會一向卡在這不往下實行 
    callback();
  });

常常使用 API

插件能夠用來修正輸出文件、增添輸出文件、以至能夠提拔 Webpack 機能、等等,總之插件經由過程挪用 Webpack 供應的 API 能完成很多事變。 因為 Webpack 供應的 API 異常多,有很多 API 很少用的上,又加上篇幅有限,下面來引見一些常常使用的 API。

讀取輸出資本、代碼塊、模塊及其依靠

有些插件能夠須要讀取 Webpack 的處置懲罰效果,比方輸出資本、代碼塊、模塊及其依靠,以便做下一步處置懲罰。

emit 事宜發作時,代表源文件的轉換和組裝已完成,在這裡能夠讀取到終究將輸出的資本、代碼塊、模塊及其依靠,而且能夠修正輸出資本的內容。 插件代碼以下:

<p data-height=”585″ data-theme-id=”0″ data-slug-hash=”RJwjPj” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”emit” class=”codepen”>See the Pen emit by whjin (@whjin) on CodePen.</p>
<script async src=”https://static.codepen.io/ass…;></script>

監聽文件變化

Webpack 會從設置的進口模塊動身,順次找出統統的依靠模塊,當進口模塊或許其依靠的模塊發作變化時, 就會觸發一次新的 Compilation。

在開闢插件時常常須要曉得是哪一個文件發作變化致使了新的 Compilation,為此能夠運用以下代碼:

<p data-height=”255″ data-theme-id=”0″ data-slug-hash=”jKOabJ” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”Compilation” class=”codepen”>See the Pen Compilation by whjin (@whjin) on CodePen.</p>
<script async src=”https://static.codepen.io/ass…;></script>

默許狀況下 Webpack 只會看管進口和其依靠的模塊是不是發作變化,在有些狀況下項目能夠須要引入新的文件,比方引入一個 HTML 文件。 因為 JavaScript 文件不會去導入 HTML 文件,Webpack 就不會監聽 HTML 文件的變化,編輯 HTML 文件時就不會從新觸發新的 Compilation。 為了監聽 HTML 文件的變化,我們須要把 HTML 文件加入到依靠列表中,為此能夠運用以下代碼:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件增添到文件依靠列表,好讓 Webpack 去監聽 HTML 模塊文件,在 HTML 模版文件發作變化時從新啟動一次編譯
    compilation.fileDependencies.push(filePath);
    callback();
});

修正輸出資本

有些場景下插件須要修正、增添、刪除輸出的資本,要做到這點須要監聽 emit 事宜,因為發作 emit 事宜時統統模塊的轉換和代碼塊對應的文件已天生好, 須要輸出的資本行將輸出,因而 emit 事宜是修正 Webpack 輸出資本的末了機遇。

統統須要輸出的資本會寄存在 compilation.assets 中,compilation.assets 是一個鍵值對,鍵為須要輸出的文件稱號,值為文件對應的內容。

設置 compilation.assets 的代碼以下:

compiler.plugin('emit', (compilation, callback) => {
  // 設置稱號為 fileName 的輸出資本
  compilation.assets[fileName] = {
    // 返迴文件內容
    source: () => {
      // fileContent 既能夠是代表文本文件的字符串,也能夠是代表二進制文件的 Buffer
      return fileContent;
      },
    // 返迴文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

讀取 compilation.assets 的代碼以下:


compiler.plugin('emit', (compilation, callback) => {
  // 讀取稱號為 fileName 的輸出資本
  const asset = compilation.assets[fileName];
  // 獵取輸出資本的內容
  asset.source();
  // 獵取輸出資本的文件大小
  asset.size();
  callback();
});

推斷 Webpack 運用了哪些插件

在開闢一個插件時能夠須要依據當前設置是不是運用了別的某個插件而做下一步決議,因而須要讀取 Webpack 當前的插件設置狀況。 以推斷當前是不是運用了 ExtractTextPlugin 為例,能夠運用以下代碼:

// 推斷當前設置運用運用了 ExtractTextPlugin,
// compiler 參數即為 Webpack 在 apply(compiler) 中傳入的參數
function hasExtractTextPlugin(compiler) {
  // 當前設置統統運用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中尋覓有無 ExtractTextPlugin 的實例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

實戰

下面我們舉一個現實的例子,帶你一步步去完成一個插件。

該插件的稱號取名叫 EndWebpackPlugin,作用是在 Webpack 行將退出時再附加一些分外的操縱,比方在 Webpack 勝利編譯和輸出了文件后實行宣布操縱把輸出的文件上傳到服務器。 同時該插件還能辨別 Webpack 構建是不是實行勝利。運用該插件時要領以下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 時傳入了兩個參數,離別是在勝利時的回調函數和失利時的回調函數;
    new EndWebpackPlugin(() => {
      // Webpack 構建勝利,而且文件輸出了後會實行到這裏,在這裡能夠做宣布文件操縱
    }, (err) => {
      // Webpack 構建失利,err 是致使毛病的緣由
      console.error(err);        
    })
  ]
}

要完成該插件,須要藉助兩個事宜:

  • done:在勝利構建而且輸出了文件后,Webpack 行將退出時發作;
  • failed:在構建湧現異常致使構建失利,Webpack 行將退出時發作;

完成該插件異常簡樸,完全代碼以下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在構造函數中傳入的回調函數
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

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

從開闢這個插件能夠看出,找到適宜的事宜點去完勝利能在開闢插件時顯得尤為主要。 在 事變道理歸納綜合 中細緻引見過 Webpack 在運轉歷程當中播送出常常使用事宜,你能夠從中找到你須要的事宜。

調試 Webpack

在編寫 Webpack 的 Plugin 和 Loader 時,能夠實行效果會和你預期的不一樣,就和你日常平凡寫代碼碰到了新鮮的 Bug 一樣。 關於沒法一眼看出題目標 Bug,一般須要調試順序源碼才找出題目地點。

雖然能夠經由過程 console.log 的體式格局完成調試,但這類要領異常不輕易也不文雅,本節將教你怎樣斷點調試 事變道理歸納綜合 中的插件代碼。 因為 Webpack 運轉在 Node.js 之上,調試 Webpack 就相關於調試 Node.js 順序。

在 Webstorm 中調試

Webstorm 集成了 Node.js 的調試東西,因而運用 Webstorm 調試 Webpack 異常簡樸。

1. 設置斷點

在你以為能夠湧現題目標處所設下斷點,點擊編輯區代碼左邊湧現紅點示意設置了斷點。

2. 設置實行進口

關照 Webstorm 怎樣啟動 Webpack,因為 Webpack 現實上就是一個 Node.js 運用,因而須要新建一個 Node.js 範例的實行進口。

以上設置中有三點須要注重:

  • Name 設置成了 debug webpack,就像設置了一個別號,輕易影象和辨別;
  • Working directory 設置為須要調試的插件地點的項目標根目次;
  • JavaScript file 即 Node.js 的實行進口文件,設置為 Webpack 的實行進口文件 node_modules/webpack/bin/webpack.js

3. 啟動調試

經由以上兩步,準備事變已完成,下面啟動調試,啟動時選中前面設置的 debug webpack

4. 實行到斷點

啟動后順序就會停在斷點地點的位置,在這裏你能夠輕易的檢察變量當前的狀況,找出題目。

道理總結

Webpack 是一個巨大的 Node.js 運用,如果你瀏覽過它的源碼,你會發明完成一個完全的 Webpack 須要編寫異常多的代碼。 但你無需相識統統的細節,只需相識其團體架構和部份細節即可。

對 Webpack 的運用者來講,它是一個簡樸壯大的東西; 對 Webpack 的開闢者來講,它是一個擴大性的高體系。

Webpack 之所以能勝利,在於它把龐雜的完成隱蔽了起來,給用戶暴露出的只是一個簡樸的東西,讓用戶能疾速殺青目標。 同時團體架構設想合理,擴大性高,開闢擴大難度不高,經由過程社區補足了大批缺失的功用,讓 Webpack 險些能勝任任何場景。

經由過程本章的進修,願望你不僅能學會怎樣編寫 Webpack 擴大,也能從中領悟到怎樣設想好的體系架構。

    原文作者:whjin
    原文地址: https://segmentfault.com/a/1190000015088834
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞