Webpack經過Loader完成模塊的轉換工做,讓「一切皆模塊」成爲可能。Plugin機制則讓其更加靈活,能夠在Webpack生命週期中調用鉤子完成各類任務,包括修改輸出資源、輸出目錄等等。javascript
今天咱們一塊兒來學習如何編寫Webpack插件。html
在編寫插件以前,還須要瞭解一下Webpack的構建流程,以便在合適的時機插入合適的插件邏輯。Webpack的基本構建流程以下:前端
一個典型的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
在編寫Webpack插件過程當中,最經常使用也是最主要的兩個對象就是Webpack提供的Compiler和Compilation,Plugin經過訪問Compiler和Compilation對象來完成工做。web
Webpack會根據執行流程來回調對應的鉤子,下面咱們來看看都有哪些常見鉤子,這些鉤子支持的tap操做是什麼。npm
鉤子 | 說明 | 參數 | 類型 |
---|---|---|---|
afterPlugins | 啓動一次新的編譯 | compiler | 同步 |
compile | 建立compilation對象以前 | compilationParams | 同步 |
compilation | compilation對象建立完成 | compilation | 同步 |
emit | 資源生成完成,輸出以前 | compilation | 異步 |
afterEmit | 資源輸出到目錄完成 | compilation | 異步 |
done | 完成編譯 | stats | 同步 |
Tapable是Webpack的一個核心工具,Webpack中許多對象擴展自Tapable類。Tapable類暴露了tap、tapAsync和tapPromise方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯。json
tap是一個同步鉤子,同步鉤子在使用時不能夠包含異步調用,由於函數返回時異步邏輯有可能未執行完畢致使問題。bash
下面一個在compile階段插入同步鉤子的示例。
compiler.hooks.compile.tap('MyWebpackPlugin', params => { console.log('我是同步鉤子') });
tapAsync是一個異步鉤子,咱們能夠經過callback告知Webpack異步邏輯執行完畢。
下面是一個在emit階段的示例,在1秒後打印文件列表。
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => { setTimeout(()=>{ console.log('文件列表', Object.keys(compilation.assets).join(',')); callback(); }, 1000); });
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函數('插件名稱', (階段回調參數) => { });
在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生態,必定會涌現出更多優秀的新語言和新工具!