這是第 101 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客:手把手帶你入門Webpack Pluginjavascript
在講 Plugin 以前,咱們先來了解下 Webpack。本質上,Webpack 是一個用於現代 JavaScript 應用程序的靜態模塊打包工具。它可以解析咱們的代碼,生成對應的依賴關係,而後將不一樣的模塊達成一個或多個 bundle。前端
Webpack 的基本概念包括了以下內容:java
瞭解完 Webpack 的基本概念以後,咱們再來看下,爲何咱們會須要 Plugin。webpack
我先舉一個咱們政採雲內部的案例:git
在 React 項目中,通常咱們的 Router 文件是寫在一個項目中的,若是項目中包含了許多頁面,難免會出現全部業務模塊 Router 耦合的狀況,因此咱們開發了一個 Plugin,在構建打包時,該 Plugin 會讀取全部文件夾下的 index.js 文件,再合併到一塊兒造成一個統一的 Router 文件,輕鬆解決業務耦合問題。這就是 Plugin 的應用(具體實現會在最後一小節說明)。web
來看一下咱們合成前項目代碼結構:typescript
├── 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 以後的結構:json
├── 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 的做用以下:api
瞭解完 Plugin 的大體做用以後,咱們來聊一聊如何建立一個 Plugin。promise
在聊建立 Plugin 以前,咱們先來聊一下什麼是 Hook。
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