這是第 101 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: 手把手帶你入門Webpack Plugin
在講 Plugin 以前,咱們先來了解下 Webpack。本質上,Webpack 是一個用於現代 JavaScript 應用程序的靜態模塊打包工具。它可以解析咱們的代碼,生成對應的依賴關係,而後將不一樣的模塊達成一個或多個 bundle。javascript
Webpack 的基本概念包括了以下內容:前端
瞭解完 Webpack 的基本概念以後,咱們再來看下,爲何咱們會須要 Plugin。java
我先舉一個咱們政採雲內部的案例:webpack
在 React 項目中,通常咱們的 Router 文件是寫在一個項目中的,若是項目中包含了許多頁面,難免會出現全部業務模塊 Router 耦合的狀況,因此咱們開發了一個 Plugin,在構建打包時,該 Plugin 會讀取全部文件夾下的 index.js 文件,再合併到一塊兒造成一個統一的 Router 文件,輕鬆解決業務耦合問題。這就是 Plugin 的應用(具體實現會在最後一小節說明)。git
來看一下咱們合成前項目代碼結構:web
├── package.json ├── README.md ├── zoo.config.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .stylelintrc ├── build (Webpack 配置目錄) │ └── webpack.dev.conf.js ├── src │ ├── index.hbs │ ├── main.js (入口文件) │ ├── common (通用模塊,包權限,統一報錯攔截等) │ └── ... │ ├── components (項目公共組件) │ └── ... │ ├── layouts (項目頂通) │ └── ... │ ├── utils (公共類) │ └── ... │ ├── routes (頁面路由) │ │ ├── Hello (對應 Hello 頁面的代碼) │ │ │ ├── config (頁面配置信息) │ │ │ └── ... │ │ │ ├── models (dva數據中心) │ │ │ └── ... │ │ │ ├── services (請求相關接口定義) │ │ │ └── ... │ │ │ ├── views (請求相關接口定義) │ │ │ └── ... │ │ │ └── index.js (router定義的路由信息) ├── .eslintignore ├── .eslintrc ├── .gitignore └── .stylelintrc
再看一下通過 Plugin 合成 Router 以後的結構:typescript
├── package.json ├── README.md ├── zoo.config.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .stylelintrc ├── build (Webpack 配置目錄) │ └── webpack.dev.conf.js ├── src │ ├── index.hbs │ ├── main.js (入口文件) │ ├── router-config.js (合成後的router文件) │ ├── common (通用模塊,包權限,統一報錯攔截等) │ └── ... │ ├── components (項目公共組件) │ └── ... │ ├── layouts (項目頂通) │ └── ... │ ├── utils (公共類) │ └── ... │ ├── routes (頁面路由) │ │ ├── Hello (對應 Hello 頁面的代碼) │ │ │ ├── config (頁面配置信息) │ │ │ └── ... │ │ │ ├── models (dva數據中心) │ │ │ └── ... │ │ │ ├── services (請求相關接口定義) │ │ │ └── ... │ │ │ ├── views (請求相關接口定義) │ │ │ └── ... ├── .eslintignore ├── .eslintrc ├── .gitignore └── .stylelintrc
總結來講 Plugin 的做用以下:json
瞭解完 Plugin 的大體做用以後,咱們來聊一聊如何建立一個 Plugin。api
在聊建立 Plugin 以前,咱們先來聊一下什麼是 Hook。promise
Webpack 在編譯的過程當中會觸發一系列流程,而在這樣一連串的流程中,Webpack 把一些關鍵的流程節點暴露出來供開發者使用,這就是 Hook,能夠類比 React 的生命週期鉤子。
Plugin 就是在這些 Hook 上暴露出方法供開發者作一些額外操做,在寫 Plugin 的時候,也須要先了解咱們應該在哪一個 Hook 上作操做。
咱們先來看一下 Webpack 官方給的案例:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { // 表明開始讀取 records 以前執行 compiler.hooks.run.tap(pluginName, compilation => { console.log("webpack 構建過程開始!"); }); } }
從上面的代碼咱們能夠總結以下內容:
Compiler 對象包含了 Webpack 環境全部的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack 實例; Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被建立。Compilation 對象也提供了不少事件回調供插件作擴展。經過 Compilation 也能讀取到 Compiler 對象。 —— 摘自「深刻淺出 Webpack」
實例.hooks.具體Hook
訪問,Hook 上還暴露了 3 個方法供使用,分別是 tap、tapAsync 和 tapPromise。這三個方法用於定義如何執行 Hook,好比 tap 表示註冊同步 Hook,tapAsync 表明 callback 方式註冊異步 hook,而 tapPromise 表明 Promise 方式註冊異步 Hook,能夠看下 Webpack 中關於這三種類型實現的源碼,爲方便閱讀,我加了些註釋。// tap方法的type是sync,tapAsync方法的type是async,tapPromise方法的type是promise // 源碼取自Hook工廠方法:lib/HookCodeFactory.js create(options) { this.init(options); let fn; // Webpack 經過new Function 生成函數 switch (this.options.type) { case "sync": fn = new Function( this.args(), // 生成函數入參 '"use strict";\n' + this.header() + // 公共方法,生成一些須要定義的變量 this.contentWithInterceptors({ // 生成實際執行的代碼的方法 onError: err => `throw ${err};\n`, // 錯誤回調 onResult: result => `return ${result};\n`, // 獲得值的時候的回調 resultReturns: true, onDone: () => "", rethrowIfPossible: true }) ); break; case "async": fn = new Function( this.args({ after: "_callback" }), '"use strict";\n' + this.header() + // 公共方法,生成一些須要定義的變量 this.contentWithInterceptors({ onError: err => `_callback(${err});\n`, // 錯誤時執行回調方法 onResult: result => `_callback(null, ${result});\n`, // 獲得結果時執行回調方法 onDone: () => "_callback();\n" // 無結果,執行完成時 }) ); break; case "promise": let errorHelperUsed = false; const content = this.contentWithInterceptors({ onError: err => { errorHelperUsed = true; return `_error(${err});\n`; }, onResult: result => `_resolve(${result});\n`, onDone: () => "_resolve();\n" }); let code = ""; code += '"use strict";\n'; code += this.header(); // 公共方法,生成一些須要定義的變量 code += "return new Promise((function(_resolve, _reject) {\n"; // 返回的是 Promise if (errorHelperUsed) { code += "var _sync = true;\n"; code += "function _error(_err) {\n"; code += "if(_sync)\n"; code += "_resolve(Promise.resolve().then((function() { throw _err; })));\n"; code += "else\n"; code += "_reject(_err);\n"; code += "};\n"; } code += content; // 判斷具體執行_resolve方法仍是執行_error方法 if (errorHelperUsed) { code += "_sync = false;\n"; } code += "}));\n"; fn = new Function(this.args(), code); break; } this.deinit(); // 清空 options 和 _args return fn; }
Webpack 共提供瞭如下十種 Hooks,代碼中全部具體的 Hook 都是如下這 10 種中的一種。
// 源碼取自:lib/index.js "use strict"; exports.__esModule = true; // 同步執行的鉤子,不能處理異步任務 exports.SyncHook = require("./SyncHook"); // 同步執行的鉤子,返回非空時,阻止向下執行 exports.SyncBailHook = require("./SyncBailHook"); // 同步執行的鉤子,支持將返回值透傳到下一個鉤子中 exports.SyncWaterfallHook = require("./SyncWaterfallHook"); // 同步執行的鉤子,支持將返回值透傳到下一個鉤子中,返回非空時,重複執行 exports.SyncLoopHook = require("./SyncLoopHook"); // 異步並行的鉤子 exports.AsyncParallelHook = require("./AsyncParallelHook"); // 異步並行的鉤子,返回非空時,阻止向下執行,直接執行回調 exports.AsyncParallelBailHook = require("./AsyncParallelBailHook"); // 異步串行的鉤子 exports.AsyncSeriesHook = require("./AsyncSeriesHook"); // 異步串行的鉤子,返回非空時,阻止向下執行,直接執行回調 exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook"); // 支持異步串行 && 並行的鉤子,返回非空時,重複執行 exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook"); // 異步串行的鉤子,下一步依賴上一步返回的值 exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook"); // 如下 2 個是 hook 工具類,分別用於 hooks 映射以及 hooks 重定向 exports.HookMap = require("./HookMap"); exports.MultiHook = require("./MultiHook");
舉幾個簡單的例子:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => { setTimeout(() => { console.log("webpack 構建過程開始!"); callback(); // callback 方法爲了讓構建繼續執行下去,必需要調用 }, 1000); }); } }
對於一些同步的方法,推薦直接使用 tap 進行註冊方法,對於異步的方案,tapAsync 經過執行 callback 方法實現回調,若是執行的方法返回的是一個 Promise,推薦使用 tapPromise 進行方法的註冊
Hook 的類型能夠經過官方 API 查詢,地址傳送門
// 源碼取自:lib/SyncHook.js const TAP_ASYNC = () => { throw new Error("tapAsync is not supported on a SyncHook"); }; const TAP_PROMISE = () => { throw new Error("tapPromise is not supported on a SyncHook"); }; function SyncHook(args = [], name = undefined) { const hook = new Hook(args, name); hook.constructor = SyncHook; hook.tapAsync = TAP_ASYNC; hook.tapPromise = TAP_PROMISE; hook.compile = COMPILE; return hook; }
講解完具體的執行方法以後,咱們再聊一下 Webpack 流程以及 Tapable 是什麼。
要理解 Plugin,咱們先大體瞭解 Webpack 打包的流程
Tapable 是 Webpack 核心工具庫,它提供了全部 Hook 的抽象類定義,Webpack 許多對象都是繼承自 Tapable 類。好比上面說的 tap、tapAsync 和 tapPromise 都是經過 Tapable 進行暴露的。源碼以下(截取了部分代碼):
// 第二節 「建立一個 Plugin」 中說的 10 種 Hooks 都是繼承了這兩個類 // 源碼取自:tapable.d.ts declare class Hook<T, R, AdditionalOptions = UnsetAdditionalOptions> { tap(options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: AsArray<T>) => R): void; } declare class AsyncHook<T, R, AdditionalOptions = UnsetAdditionalOptions> extends Hook<T, R, AdditionalOptions> { tapAsync( options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: Append<AsArray<T>, InnerCallback<Error, R>>) => void ): void; tapPromise( options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: AsArray<T>) => Promise<R> ): void; }
能夠參考 Webpack
本文列舉一些經常使用 Hooks 和其對應的類型:
Compiler Hooks
Hook | type | 調用 |
---|---|---|
run | AsyncSeriesHook | 開始讀取 records 以前 |
compile | SyncHook | 一個新的編譯 (compilation) 建立以後 |
emit | AsyncSeriesHook | 生成資源到 output 目錄以前 |
done | SyncHook | 編譯 (compilation) 完成 |
Compilation Hooks
Hook | type | 調用 |
---|---|---|
buildModule | SyncHook | 在模塊構建開始以前觸發 |
finishModules | SyncHook | 全部模塊都完成構建 |
optimize | SyncHook | 優化階段開始時觸發 |
講完這麼多理論知識,接下來咱們來看一下 Plugin 在項目中的實戰:如何將各個子模塊中的 router 文件合併到 router-config.js 中。
在 React 項目中,通常咱們的 Router 文件是寫在一個項目中的,若是項目中包含了許多頁面,難免會出現全部業務模塊 Router 耦合的狀況,因此咱們開發了一個 Plugin,在構建打包時,該 Plugin 會讀取全部文件夾下的 Router 文件,再合併到一塊兒造成一個統一的 Router Config 文件,輕鬆解決業務耦合問題。這就是 Plugin 的應用。
const fs = require('fs'); const path = require('path'); const _ = require('lodash'); function resolve(dir) { return path.join(__dirname, '..', dir); } function MegerRouterPlugin(options) { // options是配置文件,你能夠在這裏進行一些與options相關的工做 } MegerRouterPlugin.prototype.apply = function (compiler) { // 註冊 before-compile 鉤子,觸發文件合併 compiler.plugin('before-compile', (compilation, callback) => { // 最終生成的文件數據 const data = {}; const routesPath = resolve('src/routes'); const targetFile = resolve('src/router-config.js'); // 獲取路徑下全部的文件和文件夾 const dirs = fs.readdirSync(routesPath); try { dirs.forEach((dir) => { const routePath = resolve(`src/routes/${dir}`); // 判斷是不是文件夾 if (!fs.statSync(routePath).isDirectory()) { return true; } delete require.cache[`${routePath}/index.js`]; const routeInfo = require(routePath); // 多個 view 的狀況下,遍歷生成router信息 if (!_.isArray(routeInfo)) { generate(routeInfo, dir, data); // 單個 view 的狀況下,直接生成 } else { routeInfo.map((config) => { generate(config, dir, data); }); } }); } catch (e) { console.log(e); } // 若是 router-config.js 存在,判斷文件數據是否相同,不一樣刪除文件後再生成 if (fs.existsSync(targetFile)) { delete require.cache[targetFile]; const targetData = require(targetFile); if (!_.isEqual(targetData, data)) { writeFile(targetFile, data); } // 若是 router-config.js 不存在,直接生成文件 } else { writeFile(targetFile, data); } // 最後調用 callback,繼續執行 webpack 打包 callback(); }); }; // 合併當前文件夾下的router數據,並輸出到 data 對象中 function generate(config, dir, data) { // 合併 router mergeConfig(config, dir, data); // 合併子 router getChildRoutes(config.childRoutes, dir, data, config.url); } // 合併 router 數據到 targetData 中 function mergeConfig(config, dir, targetData) { const { view, models, extraModels, url, childRoutes, ...rest } = config; // 獲取 models,並去除 src 字段 const dirModels = getModels(`src/routes/${dir}/models`, models); const data = { ...rest, }; // view 拼接到 path 字段 data.path = `${dir}/views${view ? `/${view}` : ''}`; // 若是有 extraModels,就拼接到 models 對象上 if (dirModels.length || (extraModels && extraModels.length)) { data.models = mergerExtraModels(config, dirModels); } Object.assign(targetData, { [url]: data, }); } // 拼接 dva models function getModels(modelsDir, models) { if (!fs.existsSync(modelsDir)) { return []; } let files = fs.readdirSync(modelsDir); // 必需要以 js 或者 jsx 結尾 files = files.filter((item) => { return /\.jsx?$/.test(item); }); // 若是沒有定義 models ,默認取 index.js if (!models || !models.length) { if (files.indexOf('index.js') > -1) { // 去除 src return [`${modelsDir.replace('src/', '')}/index.js`]; } return []; } return models.map((item) => { if (files.indexOf(`${item}.js`) > -1) { // 去除 src return `${modelsDir.replace('src/', '')}/${item}.js`; } }); } // 合併 extra models function mergerExtraModels(config, models) { return models.concat(config.extraModels ? config.extraModels : []); } // 合併子 router function getChildRoutes(childRoutes, dir, targetData, oUrl) { if (!childRoutes) { return; } childRoutes.map((option) => { option.url = oUrl + option.url; if (option.childRoutes) { // 遞歸合併子 router getChildRoutes(option.childRoutes, dir, targetData, option.url); } mergeConfig(option, dir, targetData); }); } // 寫文件 function writeFile(targetFile, data) { fs.writeFileSync(targetFile, `module.exports = ${JSON.stringify(data, null, 2)}`, 'utf-8'); } module.exports = MegerRouterPlugin;
合併前的文件:
module.exports = [ { url: '/category/protocol', view: 'protocol', }, { url: '/category/sync', models: ['sync'], view: 'sync', }, { url: '/category/list', models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'], view: 'categoryRefactor', }, { url: '/category/conversion', models: ['conversion'], view: 'conversion', }, ];
合併後的文件:
module.exports = { "/category/protocol": { "path": "Category/views/protocol" }, "/category/sync": { "path": "Category/views/sync", "models": [ "routes/Category/models/sync.js" ] }, "/category/list": { "path": "Category/views/categoryRefactor", "models": [ "routes/Category/models/category.js", "routes/Category/models/config.js", "routes/Category/models/attributes.js", "routes/Category/models/group.js", "routes/Category/models/otherSet.js", "routes/Category/models/collaboration.js" ] }, "/category/conversion": { "path": "Category/views/conversion", "models": [ "routes/Category/models/conversion.js" ] }, }
最終項目就會生成 router-config.js 文件
但願你們看完本章以後,對 Webpack Plugin 有一個初步的認識,可以上手寫一個本身的 Plugin 來應用到本身的項目中。
文章中若有不對的地方,歡迎指正。
開源地址 www.zoo.team/openweekly/ (小報官網首頁有微信交流羣)
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com