当前位置: 首页 > news >正文

从Yargs源码学习中间件的设计

alt

yargs中间件介绍

yargs 是一个用于解析命令行参数的流行库,它能帮助开发者轻松地定义 CLI(命令行接口),并提供参数处理、命令组织、help文本自动生成等功能。今天我们来学习一下它对中间件的支持。

中间件的API详细信息,可以查看这里:https://yargs.js.org/docs/#api-reference-middlewarecallbacks-applybeforevalidation

在 yargs 中,中间件(Middleware)是一种用于在命令行参数解析过程中,插入自定义逻辑的机制。中间件能够在参数验证之前或之后执行,允许开发者对 argv 对象进行操作或修改。通过中间件,开发者可以对命令行输入进行预处理、验证、转化,或者根据业务需求添加自定义操作。

其用途主要是这三个:

  1. 参数的预处理:中间件可以在命令行参数验证之前执行,处理或修改 argv,比如对输入进行格式化。
  2. 参数验证后的处理:中间件也可以在参数验证之后执行,用于进一步处理解析后的数据或生成特定的输出。
  3. 全局中间件:中间件可以作用于所有的命令或选项,称为全局中间件。通过全局中间件,可以对所有命令的输入进行统一处理。

全局中间件

我们先看看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 中间件,然后重新添加新的中间件。

freezeunfreeze 方法

freeze() {
  this.frozens.push([...this.globalMiddleware]);
}
unfreeze() {
  const frozen = this.frozens.pop();
  if (frozen !== undefinedthis.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/ resargv 等)。
  • 输出:经过处理后的结果,传递给下一个中间件。
  • ** 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 多平台发布

相关文章:

从Yargs源码学习中间件的设计

yargs中间件介绍 yargs 是一个用于解析命令行参数的流行库&#xff0c;它能帮助开发者轻松地定义 CLI&#xff08;命令行接口&#xff09;&#xff0c;并提供参数处理、命令组织、help文本自动生成等功能。今天我们来学习一下它对中间件的支持。 中间件的API详细信息&#xff0…...

高级I/O知识分享【epoll || Reactor ET,LT模式】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;Linux_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;接口 epo…...

Matlab 的.m 文件批量转成py文件

在工作中碰到了一个问题&#xff0c;需要将原来用matlab gui做出来的程序改为python程序&#xff0c;因为涉及到很多文件&#xff0c;所以在网上搜了搜有没有直接能转化的库。参考了【Matlab】一键Matlab代码转python代码详细教程_matlab2python-CSDN博客 这位博主提到的matla…...

【软考】传输层协议TCP与UDP

目录 1. TCP1.1 说明1.2 三次握手 2. UDP3. 例题3.1 例题1 1. TCP 1.1 说明 1.TCP(Transmission Control Protocol&#xff0c;传输控制协议)是整个 TCP/IP 协议族中最重要的协议之一。2.它在IP提供的不可靠数据服务的基础上为应用程序提供了一个可靠的、面向连接的、全双工的…...

Arthas dashboard(当前系统的实时数据面板)

文章目录 二、命令列表2.1 jvm相关命令2.1.1 dashboard&#xff08;当前系统的实时数据面板&#xff09; 二、命令列表 2.1 jvm相关命令 2.1.1 dashboard&#xff08;当前系统的实时数据面板&#xff09; 使用场景&#xff1a; 在 Arthas 中&#xff0c;dashboard 命令用于提…...

微服务保护之熔断降级

在微服务架构中&#xff0c;服务之间的调用是通过网络进行的&#xff0c;网络的不确定性和依赖服务的不可控性&#xff0c;可能导致某个服务出现异常或性能问题&#xff0c;进而引发整个系统的故障&#xff0c;这被称为 微服务雪崩。为了防止这种情况发生&#xff0c;常用的一些…...

TomCat乱码问题

TomCat控制台乱码问题 乱码问题解决&#xff1a; 响应乱码问题 向客户端响应数据&#xff1a; package Servlet;import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servl…...

依赖库查看工具Dependencies

依赖库查看工具&#xff1a;Dependencies Dependencies 是一款 Windows 平台下的静态分析工具&#xff0c;用来分析可执行文件&#xff08;EXE、DLL 等&#xff09;所依赖的动态链接库&#xff08;DLL&#xff09;。它可以帮助开发者和系统管理员快速查找程序在运行时可能缺少的…...

Kafka 下载安装及使用总结

1. 下载安装 官网下载地址&#xff1a;Apache Kafka 下载对应的文件 上传到服务器上&#xff0c;解压 tar -xzf kafka_2.13-3.7.0.tgz目录结果如下 ├── bin │ └── windows ├── config │ └── kraft ├── libs ├── licenses └── site-docs官方文档…...

python实现多个pdf文件合并

打印发票时&#xff0c;需要将pdf合并成一个&#xff0c;单页两张打印。网上一些pdf合并逐渐收费&#xff0c;这玩意儿都能收费&#xff1f;自己写一个脚本使用。 实现代码&#xff1a; 输入pdf文件夹路径data_dir&#xff0c;统计目录下的“合并后的PDF”文件夹下&#xff0c;…...

2409js,学习js2

原文 全局对象 function sayHi() {alert("Hello"); }// 全局对象的函数. window.sayHi(); alert(window.innerHeight);更改背景 document.body.style.background "red";setTimeout(() > document.body.style.background "", 1000);当前地…...

SpellBERT: A Lightweight Pretrained Model for Chinese Spelling Check(EMNLP2021)

SpellBERT: A Lightweight Pretrained Model for Chinese Spelling Check(EMNLP2021) 一.概述 作者认为许多模型利用预定义的混淆集来学习正确字符与其视觉上相似或语音上相似的误用字符之间的映射&#xff0c;但映射可能是域外的。为此&#xff0c;我们提出了SpellBERT&…...

【机器学习】--- 决策树与随机森林

文章目录 决策树与随机森林的改进&#xff1a;全面解析与深度优化目录1. 决策树的基本原理2. 决策树的缺陷及改进方法2.1 剪枝技术2.2 树的深度控制2.3 特征选择的优化 3. 随机森林的基本原理4. 随机森林的缺陷及改进方法4.1 特征重要性改进4.2 树的集成方法优化4.3 随机森林的…...

[SAP ABAP] 创建域

我们可以使用事务码SE11创建域 输入要创建的域的名称&#xff0c;然后点击创建 输入简短描述&#xff0c;选择数据类型和输入字符数 激活并保存域&#xff0c;创建的域才能够生效 补充扩展练习 创建一个有关"性别"基本信息的域...

STM32 通过 SPI 驱动 W25Q128

目录 一、STM32 SPI 框图1、通讯引脚2、时钟控制3、数据控制逻辑4、整体控制逻辑5、主模式收发流程及事件说明如下&#xff1a; 二、程序编写1、SPI 初始化2、W25Q128 驱动代码2.1 读写厂商 ID 和设备 ID2.2 读数据2.3 写使能/写禁止2.4 读/写状态寄存器2.5 擦除扇区2.6 擦除整…...

C#进阶-基于雪花算法的订单号设计与实现

在现代电商系统和分布式系统中&#xff0c;高效地生成全局唯一的订单号是一个关键需求。订单号不仅需要唯一性&#xff0c;还需要具备一定的趋势递增性&#xff0c;以满足数据库索引和排序的需求。本文将介绍如何在C#中使用雪花算法&#xff08;Snowflake&#xff09;设计和实现…...

低版本SqlSugar的where条件中使用可空类型报语法错误

SQLServer数据表中有两列可空列&#xff0c;均为数值类型&#xff0c;同时在数据库中录入测试数据&#xff0c;Age和Height列均部分有值。   使用SqlSugar的DbFirst功能生成数据库表类&#xff0c;其中Age、Height属性均为可空类型。   开始使用的SqlSugar版本较低&…...

跨游戏引擎的H5渲染解决方案(腾讯)

本文是腾讯的一篇H5 跨引擎解决方案的精炼。 介绍 本文通过实现基于精简版的HTML5&#xff08;HyperText Mark Language 5&#xff09;来屏蔽不同引擎&#xff0c;平台底层的差异。 好处&#xff1a; 采用H5的开发方式&#xff0c;可以将开发和运营分离&#xff0c;运营部门自…...

docker构建java镜像,运行镜像出现日志 no main manifest attribute, in /xxx.jar

背景 本文主要是一个随笔,记录一下出现"no main manifest attribute"的解决办法 问题原因 主要是近期在构建一个镜像,在镜像构建成功后,运行一直提示"no main manifest attribute",当时还在想,是不是Dockerfile写错了,后来仔细检查了一下,发现是…...

react + antDesignPro 企业微信扫码登录

效果 实现步骤 1、项目中document.ejs文件引入企微js链接 注意&#xff1a;技术栈是使用的react antDesignPro&#xff0c;不同的技术栈有不同的入口文件&#xff08;如vue在html文件引入&#xff09; <script src"https://wwcdn.weixin.qq.com/node/wework/wwopen/j…...

浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)

✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义&#xff08;Task Definition&…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解

【关注我&#xff0c;后续持续新增专题博文&#xff0c;谢谢&#xff01;&#xff01;&#xff01;】 上一篇我们讲了&#xff1a; 这一篇我们开始讲&#xff1a; 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下&#xff1a; 一、场景操作步骤 操作步…...

Day131 | 灵神 | 回溯算法 | 子集型 子集

Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 笔者写过很多次这道题了&#xff0c;不想写题解了&#xff0c;大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战

在现代战争中&#xff0c;电磁频谱已成为继陆、海、空、天之后的 “第五维战场”&#xff0c;雷达作为电磁频谱领域的关键装备&#xff0c;其干扰与抗干扰能力的较量&#xff0c;直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器&#xff0c;凭借数字射…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)

在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马&#xff08;服务器方面的&#xff09;的原理&#xff0c;连接&#xff0c;以及各种木马及连接工具的分享 文件木马&#xff1a;https://w…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)

船舶制造装配管理现状&#xff1a;装配工作依赖人工经验&#xff0c;装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书&#xff0c;但在实际执行中&#xff0c;工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

搭建DNS域名解析服务器(正向解析资源文件)

正向解析资源文件 1&#xff09;准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2&#xff09;服务端安装软件&#xff1a;bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...