本文首發於政採雲前端團隊博客:手把手帶你入門Webpack Plugin前端
https://www.zoo.team/article/webpack-plugin
關於 Webpack
在講 Plugin 以前,咱們先來了解下 Webpack。本質上,Webpack 是一個用於現代 JavaScript 應用程序的靜態模塊打包工具。它可以解析咱們的代碼,生成對應的依賴關係,而後將不一樣的模塊達成一個或多個 bundle。webpack
Webpack 的基本概念包括了以下內容:git
-
Entry:Webpack 的入口文件,指的是應該從哪一個模塊做爲入口,來構建內部依賴圖。 -
Output:告訴 Webpack 在哪輸出它所建立的 bundle 文件,以及輸出的 bundle 文件該如何命名、輸出到哪一個路徑下等規則。 -
Loader:模塊代碼轉化器,使得 Webpack 有能力去處理除了 JS、JSON 之外的其餘類型的文件。 -
Plugin:Plugin 提供執行更廣的任務的功能,包括:打包優化,資源管理,注入環境變量等。 -
Mode:根據不一樣運行環境執行不一樣優化參數時的必要參數。 -
Browser Compatibility:支持全部 ES5 標準的瀏覽器(IE8 以上)。
瞭解完 Webpack 的基本概念以後,咱們再來看下,爲何咱們會須要 Plugin。web
Plugin 的做用
我先舉一個咱們政採雲內部的案例:json
在 React 項目中,通常咱們的 Router 文件是寫在一個項目中的,若是項目中包含了許多頁面,難免會出現全部業務模塊 Router 耦合的狀況,因此咱們開發了一個 Plugin,在構建打包時,該 Plugin 會讀取全部的文件夾下的 index.js 文件,再合併到一塊兒造成一個統一的 Router 文件,輕鬆解決業務耦合問題。這就是 Plugin 的應用(具體實現會在最後一小節說明)。api
來看一下咱們合成前項目代碼結構:promise
├── 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 以後的結構:瀏覽器
├── 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 的做用總結以下:微信
-
提供了 Loader 沒法解決的一些其餘事情 -
提供強大的擴展方法,能執行更廣的任務
瞭解完 Plugin 的大體做用以後,咱們來聊一聊如何建立一個 Plugin。app
建立一個 Plugin
Hook
在聊建立 Plugin 以前,咱們先來聊一下什麼是 Hook。
Webpack 在編譯的過程當中會觸發一系列流程,而在這樣一連串的流程中,Webpack 把一些關鍵的流程節點暴露出來供開發者使用,這就是 Hook,能夠類比 React 的生命週期鉤子。
Plugin 就是在這些 Hook 上暴露出方法供開發者作一些額外操做,在寫 Plugin 的時候,也須要先了解咱們應該在哪一個 Hook 上作操做。
如何建立 Plugin
咱們先來看一下 Webpack 官方給的案例:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// 表明開始讀取 records 以前執行
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 構建過程開始!");
});
}
}
從上面的代碼咱們能夠總結以下內容:
-
Plugin 其實就是一個類。 -
類須要一個 apply 方法,執行具體的插件方法。 -
插件方法作了一件事情就是在 run 這個 Hook 上註冊了一個同步的打印日誌的方法。 -
apply 方法的入參注入了一個 compiler 實例,compiler 實例是 Webpack 的支柱引擎,表明了 CLI 和 Node API 傳遞的全部配置項。 -
Hook 回調方法注入了 compilation 實例,compilation 可以訪問當前構建時的模塊和相應的依賴。
Compiler 對象包含了 Webpack 環境全部的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack 實例;
Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被建立。Compilation 對象也提供了不少事件回調供插件作擴展。經過 Compilation 也能讀取到 Compiler 對象。
—— 摘自「深刻淺出 Webpack」
-
compiler 實例和 compilation 實例上分別定義了許多 Hooks,能夠經過 實例.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");
舉幾個簡單的例子:
-
上面官方案例中的 run 這個 Hook,會在開始讀取 records 以前執行,它的類型是 AsyncSeriesHook,查看源碼能夠發現,run Hook 既能夠執行同步的 tap 方法,也能夠執行異步的 tapAsync 和 tapPromise 方法,因此如下寫法也是能夠的:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tapAsync(pluginName, (compilation, callback) => {
setTimeout(() => {
console.log("webpack 構建過程開始!");
callback(); // callback 方法爲了讓構建繼續執行下去,必需要調用
}, 1000);
});
}
}
-
再舉一個例子,好比 failed 這個 Hook,會在編譯失敗以後執行,它的類型是 SyncHook,查看源碼能夠發現,調用 tapAsync 和 tapPromise 方法時,會直接拋錯。
對於一些同步的方法,推薦直接使用 tap 進行註冊方法,對於異步的方案,tapAsync 經過執行 callback 方法實現回調,若是執行的方法返回的是一個 Promise,推薦使用 tapPromise 進行方法的註冊。
Hook 的類型能夠經過官方 API 查詢,地址傳送門:https://www.webpackjs.com/api/compiler-hooks/?fileGuid=3tGHdrykRgwCyTP8
// 源碼取自: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 是什麼。
Webpack && Tapable
Webpack 運行機制
要理解 Plugin,咱們先大體瞭解 Webpack 打包的流程
-
咱們打包的時候,會先合併 Webpack config 文件和命令行參數,合併爲 options。 -
將 options 傳入 Compiler 構造方法,生成 compiler 實例,並實例化了 Compiler 上的 Hooks。 -
compiler 對象執行 run 方法,並自動觸發 beforeRun、run、beforeCompile、compile 等關鍵 Hooks。 -
調用 Compilation 構造方法建立 compilation 對象,compilation 負責管理全部模塊和對應的依賴,建立完成後觸發 make Hook。 -
執行 compilation.addEntry() 方法,addEntry 用於分析全部入口文件,逐級遞歸解析,調用 NormalModuleFactory 方法,爲每一個依賴生成一個 Module 實例,並在執行過程當中觸發 beforeResolve、resolver、afterResolve、module 等關鍵 Hooks。 -
將第 5 步中生成的 Module 實例做爲入參,執行 Compilation.addModule() 和 Compilation.buildModule() 方法遞歸建立模塊對象和依賴模塊對象。 -
調用 seal 方法生成代碼,整理輸出主文件和 chunk,並最終輸出。
Tapable
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;
}
常見 Hooks API
能夠參考 Webpack:https://www.webpackjs.com/api/compiler-hooks/?fileGuid=3tGHdrykRgwCyTP8
本文列舉一些經常使用 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 在項目中的應用
講完這麼多理論知識,接下來咱們來看一下 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 來應用到本身的項目中。
文章中若有不對的地方,歡迎指正。
看完兩件事
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我兩件小事
1.點個「在看」,讓更多人也能看到這篇內容(點了「在看」,bug -1 😊)
招賢納士
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com
本文分享自微信公衆號 - 政採雲前端團隊(Zoo-Team)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。