这是第 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