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


Webpack通過Loader完成模塊的轉換工作,讓“一切皆模塊”成為可能。Plugin機制則讓其更加靈活,可以在Webpack生命周期中調用鈎子完成各種任務,包括修改輸出資源、輸出目錄等等。

本章我們一起來學習如何編寫Webpack插件。

基本構建流程

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

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

插件示例

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

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

module.exports = MyWebpackPlugin;

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

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

Webpack在啟動時會實例化插件對象,在初始化compiler對象之后會調用插件實例的apply方法,傳入compiler對象,插件實例在apply方法中會注冊感興趣的鈎子,Webpack在執行過程中會根據構建階段回調相應的鈎子。

Compiler && Compilation對象

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

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

常見鈎子

Webpack會根據執行流程來回調對應的鈎子,下面我們來看看都有哪些常見鈎子,這些鈎子支持的tap操作是什么。

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

Tapable

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

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

tap

tap是一個同步鈎子,同步鈎子在使用時不可以包含異步調用,因為函數返回時異步邏輯有可能未執行完畢導致問題。

下面一個在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,最終前端界面使用的是CDN服務器上的靜態資源。

下面我們編寫一個Webpack插件,文件構建完成后上傳CDN。

筆者使用的是七牛CDN,各位讀者根據需要選擇適合自己的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生態,一定會涌現出更多優秀的新語言和新工具!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM