如何編寫一個WebPack的插件原理及實踐

  • 2019 年 10 月 3 日
  • 筆記

閱讀目錄

一:webpack插件的基本原理

webpack構建工具大家應該不陌生了,那麼下面我們來簡單的了解下什麼是webpack的插件。比如我現在寫了一個插件叫 “kongzhi-plugin” 這個插件。那麼這個插件在處理webpack編譯過程中會處理一些特定的任務。

比如我們現在在webpack.config.js 中引入了一個如下插件:

// 引入打包html文件  const HtmlWebpackPlugin = require('html-webpack-plugin');

然後我們需要如下使用該插件:

module.exports = {    plugins: [      new HtmlWebpackPlugin({        template: './public/index.html' // 模版文件      }),    ]  };

如上就是一個 HtmlWebpackPlugin 插件 及在webpack中使用的方式了。現在我們需要實現一個類似的webpack的插件。

webpack打包是一種事件流的機制,它的原理是將各個插件串聯起來。那麼實現這一切的核心就是tapable,要想深入了解 tapable的知識可以看我之前的一篇文章.

tapable它可以暴露出掛載plugin的方法。可以讓我們能將plugin控制在webpack事件流上運行。
tapable給我們暴露了很多鉤子類,能為我們的插件提供掛載的鉤子。
如下程式碼所示:

const {    SyncHook,    SyncBailHook,    SyncWaterfallHook,    SyncLoopHook,    AsyncParallelHook,    AsyncParallelBailHook,    AsyncSeriesHook,    AsyncSeriesBailHook,    AsyncSeriesWaterfallHook  } = require('tapable');

如上各個鉤子的含義及使用方式,可以看我之前這篇文章的介紹。

下面我們來看個簡單的demo,我們會定義一個 KongZhiClass 類,在內部我們創建一個 hooks 這個對象,然後在該對象上分別創建同步鉤子kzSyncHook及非同步鉤子 kzAsyncHook。 然後分別執行,程式碼如下:

const { SyncHook, AsyncParallelHook } = require('tapable');    // 創建類     class KongZhiClass {    constructor() {      this.hooks = {        kzSyncHook: new SyncHook(['name', 'age']),        kzAsyncHook: new AsyncParallelHook(['name', 'age'])      }    }  }    // 實例化  const myName = new KongZhiClass();    // 綁定同步鉤子  myName.hooks.kzSyncHook.tap("eventName1", (name, age) => {    console.log(`同步事件eventName1: ${name} this year ${age} 周歲了, 可是還是單身`);  });    // 綁定一個非同步Promise鉤子  myName.hooks.kzAsyncHook.tapPromise('eventName2', (name, age) => {    return new Promise((resolve, reject) => {      setTimeout(() => {        console.log(`非同步事件eventName2: ${name} this year ${age}周歲了,可是還是單身`);      }, 1000);    });  });    // 執行同步鉤子  myName.hooks.kzSyncHook.call('空智', 31);    // 執行非同步鉤子  myName.hooks.kzAsyncHook.promise('空智', 31).then(() => {    console.log('非同步事件執行完畢');  }, (err) => {    console.log('非同步事件執行異常:' + err);  }) 

執行結果如下:

如上是我們使用的 tapable 的使用方式,現在我們需要使用tapable的demo來和我們的webpack的插件相關聯起來,我們要如何做呢?

我們可以將上面的程式碼來拆分成兩個文件:compiler.js、main.js. (main.js 是入口文件)

假如我們的項目結構如下:

|--- tapable項目  | |--- node_modules  | |--- public  | | |--- js  | | | |--- main.js  | | | |--- compiler.js  | |--- package.json  | |--- webpack.config.js

compiler.js 需要做的事情如下:

1. 定義一個 Compiler 類,接收一個options對象參數,該參數是從main.js中的MyPlugin類的實列對象。該對象下有 apply函數。

2. 在該類中我們定義了run方法,我們在main.js 中執行該run函數就可以自動執行對應的插件了。

程式碼如下:

const { SyncHook, AsyncParallelHook } = require('tapable');    class Compiler {    constructor(options) {      this.hooks = {        kzSyncHook: new SyncHook(['name', 'age']),        kzAsyncHook: new AsyncParallelHook(['name', 'age'])      };      let plugins = options.plugins;      if (plugins && plugins.length > 0) {        plugins.forEach(plugin => plugin.apply(this));      }    }    run() {      console.log('開始執行了---------');      this.kzSyncHook('我是空智', 31);      this.kzAsyncHook('我是空智', 31);    }    kzSyncHook(name, age) {      this.hooks.kzSyncHook.call(name, age);    }    kzAsyncHook(name, age) {      this.hooks.kzAsyncHook.callAsync(name, age);    }  }    module.exports = Compiler;

main.js 需要做的事情如下:

1. 引入 compiler.js 文件。
2. 定義一個自己的插件,比如叫 MyPlugin 類,該類下有 apply 函數。該函數有一個 compiler 參數,該參數就是我們的 compiler.js 中的實列對象。然後我們會使用 compiler 實列對象去調用 compiler.js 裡面的函數。因此就可以自動執行了。

程式碼如下所示:

const Compiler = require('./compiler');    class MyPlugin {    constructor() {      }    apply(compiler) {      compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {        console.log(`同步事件eventName1: ${name} this year ${age} 周歲了, 可是還是單身`);      });      compiler.hooks.kzAsyncHook.tapAsync('eventName2', (name, age) => {        setTimeout(() => {          console.log(`非同步事件eventName2: ${name} this year ${age}周歲了,可是還是單身`);        }, 1000)      });    }  }    const myPlugin = new MyPlugin();    const options = {    plugins: [myPlugin]  };    const compiler = new Compiler(options);  compiler.run();

最後執行的效果如下所示:

如上就是我們仿照Compiler和webpack的插件原理邏輯實現的一個簡單demo。也就是說在webpack源碼裡面也是通過類似的方式來做的。

上面只是一個簡單實現的基本原理,但是在我們的webpack當中我們要如何實現一個插件呢?
在我們的webpack官網中會介紹編寫一個插件要滿足如下條件, 官網地址

從官網得知:編寫一個webpack插件需要由以下組成:

1. 一個javascript命名函數。
2. 在插件函數的prototype上定義一個 apply 方法。
3. 指定一個綁定到webpack自身的鉤子函數。
4. 處理webpack內部實列的特定數據。
5. 功能完成後調用webpack提供的回調函數。

一個最基礎的插件程式碼像如下這個樣子:

// 一個javascript命名函數  function MyExampleWebpackPlugin() {    };  // 在插件函數的prototype上定義一個 apply 方法  MyExampleWebpackPlugin.prototype.apply = function(compiler) {    // 指定一個掛載到webpack自身的事件鉤子。    compiler.plugin('webpacksEventHook', function(compilation, callback) {      console.log('這是一個插件demo');        // 功能完成後調用 webpack 提供的回調      callback();    })  }    // 導出plugin  module.exports = MyExampleWebpackPlugin;

在我們使用該plugin的時候,相關調用及配置程式碼如下:

const MyExampleWebpackPlugin = require('./MyExampleWebpackPlugin');  module.exports = {    plugins: [      new MyExampleWebpackPlugin(options)    ]  };

webpack啟動後,在讀取配置的過程中會先執行 new MyExampleWebpackPlugin(options) 初始化MyExampleWebpackPlugin來獲得一個實列。然後我們會把該實列當做參數傳遞給我們的Compiler對象,然後會實列化 Compiler類(這個邏輯可以結合看我們上面實現了一個簡單的demo中 的main.js和compiler.js的程式碼結合起來理解)。在Compiler類中,我們會獲取到options的這個參數,該參數是一個對象,該對象下有一個 plugins 這個屬性。然後遍歷該屬性,然後依次執行 某項插件中的apply方法,即:myExampleWebpackPlugin.apply(compiler); 給插件傳遞compiler對象。插件實列獲取該compiler對象後,就可以通過 compiler.plugin(‘事件名稱’, ‘回調函數’); 監聽到webpack廣播出來的事件.(這個地方我們可以看我們上面的main.js中的如下程式碼可以看到, 在我們的main.js程式碼中有這樣程式碼:compiler.hooks.kzSyncHook.tap(“eventName1”, (name, age) => {}));
如上就是一個簡單的Plugin的插件原理(切記:結合上面的demo中main.js和compiller.js來理解效果會更好)。

二:理解 Compiler對象 和 Compilation 對象

在開發Plugin時我們最常用的兩個對象就是 Compiler 和 Compilation, 他們是Plugin和webpack之間的橋樑。

Compiler對象

Compiler 對象包含了Webpack環境所有的配置資訊,包含options,loaders, plugins這些項,這個對象在webpack啟動時候被實例化,它是全局唯一的。我們可以把它理解為webpack的實列。

基本源碼可以看如下:

// webpack/lib/webpack.js  const Compiler = require("./Compiler")    const webpack = (options, callback) => {    ...    // 初始化 webpack 各配置參數    options = new WebpackOptionsDefaulter().process(options);      // 初始化 compiler 對象,這裡 options.context 為 process.cwd()    let compiler = new Compiler(options.context);      compiler.options = options                               // 往 compiler 添加初始化參數      new NodeEnvironmentPlugin().apply(compiler)              // 往 compiler 添加 Node 環境相關方法      for (const plugin of options.plugins) {      plugin.apply(compiler);    }    ...  }

源碼可以點擊這裡查看官網可以看這裡

如上我們可以看到,Compiler對象包含了所有的webpack可配置的內容。開發插件時,我們可以從 compiler 對象中拿到所有和 webpack 主環境相關的內容。

compilation 對象

compilation 對象包含了當前的模組資源、編譯生成資源、文件的變化等。當webpack在開發模式下運行時,每當檢測到一個文件發生改變的時候,那麼一次新的 Compilation將會被創建。從而生成一組新的編譯資源。

Compiler對象 與 Compilation 對象 的區別是:Compiler代表了是整個webpack從啟動到關閉的生命周期。Compilation 對象只代表了一次新的編譯。
Compiler對象的事件鉤子,我們可以看官網. 或者我們也可以查看它的源碼也可以看得到,查看源碼

我們可以了解常見的事件鉤子:下面是一些比較常見的事件鉤子及作用:

鉤子               作用                     參數               類型  after-plugins     設置完一組初始化插件之後    compiler          sync  after-resolvers   設置完 resolvers 之後     compiler          sync  run               在讀取記錄之前             compiler          async  compile           在創建新 compilation之前  compilationParams  sync  compilation       compilation 創建完成      compilation        sync  emit              在生成資源並輸出到目錄之前  compilation        async  after-emit        在生成資源並輸出到目錄之後  compilation        async  done              完成編譯                  stats              sync

理解webpack中的事件流

我們可以把webpack理解為一條生產線,需要經過一系列處理流程後才能將源文件轉換成輸出結果。
這條生產線上的每個處理流程的職責都是單一的,多個流程之間會存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。

我們的插件就像一個插入到生產線中的一個功能,在特定的時機對生產線上的資源會做處理。webpack它是通過 Tapable來組織這條複雜的生產線的。

webpack在運行的過程中會廣播事件,插件只需要關心監聽它的事件,就能加入到這條生產線中。然後會執行相關的操作。
webpack的事件流機制它能保證了插件的有序性,使整個系統的擴展性好。事件流機制使用了觀察者模式來實現的。比如如下程式碼:

/*   * 廣播事件   * myPlugin-name 為事件名稱   * params 為附帶的參數  */    compiler.apply('myPlugin-name', params);    /*   * 監聽名稱為 'myPlugin-name' 的事件,當 myPlugin-name 事件發生時,函數就會執行。  */    compiler.hooks.myPlugin-name.tap('myPlugin-name', function(params) {    });

三:插件中常用的API

1. 讀取輸出資源、模組及依賴

在我們的emit鉤子事件發生時,表示的含義是:源文件的轉換和組裝已經完成了,在這裡事件鉤子裡面我們可以讀取到最終將輸出的資源、程式碼塊、模組及對應的依賴文件。並且我們還可以輸出資源文件的內容。比如插件程式碼如下:

class MyPlugin {    apply(compiler) {      compiler.plugin('emit', function(compilation, callback) {        // compilation.chunks 是存放了所有的程式碼塊,是一個數組,我們需要遍歷        compilation.chunks.forEach(function(chunk) {          /*           * chunk 代表一個程式碼塊,程式碼塊它是由多個模組組成的。           * 我們可以通過 chunk.forEachModule 能讀取組成程式碼塊的每個模組          */          chunk.forEachModule(function(module) {            // module 代表一個模組。            // module.fileDependencies 存放當前模組的所有依賴的文件路徑,它是一個數組            module.fileDependencies.forEach(function(filepath) {              console.log(filepath);            });          });          /*           webpack 會根據chunk去生成輸出的文件資源,每個chunk都對應一個及以上的輸出文件。           比如在 Chunk中包含了css 模組並且使用了 ExtractTextPlugin 時,           那麼該Chunk 就會生成 .js 和 .css 兩個文件          */          chunk.files.forEach(function(filename) {            // compilation.assets 是存放當前所有即將輸出的資源。            // 調用一個輸出資源的 source() 方法能獲取到輸出資源的內容            const source = compilation.assets[filename].source();          });        });        /*         該事件是非同步事件,因此要調用 callback 來通知本次的 webpack事件監聽結束。         如果我們沒有調用callback(); 那麼webpack就會一直卡在這裡不會往後執行。        */        callback();      })    }  }

2. 監聽文件變化

webpack讀取文件的時候,它會從入口模組去讀取,然後依次找出所有的依賴模組。當入口模組或依賴的模組發生改變的時候,那麼就會觸發一次新的 Compilation。

在我們開發插件的時候,我們需要知道是那個文件發生改變,導致了新的Compilation, 我們可以添加如下程式碼進行監聽。

// 當依賴的文件發生改變的時候 會觸發 watch-run 事件  class MyPlugin {    apply(compiler) {      compiler.plugin('watch-run', (watching, callback) => {        // 獲取發生變換的文件列表        const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;        // changedFiles 格式為鍵值對的形式,當鍵為發生變化的文件路徑        if (changedFiles[filePath] !== undefined) {          // 對應的文件就發生了變化了        }        callback();      });        /*       默認情況下Webpack只會監聽入口文件或其依賴的模組是否發生變化,但是在有些情況下比如html文件發生改變的時候,那麼webpack       就會去監聽html文件的變化。因此就不會重新觸發新的 Compilation。因此為了監聽html文件的變化,我們需要把html文件加入到       依賴列表中。因此我們需要添加如下程式碼:      */      compiler.plugin('after-compile', (compilation, callback) => {        /*         如下的參數filePath是html文件路徑,我們把HTML文件添加到文件依賴表中,然後我們的webpack會去監聽html模組文件,         html模板文件發生改變的時候,會重新啟動下重新編譯一個新的 Compilation.        */        compilation.fileDependencies.push(filePath);        callback();      })    }  }

3. 修改輸出資源

我們在第一點說過:在我們的emit鉤子事件發生時,表示的含義是:源文件的轉換和組裝已經完成了,在這裡事件鉤子裡面我們可以讀取到最終將輸出的資源、程式碼塊、模組及對應的依賴文件。因此如果我們現在要修改輸出資源的內容的話,我們可以在emit事件中去做修改。那麼所有輸出的資源會存放在 compilation.assets中,compilation.assets是一個鍵值對,鍵為需要輸出的文件名,值為文件對應的內容。如下程式碼:

class MyPlugin {    apply(compiler) {      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();      });    }  }

4. 判斷webpack使用了哪些插件

在我們開發一個插件的時候,我們需要根據當前配置是否使用了其他某個插件,我們可以通過讀取webpack某個插件配置的情況,比如來判斷我們當前是否使用了 HtmlWebpackPlugin 插件。程式碼如下:

/*   判斷當前配置使用了 HtmlWebpackPlugin 插件。   compiler參數即為 webpack 在 apply(compiler) 中傳入的參數  */    function hasHtmlWebpackPlugin(compiler) {    // 獲取當前配置下所有的插件列表    const plugins = compiler.options.plugins;    // 去plugins中尋找有沒有 HtmlWebpackPlugin 的實列    return plugins.find(plugin => plugin.__proto__.constructor === HtmlWebpackPlugin) !== null;  }

四:編寫插件實戰

 假如現在我們的項目的目錄結構如下:

|--- webpack-plugin-demo  | |--- node_modules  | |--- js  | | |--- main.js               # js 的入口文件  | |--- plugins  | | |--- logWebpackPlugin.js   # 編寫的webpack的插件,主要作用是列印日誌功能  | |--- styles  | |--- index.html  | |--- package.json  | |--- webpack.config.js

1. 實現一個列印日誌的LogWebpackPlugin插件

程式碼如下:

class LogWebpackPlugin {    constructor(doneCallback, emitCallback) {      this.emitCallback = emitCallback      this.doneCallback = doneCallback    }    apply(compiler) {      compiler.hooks.emit.tap('LogWebpackPlugin', () => {        // 在 emit 事件中回調 emitCallback        this.emitCallback();      });      compiler.hooks.done.tap('LogWebpackPlugin', (err) => {        // 在 done 事件中回調 doneCallback        this.doneCallback();      });      compiler.hooks.compilation.tap('LogWebpackPlugin', () => {        // compilation('編譯器'對'編譯ing'這個事件的監聽)        console.log("The compiler is starting a new compilation...")      });      compiler.hooks.compile.tap('LogWebpackPlugin', () => {        // compile('編譯器'對'開始編譯'這個事件的監聽)        console.log("The compiler is starting to compile...")      });    }  }    // 導出插件  module.exports = LogWebpackPlugin;

下面我們在webpack中引入該插件;如下程式碼:

// 引入LogWebpackPlugin 插件  const LogWebpackPlugin = require('./public/plugins/logWebpackPlugin');    module.exports = {    plugins: [      new LogWebpackPlugin(() => {        // Webpack 模組完成轉換成功        console.log('emit 事件發生啦,所有模組的轉換和程式碼塊對應的文件已經生成好~')      } , () => {        // Webpack 構建成功,並且文件輸出了後會執行到這裡,在這裡可以做發布文件操作        console.log('done 事件發生啦,成功構建完成~')      })    ]  }

然後執行結果如下所示:

可以看到我們執行成功了,執行了對應的回調函數。如上程式碼中的 compiler 這個我這邊就不講解了,上面已經講過了。那麼 compiler.hooks 代表的是對外 暴露了多少事件鉤子,具體那個鉤子是什麼含義,我們可以來看下官網

如上面程式碼,我們使用兩個鉤子事件,分別是 compiler.hooks.emit 和 compiler.hooks.done, compiler.hooks.emit 鉤子事件的含義是: 在生成資源並輸出到目錄之前。這個事件就會發生。 compiler.hooks.done 的含義是:編譯完成,該事件就會發生。因此上面截圖我們可以看到先觸發 emit事件,因此會列印 ‘done 事件發生啦,成功構建完成~’, 然後會觸發 done事件,因此會列印 “emit 事件發生啦,所有模組的轉換和程式碼塊對應的文件已經生成好~” 執行這個回調函數。
github程式碼查看

2. 編寫去除生成 bundle.js 中多餘的注釋的插件

項目結構如下:

|--- webpack-plugin-demo  | |--- node_modules  | |--- public  | | |--- js  | | | |--- main.js                     # 入口文件  | | |--- plugins                       # 存放所有的webpack插件  | | | |--- AsyncPlugin.js  | | | |--- AutoExternalPlugin.js  | | | |--- DonePlugin.js  | | | |--- FileListPlugin.js  | | | |--- MyPlugin.js  | | | |--- OptimizePlugin.js  | | |--- styles                        # 存放css樣式文件  | | |--- index.html                    # index.html模板  | |--- package.json  | |--- webpack.config.js 

項目結構如上所示;上面在 public/plugins 中一共有6個插件,我們分別來看下6個插件的程式碼:

1. public/plugins/AsyncPlugin.js 程式碼如下:

class AsyncPlugin {    constructor() {      }    apply(compiler) {      // 監聽emit事件,編譯完成後,文件內容輸出到硬碟上 觸發該事件      compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {        setTimeout(() => {          console.log('文件將要被寫入到硬碟中');          callback();        }, 2000)      })    }  }    module.exports = AsyncPlugin;

如上該插件程式碼沒有什麼實際作用,無非就是監聽 emit 非同步事件鉤子,emit事件鉤子我們從官網 

上可以看到具體的含義為:’在生成資源並輸出到目錄之前’,會執行該事件鉤子中函數程式碼,這邊無非就是在控制台中列印一些提示資訊的,沒有什麼實際作用的。
2. public/plugins/DonePlugin.js 程式碼如下:

class DonePlugin {    constructor() {      }    apply(compiler) {      compiler.hooks.done.tapAsync('DonePlugin', (name, callback) => {        console.log('全部編譯完成');        callback();      })    }  }    module.exports = DonePlugin;

如上程式碼也是一個意思,當編譯完成後,就會執行 done的事件鉤子的回調函數,也是在命令中提示作用的。

3. public/plugins/OptimizePlugin.js 程式碼如下:

class OptimizePlugin {    constructor() {      }    apply(compiler) {      // 監聽 compilation 事件      compiler.hooks.compilation.tap('OptimizePlugin', (compilation) => {        compilation.hooks.optimize.tap('OptimizePlugin', () => {          console.log('compilation 完成,正在優化,準備輸出');        });      });    }  }    module.exports = OptimizePlugin;

也是一樣監聽 compilation 事件的,每當檢測到一個文件發生改變的時候,那麼一次新的 Compilation將會被創建。從而生成一組新的編譯資源。

4. public/plugins/FileListPlugin.js 程式碼如下:

class FileListPlugin {    constructor() {      }    apply(compiler) {      compiler.hooks.compilation.tap('FileListPlugin', (compilation) => {        compiler.hooks.emit.tap('FileListPlugin', () => {          let content = '生成的文件列表rn';          content = Object.keys(compilation.assets).reduce((current, prev) => current + '- ' + prev + 'rn', content);          console.log(content);          compilation.assets['README.md'] = {            source() {              return content;            },            size() {              return content.length;            }          }        })      })    }  }  module.exports = FileListPlugin;

生成文件列表的時候,就會觸發該文件的程式碼。

5. public/plugins/AutoExternalPlugin.js 程式碼如下:

const ExternalModules = require('webpack/lib/ExternalModule');    class AutoExternalPlugin {    constructor(options) {      this.options = options;      this.externalModules = {};    }    apply(compiler) {      compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {        // parser 將程式碼轉換為語法書 判斷有無 import        normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {          parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {            if (this.options[source]) {              this.externalModules[source] = true;            }          })        })        // factory 是創建模組的方法        // data 是創建模組的參數        normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => {          const dependencies = data.dependencies;          const value = dependencies[0].request; // jquery          if (this.externalModules[value]) {            const varName = this.options[value].varName;            callback(null, new ExternalModules(varName, 'window'));          } else {            factory(data, callback);          }        })      });      compiler.hooks.compilation.tap('InlinePlugin', (compilation) => {        compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('AutoExternalPlugin', (htmlPluginData, callback) => {          Object.keys(this.options).forEach(key => {            this.externalModules[key] = this.options[key];            htmlPluginData.body.unshift(this.processTags(compilation, htmlPluginData, this.options[key]))          });          callback(null, htmlPluginData);        });      });    }    processTags(compilation, htmlPluginData, value) {      var tag;      return tag = {        tagName: 'script',        closeTag: true,        attributes: {          type: 'text/javascript',          src: value.url        }      }    }  }    module.exports = AutoExternalPlugin;

如上該插件的程式碼的作用是可以解決外部的js引用,比如我在webpack中如下使用該插件:

const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');  module.exports = {    plugins:[      new AutoExternalPlugin({        jquery:{          varName:'jQuery',          url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'        }      })    ]  }

這樣我就可以在頁面中使用jquery插件了;如下程式碼所示:

import $ from 'jquery';  console.log($);

然後在我們的頁面中引入的是 該 jquery庫文件,它會把該庫文件自動生成到 index.html 上去,如下index.html 程式碼變成如下了:

<html lang="en">  <head>    <meta charset="UTF-8">    <title></title>    <link rel="manifest" href="/public/manifest.json" />  <link href="main.css" rel="stylesheet"></head>  <body>    <div id="app">222226666</div>  <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script><script type="text/javascript" src="bundle.js"></script></body>  </html>

我們可以來簡單的分析下 AutoExternalPlugin.js 的程式碼:

在apply方法內部會生成一個 compiler 實列,然後我們監聽 normalModuleFactory 事件,該事件的作用我們可以看下官網就知道了。

compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {    // parser 將程式碼轉換為語法書 判斷有無 import    normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {      parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {        if (this.options[source]) {          this.externalModules[source] = true;        }      })    })  }

如上 parser 實例,是用來解析由 webpack 處理過的每個模組。parser 也是擴展自 tapable 的 webpack 類,並且提供多種 tapable 鉤子,插件作者可以使用它來自定義解析過程。官網解釋可以看這裡

如上程式碼,我們調用 parser.hooks.import 鉤子函數, 然後返回的 source 就是我們的在 我們的main.js 中調用插件名。如main.js 程式碼如下:

import $ from 'jquery';

因此在我們的webpack.config.js 中會如下初始化插件 

new AutoExternalPlugin({    jquery:{      varName:'jQuery',      url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'    }  });

因此 source 返回的值 就是 ‘jquery’; 其他的程式碼可以自己稍微看看就行了。這裡暫時先不講了,由於時間問題。

6. public/plugins/MyPlugin.js 程式碼如下:

class MyPlugin {    constructor(options) {      this.options = options;      this.externalModules = {};    }    apply(compiler) {      var reg = /("([^\"]*(\.)?)*")|('([^\']*(\.)?)*')|(/{2,}.*?(r|n))|(/*(n|.)*?*/)|(/******/)/g;      compiler.hooks.emit.tap('CodeBeautify', (compilation) => {        Object.keys(compilation.assets).forEach((data) => {          console.log(data);          let content = compilation.assets[data].source(); // 獲取處理的文本          content = content.replace(reg, function (word) { // 去除注釋後的文本            return /^/{2,}/.test(word) || /^/*!/.test(word) || /^/*{3,}//.test(word) ? "" : word;          });          compilation.assets[data] = {            source() {              return content;            },            size() {              return content.length;            }          }        });      });    }  }  module.exports = MyPlugin;

這個js程式碼的真正的含義才是我們今天要講到的,這個插件最主要作用是 去除注釋後的文本。

1. 第一步,我們使用 compiler.hooks.emit 鉤子函數。在生成資源並輸出到目錄之前觸發該函數,也就是說將編譯好的程式碼發射到指定的stream中就會觸發,然後我們從回調函數返回的 compilation 對象上可以拿到編譯好的 stream.

2. 訪問compilation對象,compilation內部會返回很多內部對象,這邊先不列印了,因為列印的話直接會卡死掉,要等很長時間才會列印出來,你們自己可以試試;然後我們遍歷 assets.

Object.keys(compilation.assets).forEach((data) => {    console.log(compilation.assets);    console.log(8888)    console.log(data);  });

如下圖所示:

1) assets 數組對象中的key是資源名。在如上程式碼,我們通過 Object.key()方法拿到了。如下所示:

main.css  bundle.js  index.html

2) 然後我們調用 compilation.assets[data].source(); 可以獲取資源的內容。

3) 使用正則,去掉注釋,如下程式碼:

Object.keys(compilation.assets).forEach((data) => {    let content = compilation.assets[data].source(); // 獲取處理的文本    content = content.replace(reg, function (word) { // 去除注釋後的文本      return /^/{2,}/.test(word) || /^/*!/.test(word) || /^/*{3,}//.test(word) ? "" : word;    });  });

4) 更新 compilation.assets[data] 對象,如下程式碼:

compilation.assets[data] = {    source() {      return content;    },    size() {      return content.length;    }  }

然後我們就可以在webpack中引入該所有的插件:

const DonePlugin = require('./public/plugins/DonePlugin');  const OptimizePlugin = require('./public/plugins/OptimizePlugin');  const AsyncPlugin = require('./public/plugins/AsyncPlugin');  const FileListPlugin = require('./public/plugins/FileListPlugin');  const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');  const MyPlugin = require('./public/plugins/MyPlugin');

調用方式如下:

module.exports = {    plugins:[      new DonePlugin(),      new OptimizePlugin(),      new AsyncPlugin(),      new FileListPlugin(),      new MyPlugin(),      new AutoExternalPlugin({        jquery:{          varName:'jQuery',          url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'        }      })    ]  }

然後我們進行打包運行效果如下所示:

github源碼查看