編寫webpack loader和插件

webpack簡介

基本概念

  • Entry:入口,Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。
  • Module:模塊,在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出全部依賴的模塊。
  • Chunk:代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。
  • Loader:模塊轉換器,用於把模塊原內容按照需求轉換成新內容。
  • Plugin:擴展插件,在 Webpack 構建流程中的特定時機會廣播出對應的事件,插件能夠監聽這些事件的發生,在特定時機作對應的事情

工做流程

Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:node

  1. 初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
  2. 開始編譯:用上一步獲得的參數初始化 Compiler 對象,加載全部配置的插件,執行對象的 run 方法開始執行編譯;
  3. 肯定入口:根據配置中的 entry 找出全部的入口文件;
  4. 編譯模塊:從入口文件出發,調用全部配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理;
  5. 完成模塊編譯:在通過第4步使用 Loader 翻譯完全部模塊後,獲得了每一個模塊被翻譯後的最終內容以及它們之間的依賴關係;
  6. 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會;
  7. 輸出完成:在肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫入到文件系統。

在以上過程當中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。
20161115173646466.jpgwebpack

編寫loader

職責:一個 Loader 的職責是單一的,只須要完成一種轉換。git

初始化

module.exports = function(source) {  
    // source 爲 compiler 傳遞給 Loader 的一個文件的原內容  
    // 對source進行一些操做 以後返回給下一個loader
    return source;
};
  • 得到 Loader 的 optionsgithub

    const loaderUtils = require('loaderutils');
    module.exports = function(source) {  
        // 獲取到用戶給當前 Loader 傳入的 options 
        const options = loaderUtils.getOptions(this);
        // 根據不一樣的options 進行不一樣的操做
        return source;
    };

返回其它結果

例如以用 babel-loader 轉換 ES6 代碼爲例,它還須要輸出轉換後的 ES5 代碼對應的 Source Map,以方便調試源碼。 爲了把 Source Map 也一塊兒隨着 ES5 代碼返回給 Webpackweb

module.exports = function(source) { 
    this.callback(null, source, sourceMaps); 
    // 經過 this.callback 告訴 Webpack 返回的結果
    //當使用this.callback返回內容時,該 Loader 必須返回undefined以讓 Webpack 知道該 Loader 返回的結果this.callback 中,而不是 return 中   
    return;
};

其中的 this.callback 是 Webpack 給 Loader 注入的 API,以方便 Loader 和 Webpack 之間通訊。 this.callback 的詳細使用方法以下:npm

this.callback(    
    // 當沒法轉換原內容時,給 Webpack 返回一個 Error   
    err: Error | null,    
    // 原內容轉換後的內容    
    content: string | Buffer,    
    // 用於把轉換後的內容得出原內容的 Source Map,方便調試    sourceMap?: SourceMap,    
    // 若是本次轉換爲原內容生成了 AST 語法樹,能夠把這個 AST 返回,以方便以後須要 AST 的 Loader 複用該 AST,以免重複生成 AST,提高性能    
    abstractSyntaxTree?: AST
);

同步與異步

但在有些場景下轉換的步驟只能是異步完成的,例如你須要經過網絡請求才能得出結果,若是採用同步的方式網絡請求就會阻塞整個構建,致使構建很是緩慢。json

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 傳入二進制格式的數據。api

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;

其它 Loader API(Loader 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

Npmlink

Npm link 專門用於開發和調試本地 Npm 模塊,能作到在不發佈模塊的狀況下,把本地的一個正在開發的模塊的源碼連接到項目的 node_modules 目錄下,讓項目能夠直接使用本地的 Npm 模塊。 因爲是經過軟連接的方式實現的,編輯了本地的 Npm 模塊代碼,在項目中也能使用到編輯後的代碼。babel

完成 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/ 目錄下尋找。

編寫插件

Webpack 插件組成

在自定義插件以前,咱們須要瞭解,一個 Webpack 插件由哪些構成,下面摘抄文檔:

  • 一個具名 JavaScript 函數;
  • 在它的原型上定義 apply 方法;
  • 指定一個觸及到 Webpack 自己的事件鉤子
  • 操做 Webpack 內部的實例特定數據;
  • 在實現功能後調用 Webpack 提供的 callback。

Webpack 插件基本架構

插件由一個構造函數實例化出來。構造函數定義 apply 方法,在安裝插件時,apply 方法會被 Webpack compiler調用一次。apply 方法能夠接收一個 Webpack compiler對象的引用,從而能夠在回調函數中訪問到 compiler 對象。

官方文檔提供一個簡單的插件結構:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (
      stats /* 在 hook 被觸及時,會將 stats 做爲參數傳入。 */
    ) => {
      console.log('Hello World!');
    });
  }
}
module.exports = HelloWorldPlugin;

使用插件:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... 這裏是其餘配置 ...
  plugins: [new HelloWorldPlugin({ options: true })]
};

插件觸發時機

Webpack 提供鉤子有不少,完整具體可參考文檔《Compiler Hooks

  • entryOption : 在 webpack 選項中的 entry 配置項 處理過以後,執行插件。
  • afterPlugins : 設置完初始插件以後,執行插件。
  • compilation : 編譯建立以後,生成文件以前,執行插件。。
  • emit : 生成資源到 output 目錄以前。
  • done : 編譯完成。

compiler.hooks 下指定事件鉤子函數,便會觸發鉤子時,執行回調函數。
Webpack 提供三種觸發鉤子的方法:

  • tap :以同步方式觸發鉤子;
  • tapAsync :以異步方式觸發鉤子;
  • tapPromise :以異步方式觸發鉤子,返回 Promise;

compiler和compilation介紹

Compiler 和 Compilation 的區別在於:Compiler 表明了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是表明了一次新的編譯。

compiler

webpack的compiler模塊是其核心部分。其包含了webpack配置文件傳遞的全部選項,包含了諸如loader、plugins等信息。

咱們能夠看看Compiler類中定義的一些核心方法。

//繼承自Tapable類,使得自身擁有發佈訂閱的能力
class Compiler extends Tapable {
  //構造函數,context實際傳入值爲process.cwd(),表明當前的工做目錄
  constructor(context) {
    super();
    // 定義了一系列的事件鉤子,分別在不一樣的時刻觸發
    this.hooks = {
      shouldEmit: new SyncBailHook(["compilation"]),
      done: new AsyncSeriesHook(["stats"]),
      //....更多鉤子
    };
    this.running = true;
    //其餘一些變量聲明
  }

  //調用該方法以後會監聽文件變動,一旦變動則從新執行編譯
  watch(watchOptions, handler) {
    this.running = true;
    return new Watching(this, watchOptions, handler)
  }
  
  //用於觸發編譯時全部的工做
  run(callback) {
    //編譯以後的處理,省略了部分代碼
    const onCompiled = (err, compilation) => {
      this.emitAssets(compilation, err => {...})
    }
  }

  //負責將編譯輸出的文件寫入本地
  emitAssets(compilation, callback) {}

  //建立一個compilation對象,並將compiler自身做爲參數傳遞
  createCompilation() {
    return new Compilation(this);
  }

  //觸發編譯,在內部建立compilation實例並執行相應操做
  compile() {}


  //以上核心方法中不少會經過this.hooks.someHooks.call來觸發指定的事件
  
}

能夠看到,compiler中設置了一系列的事件鉤子和各類配置參數,並定義了webpack諸如啓動編譯、觀測文件變更、將編譯結果文件寫入本地等一系列核心方法。在plugin執行的相應工做中咱們確定會須要經過compiler拿到webpack的各類信息。

compilation

若是把compiler算做是總控制檯,那麼compilation則專一於編譯處理這件事上。

在啓用Watch模式後,webpack將會監聽文件是否發生變化,每當檢測到文件發生變化,將會執行一次新的編譯,並同時生成新的編譯資源和新的compilation對象。
compilation對象中包含了模塊資源、編譯生成資源以及變化的文件和被跟蹤依賴的狀態信息等等,以供插件工做時使用。若是咱們在插件中須要完成一個自定義的編譯過程,那麼必然會用到這個對象。

經常使用 API(所有API)

插件能夠用來修改輸出文件、增長輸出文件、甚至能夠提高 Webpack 性能、等等,總之插件經過調用 Webpack 提供的 API 能完成不少事情。

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

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

emit 事件發生時,表明源文件的轉換和組裝已經完成,在這裏能夠讀取到最終將輸出的資源、代碼塊、模塊及其依賴,而且能夠修改輸出資源的內容。

監聽文件變化

Webpack 會從配置的入口模塊出發,依次找出全部的依賴模塊,當入口模塊或者其依賴的模塊發生變化時, 就會觸發一次新的 Compilation。

在開發插件時常常須要知道是哪一個文件發生變化致使了新的 Compilation

默認狀況下 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;}

寫在最後

參考文章

推薦閱讀