1

Rust 写脚手架,Clap你应该知道的二三事

 1 month ago
source link: https://www.51cto.com/article/783357.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rust 写脚手架,Clap你应该知道的二三事

作者:前端柒八九 2024-03-11 13:18:00
和前端开发类似,当我们把包下载到本地后,我们就需要在对应的入口文件中引入并执行。在前端开发中我们一般挑选的是项目根目录下的index.js。而对于Rust项目来讲,它的入口文件是src/main.rs。

最近,在和前端小伙伴聊天发现,在2024年,她们都有打算入局Rust学习的行列。毕竟前端现在太卷了,框架算是走到「穷途末路」了,无非就是在原有基础上修修补补。所有他们想在新的赛道弯道超车。但是,苦于各种原因,迟迟找不到入门之法。

确实如她们所言,Rust由于学习路径比较陡峭,加之和前端语言可以说是交集很少。然后,给大家一种学了马上就会忘记的感觉。并且,由于现在Rust在前端领域的应用少之又少。除了字节跳动的Rspack,还有Vivo的Vivo Blue OS(我们在国货之光?用Rust编写的Vivo Blue OS有过介绍),就很少听说其他国内互联网公司有相关的产品和应用。

相比国外,我们的道路还任重而道远。像国外很多耳熟能详的公司都早已布局Rust开发。最明显的就是PhotoShop,它已经将只能在桌面运行的PS搬入了浏览器上。(这个我们也在之前的师夷长技以制夷:跟着PS学前端技术中有过相关介绍)

不过,从最新的招聘网站中搜索Rust相关岗位,相比前几年有了很好的改观。并且很多岗位都和前端相关。这说明,Rust在国内已经有了自己的市场,也意味着在前端领域也有了一席之地。那么作为职业前端,不想在红海中继续卷,那势必就需要选择蓝海,方可在千军万马之中,杀出一条光明之路。

其实,像我在学习Rust也遇到很她们一样的困境。知识点看了,也理解了。但是隔断时间就会忘记。周而复始,就会对这门语言产生一种抗拒感。毕竟,编程也算是一种技术工种,唯手熟尔。

后面,我就转变思路,那就是动手做一些自己认为可以解决前端痛点的事。哪怕做这个事情,其他语言也可以胜任,但是为什么我们不做更进一步的尝试呢。现阶段,Rust在前端赋能的场景,大部分都是提高编译效率方向。像Rspack[1]/OXC[2]。

既然,大方向已经定了,然后就有了我们新的尝试。从那开始,就有了我们下面的尝试方向

  1. Rust 开发命令行工具(上)
  2. Rust 开发命令行工具(中)
  3. Rust 编译为 WebAssembly 在前端项目中使用
  4. Game = Rust + WebAssembly + 浏览器
  5. Rust 赋能前端-开发一款属于你的前端脚手架

就是基于上面的不断试错和尝试,到现在我们已经有了像f_cli[3]的npm包,并且已经部署到公司私库,并投入生产开发了。

同时,在最近的项目开发中,还利用Rust编写WebAssembly进行前端功能的处理。这块等有机会写一篇相关的文章。

耽误了大家几分钟的时间,在上面絮叨了半天,其实就是想传达一个思想。Rust其实不可怕,可怕的是学了但是你没用到工作中。就是想着法都要让它贴切工作,应用于工作。

我们回到正题,其实Rust赋能前端这个方向我也在摸索,然后现阶段自我感觉能用到前端项目中的无非就两点

  1. 写一个脚手架,将一些繁琐操作工具化
  2. 写wasm模块,嵌入到前端逻辑中

大家不管是从哪个方面获取Rust知识点,想必大家尝试的第一个Rust应用就是Cli了。

那我们今天就来聊聊在Rust开发Cli时的神器 -clap[4]。

今天,我们只要是讲相关的概念,针对如何用Rust构建一个CLI,可以翻看我们之前的文章。

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 项目初始化
  2. 编写子命令
  3. 添加命令标志
  4. 交互式cli
  5. 其他有用的库

1. 项目初始化

首先,让我们通过运行以下命令来初始化我们的项目:cargo init clap_demo。随后我们再配置一下项目的基础信息。(description等)

[package]
name = "clap_demo"
version = "0.1.0"
edition = "2021"
description = "front789带你学习clap"

我们可以通过运行以下命令将 clap 添加到我们的程序中:

cargo add clap -F derive

这样在Cargo.toml中的[dependencies]中就有了相关的信息。

[dependencies]
clap = { version = "4.5.1", features = ["derive"] }

其中-F表示,我们只需要clap中的derive特性。

图片

图片

上述流程中,我们使用的clap的版本是最新版,有些和大家用过的语法有区别的话,需要大家甄别。

这里多说一嘴,如果对前端开发熟悉的同学是不是感觉到上述流程很熟悉。当我们创建一个前端项目时,是不是会遇到下面的步骤。

npm init 
yarn add xx

和前端开发类似,当我们把包下载到本地后,我们就需要在对应的入口文件中引入并执行。在前端开发中我们一般挑选的是项目根目录下的index.js。而对于Rust项目来讲,它的入口文件是src/main.rs。(作为二进制项目(Binary Projects)而言)

use clap::Parser;

#[derive(Parser)]
#[command(version, about)]
struct Cli {
    name: String
}

fn main() {
    let cli = Cli::parse();
  
    println!("Hello, {}!", cli.name);
}

我们来简单解释一下上面的代码。

在前端开发中我们一般使用import/require进行第三方库的引入,而在Rust中我们使用use来导入第三方库clap中的Parser trait。也就是说,通过use xx我们就可以使用clap中的特定功能。也就是把对应的功能引入到该作用域内。

定义了一个结构体,它使用 clap::Parser 的 derive 宏和command宏,并且只接受一个参数,即 name。

#[derive(Parser)]/#[command(version, about)]不是Rust内置的宏,它们是由clap库自定义的过程宏(procedural macros)。

Rust有两种类型的宏:

  1. 声明式宏(Declarative Macros):

这些是Rust内置的,使用macro_rules定义,例如vec!、println!等。

它们主要用于元编程(metaprogramming),在编译期执行代码生成。

  1. 过程宏(Procedural Macros):
  • 这些是由外部crate定义的,在编译期间像函数一样被调用。
  • 它们可以用来实现自定义的代码生成、lint检查、trait派生,解析、操作和生成 AST等操作。

#[derive(Parser)]它使用 derive 属性来自动为 Cli 结构体实现 Parser trait。这意味着 Cli 结构体将获得解析命令行参数的功能,而无需手动实现 Parser trait。

图片

图片

#[command(version, about)]用于配置命令行应用程序的元数据。

  • version: 设置应用程序的版本信息。
  • about: 设置应用程序的简短描述。这里的信息就是我们在Cargo.toml中配置的description的信息。

最后,我们可以通过cargo run -- --help来查看对应的信息。

图片

图片

总的来说,这段代码使用 clap 库定义了一个命令行应用程序,它接受一个名为 name 的字符串参数。当运行这个应用程序时,它会打印出 "Hello, {name}"。#[derive(Parser)] 和 #[command(...)] 这两个属性分别用于自动实现 Parser trait 和配置应用程序的元数据。

当我们加载程序并使用 Cli::parse() 时,它将从 std::env::args 中获取参数(这个概念我们之前在环境变量:熟悉的陌生人有过介绍)。

  • 如果你尝试运行 cargo run front789,它应该会打印出 Hello, front789!
  • 但如果尝试不添加任何额外值运行它,它将打印出帮助菜单。Clap 在默认特性中包含了一个帮助功能,当输入的命令无效时会自动显示帮助菜单。

当然,如果想让我们的程序更加健壮,我们可以给name设定一个默认值,这样在没有提供参数的情况下,也能合理运行。

#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String
}

现在,尝试仅使用 cargo run 而不添加其他任何东西,它应该会打印出 Hello, front789!。

图片

图片

当然,我们也可以像在f_cli中一样为参数添加更多的配置,来增强我们的Cli。

图片

图片

如果想了解更多关于参数配置,可以翻看clap_command-attributes[5]

图片

图片

2. 编写子命令

作为一个功能强大的CLI,我们有时候需要通过定义一些子命令来让我们的目的更加明确。

如果大家用过我们的f_cli,那就心领神会了。

下图是我们f_cli的根据用户提供的参数,默认构建前端项目的命令。

图片

图片

在f_cli的实现中,我们就用到了子命令的操作。

图片

图片

下面我们来简单实现一个拥有子命令的cli。在之前代码的基础上,我们只需要将刚才结构体中再新增一个参数 - command并且其类型为实现sumcommad trait的枚举

use clap::{ Parser, Subcommand };

#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String,
    #[command(subcommand)]
    command: Commands
}

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create,
    Replace,
    Update,
    Delete
}

fn main() {
    let cli = Cli::parse();
  
    println!("Hello, {:?}!", cli);
}

这样,我们就在上面的基础上拥有了一组子命令(CRUD)。这样我们就可以在cli中调用对应的子命令然后执行对应的操作了。

图片

图片

3. 添加命令标志

我们可以继续丰富我们子命令。上面的我们不是通过一个枚举Commands够了一个组件命令(Create/Replace/Update/Delete)吗。

有时候,在某一个子命令下,还需要收集更多的用户选择。那么我们就可以将枚举中的值关联成一个「匿名结构体」。这样,我们就可以针对某个子命令做更深的操作了。

还是举我们之前的f_cli的例子,在我们通过f_cli create xxx构建项目时,我们可以通过-x来像CLI传递Create所用到的必要信息。

图片

图片

use clap::{ Parser, Subcommand };

#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String,
    #[command(subcommand)]
    command: Commands
}

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(default_value = "front789")]
        name: String,
         #[arg(default_value = "山西")]
        address: String,
    },
    Replace,
    Update,
    Delete
}

这样我们就对Create进一步处理,并且在create的时候,它会从命令行中寻找对应的name/address信息,并且收集到clap实例中。

随后,我们就可以在主函数中通过match来匹配枚举信息,然后执行相对应的操作。

Rust 中的匹配是穷举式的:必须穷举到最后的可能性来使代码有效

为了节约代码量,我们通过_占位符来处理其他的逻辑。

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Create{name,address} => {
            println!("我是{},来自:{}", name,address);
        },
       _=>(),
    }
}

当我们运行cargo run create时,由于我们提供了默认值,在控制台就会输出对应的信息。当然,我们也可以通过-- name xx -- address xx来进行操作。

有人会觉得输入较长的子命令不是很友好,我们可以通过short = 'n'来为子命令提供一个别名。同时我们还可以通过help="xxx"设置对应在--help时,提供给用户的帮助信息。

图片

图片

对应的代码如下:

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(
            short = 'n',
            lnotallow="name",
            help = "用户信息",
            default_value = "front789"
        )]
        name: String,
         #[arg(
            short = 'a',
            lnotallow="address",
            help = "地址信息",
            requires = "name",
            default_value = "山西"
        )]
        address: String,
    },
    Replace,
    Update,
    Delete
}

4. 交互式cli

在上一节中我们通过对CLI枚举进行改造,让其能够拥有了子命令的功能。其实到这步已经能够获取到cli中用户输入的值,并且能够进行下一步的操作了。

但是呢,你是一个精益求精的人。见多识广的你突然有一个想法,为什么不能像vite/create/next一样。在触发对应的构建和更新操作后,有一个「人机交互」的过程。然后,用户可以根据自己的喜好来选择我们cli的内置功能。这样是不是显的更加友好。

像我们的f_cli就是这种交互流程。用户通过人机交互的方式可以选择内置功能。

图片

图片

f_cli 选择UI库

那我们就再次用一个简单的例子来介绍一下哇。

安装新的包

首先,我们需要安装几个用于交互的包。

cargo add anyhow
cargo add dialoguer
cargo add console

随后,就他们就会自动被注入到Cargo.toml中了。关于anyhow/dialoguer/console我们就不在这里过多介绍了。大家感兴趣可以去对应的官网查找.

  • dialoguer[6]
  • console[7]
  • anyhow[8]

现在,我们需要在src/main.rs中引入相关的功能,同时我们在处理cli变量的时候,用的是枚举值,所以我们需要引入clap中针对这类的操作。

use clap::{ 
+    builder::EnumValueParser, 
     Parser, 
     Subcommand, 
+    ValueEnum 
};

+use dialoguer::{ 
+  console::Term, 
+  theme::ColorfulTheme, 
+  Select 
+};
+use console::style;

新增枚举信息

前面说过,我们想通过人机交互的方式,在cli运行过程中让用户自己选择我们内置的功能点。所以,这些内置功能我们可以需要事先设定好。

#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Name {
    N1,
    N2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Address {
    A1,
    A2
}

处理结构体中参数的默认值

既然,已经有了对应的默认值,那么我们就需要限制我们cli中的参数必须是这些内置参数中值。

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(
            short = 'n',
            lnotallow="name",
            help = "用户信息",
+            value_parser = EnumValueParser::<Name>::new(),
            ignore_case = true
        )]
+        name: Option<Name>,
         #[arg(
            short = 'a',
            lnotallow="address",
            help = "地址信息",
            requires = "name",
+           value_parser = EnumValueParser::<Address>::new(),
        )]
+       address: Option<Address>,
    }
}

上面的配置,见名知意,就是从对应的枚举中解析对应的值。

其实,这步的操作和之前是差不多的,我们还是利用match对cli.command进行匹配处理。不过我们这里又进一步的做了容错处理。

  1. 首先判断是否提供子命令
  2. 在提供子命令的情况下,再判断是否是Craete

因为,在进行操作中我们会有错误抛出,所以我们对main的返回值也做了处理。(anyhow::Result<()>)

fn main() ->anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        // - 如果有子命令,则根据子命令执行相应的逻辑;
        Some(command) => {
            match command {
                Commands::Create {
                    name,
                    address,
                } => 
                operation_params(
                  name,
                  address
                )?,
            }
        },
         _ => panic!("Fatal: cli为提供参数,退出处理."),
    }
    Ok(())
}

operation_params

在main中我们通过match是可以获取到cli中参数的,而此时我们还需要根据参数做进一步的处理。我们把这个逻辑提取到了一个函数中了。

fn operation_params (
    name: Option<Name>,
    address: Option<Address>
) -> anyhow::Result<()> {
     let n = match name {
        Some(na) => na,
        None => {
            multiselect_msg("选择一个姓名:");
            message("使用上/下箭头进行选择,使用空格或回车键确认。");
            let items = vec!["张三", "王五"];
            let selection = Select::with_theme(&ColorfulTheme::default())
                .items(&items)
                .default(0)
                .interact_on_opt(&Term::stderr())?;

            match selection {
                Some(0) => Name::N1,
                Some(1) => Name::N2,
                _ => panic!("Fatal: 用户信息制定错误."),
            }
        }
    };
     let a = match address {
        Some(na) => na,
        None => {
            multiselect_msg("选择一个地址:");
            message("使用上/下箭头进行选择,使用空格或回车键确认。");
            let items = vec!["太原", "晋中"];
            let selection = Select::with_theme(&ColorfulTheme::default())
                .items(&items)
                .default(0)
                .interact_on_opt(&Term::stderr())?;

            match selection {
                Some(0) => Address::A1,
                Some(1) => Address::A2,
                _ => panic!("Fatal: 地址信息制定错误."),
            }
        }
    };
    println!("name:{:?},地址:{:?}",n,a);
   Ok(())
}

其实上面的逻辑也是比较简单明了的。 我们接收cli中的参数name/address。因为他们都是枚举类型,所以我们继续用match进行对应值的匹配。

虽然,我们对两个枚举值都做了处理,但是他们的逻辑都是相同的。

上面的逻辑就是当我们运行子命令时候

  • 当提供对应的参数的话,那就原封不动的返回对应的值
  • 当没有提供对应的参数的话,我们就调用dialoguer::Select进行我们预设值的选择。
图片

图片

这样,不管我们上面那种情况,我们最后都可以拿到对应的值。这样我们方便我们后期进行其他操作。

5. 其他有用的库

上面我们通过几个例子,讲了很多clap的应用例子,其中我们还配合dialoguer进行人机交互的处理。如果我们想实现功能更加强大的cli我们还可以借助其他的工具。下面我们就来简单介绍几种。

Crossterm

crossterm[9] 是一款跨终端的crate。 它具有各种很酷的功能,如能够更改背景和文本颜色、操作终端本身和光标,以及捕获键盘和其他事件。

图片

图片

comfy-table

comfy-table[10] 是一个设计用于在终端中创建漂亮表格的 crate。

以下是其官网的案例。用仅仅几句话就可以实现一个在终端展示的表格。

use comfy_table::Table;

fn main() {
    let mut table = Table::new();
    table
        .set_header(vec!["Header1", "Header2", "Header3"])
        .add_row(vec![
            "This is a text",
            "This is another text",
            "This is the third text",
        ])
        .add_row(vec![
            "This is another text",
            "Now\nadd some\nmulti line stuff",
            "This is awesome",
        ]);

    println!("{table}");
}

执行后的效果如下:

+----------------------+----------------------+------------------------+
| Header1              | Header2              | Header3                |
+======================================================================+
| This is a text       | This is another text | This is the third text |
|----------------------+----------------------+------------------------|
| This is another text | Now                  | This is awesome        |
|                      | add some             |                        |
|                      | multi line stuff     |                        |
+----------------------+----------------------+------------------------+

inquire

inquire[11] 是一个用于构建终端上交互式提示的 crate。它支持单选、多选、选择日历等功能:

下面的动图是其官网的案例。其中最吸引我的就是那个多选。哈哈。

图片

图片


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK