Webpack4不求人(5)——編寫自定義插件

Webpack經過Loader完成模塊的轉換工做,讓「一切皆模塊」成爲可能。Plugin機制則讓其更加靈活,能夠在Webpack生命週期中調用鉤子完成各類任務,包括修改輸出資源、輸出目錄等等。javascript

今天咱們一塊兒來學習如何編寫Webpack插件。html

構建流程

在編寫插件以前,還須要瞭解一下Webpack的構建流程,以便在合適的時機插入合適的插件邏輯。Webpack的基本構建流程以下:前端

  1. 校驗配置文件
  2. 生成Compiler對象
  3. 初始化默認插件
  4. run/watch:若是運行在watch模式則執行watch方法,不然執行run方法
  5. compilation:建立Compilation對象回調compilation相關鉤子
  6. emit:文件內容準備完成,準備生成文件,這是最後一次修改最終文件的機會
  7. afterEmit:文件已經寫入磁盤完成
  8. done:完成編譯

插件示例

一個典型的Webpack插件代碼以下:java

// 插件代碼
class MyWebpackPlugin {
  constructor(options) {
  }
  
  apply(compiler) {
    // 在emit階段插入鉤子函數
    compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => {});
  }
}

module.exports = MyWebpackPlugin;

接下來須要在webpack.config.js中引入這個插件。node

module.exports = {
  plugins:[
    // 傳入插件實例
    new MyWebpackPlugin({
      param:'paramValue'
    }),
  ]
};

Webpack在啓動時會實例化插件對象,在初始化compiler對象以後會調用插件實例的apply方法,傳入compiler對象,插件實例在apply方法中會註冊感興趣的鉤子,Webpack在執行過程當中會根據構建階段回調相應的鉤子。webpack

Compiler && Compilation對象

在編寫Webpack插件過程當中,最經常使用也是最主要的兩個對象就是Webpack提供的Compiler和Compilation,Plugin經過訪問Compiler和Compilation對象來完成工做。web

  • Compiler 對象包含了當前運行Webpack的配置,包括entry、output、loaders等配置,這個對象在啓動Webpack時被實例化,並且是全局惟一的。Plugin能夠經過該對象獲取到Webpack的配置信息進行處理。
  • Compilation對象能夠理解編譯對象,包含了模塊、依賴、文件等信息。在開發模式下運行Webpack時,每修改一次文件都會產生一個新的Compilation對象,Plugin能夠訪問到本次編譯過程當中的模塊、依賴、文件內容等信息。

常見鉤子

Webpack會根據執行流程來回調對應的鉤子,下面咱們來看看都有哪些常見鉤子,這些鉤子支持的tap操做是什麼。npm

鉤子 說明 參數 類型
afterPlugins 啓動一次新的編譯 compiler 同步
compile 建立compilation對象以前 compilationParams 同步
compilation compilation對象建立完成 compilation 同步
emit 資源生成完成,輸出以前 compilation 異步
afterEmit 資源輸出到目錄完成 compilation 異步
done 完成編譯 stats 同步

Tapable

Tapable是Webpack的一個核心工具,Webpack中許多對象擴展自Tapable類。Tapable類暴露了tap、tapAsync和tapPromise方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯。json

  • tap 同步鉤子
  • tapAsync 異步鉤子,經過callback回調告訴Webpack異步執行完畢
  • tapPromise 異步鉤子,返回一個Promise告訴Webpack異步執行完畢

tap

tap是一個同步鉤子,同步鉤子在使用時不能夠包含異步調用,由於函數返回時異步邏輯有可能未執行完畢致使問題。bash

下面一個在compile階段插入同步鉤子的示例。

compiler.hooks.compile.tap('MyWebpackPlugin', params => {
  console.log('我是同步鉤子')
});

tapAsync

tapAsync是一個異步鉤子,咱們能夠經過callback告知Webpack異步邏輯執行完畢。

下面是一個在emit階段的示例,在1秒後打印文件列表。

compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
  setTimeout(()=>{
    console.log('文件列表', Object.keys(compilation.assets).join(','));
    callback();
  }, 1000);
});

tapPromise

tapPromise也是也是異步鉤子,和tapAsync的區別在於tapPromise是經過返回Promise來告知Webpack異步邏輯執行完畢。

下面是一個將生成結果上傳到CDN的示例。

compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
  return new Promise((resolve, reject) => {
    const filelist = Object.keys(compilation.assets);
    uploadToCDN(filelist, (err) => {
      if(err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
});

apply方法中插入鉤子的通常形式以下:

compileer.hooks.階段.tap函數('插件名稱', (階段回調參數) => {
  
});

經常使用API

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

在emit階段,咱們能夠讀取最終須要輸出的資源、chunk、模塊和對應的依賴,若是有須要還能夠更改輸出資源。

apply(compiler) {
  compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
    // compilation.chunks存放了代碼塊列表
    compilation.chunks.forEach(chunk => {
     // chunk包含多個模塊,經過chunk.modulesIterable能夠遍歷模塊列表 
            for(const module of chunk.modulesIterable) {
        // module包含多個依賴,經過module.dependencies進行遍歷
          module.dependencies.forEach(dependency => {
          console.log(dependency);
        });
      }
    });
    callback();
  });
}

修改輸出資源

經過操做compilation.assets對象,咱們能夠添加、刪除、更改最終輸出的資源。

apply(compiler) {
  compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation) => {
    // 修改或添加資源
    compilation.assets['main.js']  = {
      source() {
        return 'modified content';
      },
      size() {
        return this.source().length;
      }
    };
    // 刪除資源
    delete compilation.assets['main.js'];
  });
}

assets對象須要定義source和size方法,source方法返回資源的內容,支持字符串和Node.js的Buffer,size返回文件的大小字節數。

插件編寫實例

接下來咱們開始編寫自定義插件,全部插件使用的示例項目以下(須要安裝webpack和webpack-cli):

|----src
        |----main.js
|----plugins
        |----my-webpack-plugin.js
|----package.json
|----webpack.config.js

相關文件的內容以下:

// src/main.js
console.log('Hello World');
// package.json
{
  "scripts":{
    "build":"webpack"
  }
}
const path = require('path');
const MyWebpackPlugin = require('my-webpack-plugin');

// webpack.config.js
module.exports = {
  entry:'./src/main',
  output:{
    path: path.resolve(__dirname, 'build'),
    filename:'[name].js',
  },
  plugins:[
    new MyWebpackPlugin()
  ]
};

生成清單文件

經過在emit階段操做compilation.assets實現。

class MyWebpackPlugin {
    apply(compiler) {
        compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
            const manifest = {};
            for (const name of Object.keys(compilation.assets)) {
                manifest[name] = compilation.assets[name].size();
                // 將生成文件的文件名和大小寫入manifest對象
            }
            compilation.assets['manifest.json'] = {
                source() {
                    return JSON.stringify(manifest);
                },
                size() {
                    return this.source().length;
                }
            };
            callback();
        });
    }
}

module.exports = MyWebpackPlugin;

構建完成後會在build目錄添加manifest.json,內容以下:

{"main.js":956}

構建結果上傳到七牛

在實際開發中,資源文件構建完成後通常會同步到CDN,最終前端界面使用的是CDN服務器上的靜態資源。

下面咱們編寫一個Webpack插件,文件構建完成後上傳的七牛CDN。

咱們的插件依賴qiniu,所以須要額外安裝qiniu模塊

npm install qiniu --save-dev

七牛的Node.js SDK文檔地址以下:

https://developer.qiniu.com/kodo/sdk/1289/nodejs

開始編寫插件代碼:

const qiniu = require('qiniu');
const path = require('path');

class MyWebpackPlugin {
    // 七牛SDK mac對象
    mac = null;

    constructor(options) {
          // 讀取傳入選項
        this.options = options || {};
          // 檢查選項中的參數
        this.checkQiniuConfig();
          // 初始化七牛mac對象
        this.mac = new qiniu.auth.digest.Mac(
            this.options.qiniu.accessKey,
            this.options.qiniu.secretKey
        );
    }
    checkQiniuConfig() {
        // 配置未傳qiniu,讀取環境變量中的配置
        if (!this.options.qiniu) {
            this.options.qiniu = {
                accessKey: process.env.QINIU_ACCESS_KEY,
                secretKey: process.env.QINIU_SECRET_KEY,
                bucket: process.env.QINIU_BUCKET,
                keyPrefix: process.env.QINIU_KEY_PREFIX || ''
            };
        }
        const qiniu = this.options.qiniu;
        if (!qiniu.accessKey || !qiniu.secretKey || !qiniu.bucket) {
            throw new Error('invalid qiniu config');
        }
    }

    apply(compiler) {
        compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
            return new Promise((resolve, reject) => {
                // 總上傳數量
                const uploadCount = Object.keys(compilation.assets).length;
                // 已上傳數量
                let currentUploadedCount = 0;
                                // 七牛SDK相關參數
                const putPolicy = new qiniu.rs.PutPolicy({ scope: this.options.qiniu.bucket });
                const uploadToken = putPolicy.uploadToken(this.mac);
                const config = new qiniu.conf.Config();
                config.zone = qiniu.zone.Zone_z1;
                const formUploader = new qiniu.form_up.FormUploader()
                const putExtra = new qiniu.form_up.PutExtra();
                                // 由於是批量上傳,須要在最後將錯誤對象回調
                let globalError = null;

                  // 遍歷編譯資源文件
                for (const filename of Object.keys(compilation.assets)) {
                    // 開始上傳
                    formUploader.putFile(
                        uploadToken,
                        this.options.qiniu.keyPrefix + filename,
                        path.resolve(compilation.outputOptions.path, filename),
                        putExtra,
                        (err) => {
                            console.log(`uploade ${filename} result: ${err ? `Error:${err.message}` : 'Success'}`)
                            currentUploadedCount++;
                            if (err) {
                                globalError = err;
                            }
                            if (currentUploadedCount === uploadCount) {
                                globalError ? reject(globalError) : resolve();
                            }
                        });
                }
            })
        });
    }
}

module.exports = MyWebpackPlugin;

Webpack中須要傳遞給該插件傳遞相關配置:

module.exports = {
    entry: './src/index',
    target: 'node',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: '[name].js',
          publicPath: 'CDN域名'
    },
    plugins: [
        new CleanWebpackPlugin(),
        new QiniuWebpackPlugin({
            qiniu: {
                accessKey: '七牛AccessKey',
                secretKey: '七牛SecretKey',
                bucket: 'static',
                keyPrefix: 'webpack-inaction/demo1/'
            }
        })
    ]
};

編譯完成後資源會自動上傳到七牛CDN,這樣前端只用交付index.html便可。

小結

至此,Webpack相關經常使用知識和進階知識都介紹完畢,須要各位讀者在工做中去多加探索,Webpack配合Node.js生態,必定會涌現出更多優秀的新語言和新工具!

相關文章
相關標籤/搜索