从Yargs源码学习中间件的设计
yargs中间件介绍
yargs 是一个用于解析命令行参数的流行库,它能帮助开发者轻松地定义 CLI(命令行接口),并提供参数处理、命令组织、help文本自动生成等功能。今天我们来学习一下它对中间件的支持。
中间件的API详细信息,可以查看这里:https://yargs.js.org/docs/#api-reference-middlewarecallbacks-applybeforevalidation
在 yargs 中,中间件(Middleware)是一种用于在命令行参数解析过程中,插入自定义逻辑的机制。中间件能够在参数验证之前或之后执行,允许开发者对 argv 对象进行操作或修改。通过中间件,开发者可以对命令行输入进行预处理、验证、转化,或者根据业务需求添加自定义操作。
其用途主要是这三个:
-
参数的预处理:中间件可以在命令行参数验证之前执行,处理或修改 argv,比如对输入进行格式化。 -
参数验证后的处理:中间件也可以在参数验证之后执行,用于进一步处理解析后的数据或生成特定的输出。 -
全局中间件:中间件可以作用于所有的命令或选项,称为全局中间件。通过全局中间件,可以对所有命令的输入进行统一处理。
全局中间件
我们先看看api的介绍
.middleware(callbacks, [applyBeforeValidation])
-
callbacks
:可以是一个函数或函数列表。每个回调函数都会接收一个对 argv 的引用,argv 是一个包含命令行参数的对象。 -
applyBeforeValidation
(可选):布尔值,默认为 false。如果设置为 true,则中间件将在验证之前执行,但在解析之后。
从这里可以看出,其可以定义单个或者多个中间件,也可以定义执行顺序。
下面来看几个例子:
const mwFunc1 = argv => console.log('I\'m a middleware function');
const mwFunc2 = argv => console.log('I\'m another middleware function');
yargs
.command('myCommand', 'some command', {}, function(argv){
console.log('Running myCommand!');
})
.middleware([mwFunc1, mwFunc2]).parse();
在这个例子中,当从命令行调用 myCommand 时,mwFunc1 首先被调用,然后是 mwFunc2,最后是命令的处理函数。控制台的输出将是:
I'm a middleware function
I'm another middleware function
Running myCommand!
require('yargs/yargs')(process.argv.slice(2))
.middleware(function (argv) {
if (process.env.HOME) argv.home = process.env.HOME
}, true)
.command('configure-home', "do something with a user's home directory",
{
'home': {
'demand': true,
'string': true
}
},
function(argv) {
console.info(`we know the user's home directory is ${argv.home}`)
}
)
.parse()
在这个例子中,中间件用于从环境变量中填充 home 目录。由于中间件会接受一个形参argv,所以其也可以对该参数做二次修改。
command中间件
command中间件只对当前command生效,其会强制把applyBeforeValidation
参数设置为false
。其接口形式如下:
.command(cmd, desc, [builder], [handler])
command中间件只会在command运行的时候执行,所以它将晚于全局中间件执行。
require('yargs')
.command('$0', 'accept username', () => {}, (argv) => {
// The middleware will have been applied before the default
// command is called:
console.info(argv);
})
.choices('user', ['Goofy', 'Miky'])
.middleware(argv => {
console.info('gots here');
const user = argv.user;
switch (user) {
case 'Goofy':
argv.user = {
firstName: 'Mark',
lastName: 'Pipe',
};
break;
}
return argv;
})
.parse('--user Miky');
如何实现中间件
前面介绍了两种不同的中间件,那其内部是如何实现的呢?其内部主要依赖middleware.ts来处理全局中间件的添加、应用和管理。它定义了全局中间件的类和相关函数,并提供了工具来处理在命令行解析过程中的中间件逻辑。
https://github.com/yargs/yargs/blob/main/lib/middleware.ts
GlobalMiddleware 类
GlobalMiddleware
是一个管理全局中间件的类,它存储中间件并提供相关操作,如添加、冻结、解冻和重置中间件。
-
globalMiddleware: Middleware[] = []
存储所有注册的中间件。 -
frozens: Array<Middleware[]> = []
用于存储冻结状态下的中间件组,支持回滚到之前的中间件配置。
构造函数
constructor(yargs: YargsInstance) {
this.yargs = yargs;
}
构造函数接受一个 YargsInstance
对象(即 yargs
实例),用于后续调用命令行解析逻辑。
addMiddleware
方法
addMiddleware(
callback: MiddlewareCallback | MiddlewareCallback[],
applyBeforeValidation: boolean,
global = true,
mutates = false
): YargsInstance {
-
功能:该方法允许添加单个或多个中间件。 -
** callback
**:接受中间件函数或中间件数组。 -
** applyBeforeValidation
**:标记中间件是否应该在命令行参数验证之前应用。 -
** global
**:标识中间件是否为全局作用域。 -
** mutates
**:标识中间件是否会修改argv
。
通过该方法添加的中间件会被存储在 globalMiddleware
数组中。
addCoerceMiddleware
方法
addCoerceMiddleware(
callback: MiddlewareCallback,
option: string
): YargsInstance {
-
功能:该方法专门用于处理 coerce
类型的中间件,每个选项只能注册一个coerce
中间件。 -
操作:先过滤掉之前注册的同一选项的 coerce
中间件,然后重新添加新的中间件。
freeze
和 unfreeze
方法
freeze() {
this.frozens.push([...this.globalMiddleware]);
}
unfreeze() {
const frozen = this.frozens.pop();
if (frozen !== undefined) this.globalMiddleware = frozen;
}
-
** freeze
**:将当前的中间件快照保存到frozens
数组中。 -
** unfreeze
**:从frozens
中取出最后保存的快照,并恢复到globalMiddleware
中。
reset
方法
reset() {
this.globalMiddleware = this.globalMiddleware.filter(m => m.global);
}
-
功能:重置中间件,仅保留全局中间件( global: true
)。
工具方法:commandMiddlewareFactory
export function commandMiddlewareFactory(
commandMiddleware?: MiddlewareCallback[]
): Middleware[] {
-
功能:接受命令级中间件数组,并将 applyBeforeValidation
设置为false
,表示这些中间件默认在验证之后应用。
工具方法:applyMiddleware
export function applyMiddleware(
argv: Arguments | Promise<Arguments>,
yargs: YargsInstance,
middlewares: Middleware[],
beforeValidation: boolean
) {
return middlewares.reduce<Arguments | Promise<Arguments>>(
(acc, middleware) => {
if (middleware.applyBeforeValidation !== beforeValidation) {
return acc;
}
if (middleware.mutates) {
if (middleware.applied) return acc;
middleware.applied = true;
}
if (isPromise(acc)) {
return acc
.then(initialObj =>
Promise.all([initialObj, middleware(initialObj, yargs)])
)
.then(([initialObj, middlewareObj]) =>
Object.assign(initialObj, middlewareObj)
);
} else {
const result = middleware(acc, yargs);
return isPromise(result)
? result.then(middlewareObj => Object.assign(acc, middlewareObj))
: Object.assign(acc, result);
}
},
argv
);
}
-
功能:应用所有匹配条件的中间件。 -
** argv
**:代表命令行参数对象,可能是普通对象也可能是Promise
。 -
** middlewares
**:传入的中间件数组。 -
** beforeValidation
**:根据此标识决定是否只应用验证前的中间件。
此函数是核心逻辑,通过 reduce
迭代应用中间件,依次修改 argv
对象。如果 argv
或中间件返回值是 Promise
,则将其转换为异步逻辑处理。
到这里,我们就了解了Yarg是如何实现中间件的了。
中间件知识的迁移
除了Yargs之外,Express、Koa等同样也拥有中间件,其实我们可以从他们身上总结出一套通用的中间件实现,在我们需要的时候,可以迁移到其它场景。
中间件的核心是一种可以在处理逻辑链中插入处理函数的技术。它能够接收输入、处理输入,并将输出传递给下一个中间件,或者返回最终结果。
所以,其是对流程的抽象,中间接负责承接流程中处理的差异,而把调用留给核心主流程。通过中间件的技术,我们可以实现如下几点:
-
分离关注点:中间件允许将应用中的不同功能模块分开,使得每个模块只处理自己关心的部分,例如用户身份验证、错误异常处理等 -
提高代码的可扩展性:通过中间件对外暴露处理函数,使得系统功能更易于扩展 -
简化复杂逻辑:对于复杂的流程,我们可以拆解成多个简单的步骤,既增加了每个步骤的控制性,又简化了流程操作。例如,在处理 HTTP 请求时,可以拆解成:解析请求体 → 检查身份认证 → 处理权限 → 执行主要业务逻辑 → 格式化返回值 → 记录日志 -
提高代码的可扩展性:中间件可以使系统功能更易于扩展。例如,在一个请求处理的生命周期中,添加一个新的功能只需要添加一个中间件。无需修改现有的逻辑,只需将新中间件插入到处理中间。
中间件的基本结构:
function middleware(input, next) {
// 对 input 进行处理
const result = process(input);
// 调用下一个中间件
return next(result);
}
通用结构包括:
-
输入:通常是某种上下文对象(如 req
/res
、argv
等)。 -
输出:经过处理后的结果,传递给下一个中间件。 -
** next
**:指向下一个中间件的函数或处理器。
中间件的注册与存储
为了灵活添加和管理中间件,通常需要将中间件存储在一个有序列表中,便于按顺序执行。
通用的中间件存储和注册方法:
class MiddlewareManager {
constructor() {
this.middlewares = [];
}
addMiddleware(middleware) {
this.middlewares.push(middleware);
}
getMiddlewares() {
return this.middlewares;
}
}
中间件执行控制
在某些情况下,中间件需要有能力决定是否中止链的执行。这通常通过不调用 next
来实现。
通用模式:
function middleware(input, next) {
if (shouldStop(input)) {
return input; // 不调用 next,中止链的执行
}
return next(input);
}
在 HTTP 请求处理中,可能会根据某些条件终止请求处理链并直接返回响应。同样地,在命令行工具中,某些条件下可以提前结束中间件链的执行。
中间件的管理与重置
中间件链可以根据业务需求进行管理、冻结、解冻和重置,这通常用在特定场景下修改中间件或者重置中间件的行为。
通用模式:
class MiddlewareManager {
constructor() {
this.middlewares = [];
this.frozens = [];
}
freeze() {
this.frozens.push([...this.middlewares]);
}
unfreeze() {
const frozen = this.frozens.pop();
if (frozen) this.middlewares = frozen;
}
reset() {
this.middlewares = this.middlewares.filter(m => m.global);
}
}
这种机制允许保存当前的中间件状态,并在需要时恢复。
如果需要灵活的配置,还可以给中间件附加上option的配置项。
执行中间件链
核心是按顺序调用中间件。可以通过 reduce
或递归的方式将中间件串联起来。每个中间件完成当前处理后,需要决定是否将处理权传递给下一个中间件。
通用的执行逻辑:
function executeMiddlewares(input, middlewares) {
let index = -1;
function next(currentInput) {
index++;
if (index < middlewares.length) {
return middlewares[index](currentInput, next);
}
return currentInput; // 所有中间件处理完成后的结果
}
return next(input);
}
这里的 next
函数控制中间件的执行顺序,每次调用都会进入下一个中间件。
支持异步中间件
在实际应用中,很多中间件需要处理异步操作(如数据库查询、HTTP 请求等)。因此,中间件链需要支持异步操作。
通用的异步中间件支持:
async function executeAsyncMiddlewares(input, middlewares) {
let index = -1;
async function next(currentInput) {
index++;
if (index < middlewares.length) {
const result = await middlewares[index](currentInput, next);
return result;
}
return currentInput;
}
return next(input);
}
这可以确保异步中间件正确地等待 Promise
解决后再执行下一个中间件。
大白话总结一下,中间件就是管理一堆函数,并在特定的时候调用这些函数。
总结
yargs 的中间件为命令行工具的开发提供了极大的灵活性。通过中间件,开发者可以轻松地定制参数解析和处理的过程,适用于复杂的命令行应用场景。
我们也可以将中间件的思维迁移到我们的业务开发中,对于复杂的流程,做好模块拆分,就可以增加一个Middlewares来管理对应模块的处理函数,并在流程需要的时候调用他们。
本文由 mdnice 多平台发布
原文地址:https://blog.csdn.net/zmh_fuhuasishui/article/details/142430355
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!