文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-30 01:10

关注

确实如她们所言,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):

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

图片

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

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

图片

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

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

当然,如果想让我们的程序更加健壮,我们可以给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我们就不在这里过多介绍了。大家感兴趣可以去对应的官网查找.

现在,我们需要在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::::new(),
            ignore_case = true
        )]
+        name: Option,
         #[arg(
            short = 'a',
            lnotallow="address",
            help = "地址信息",
            requires = "name",
+           value_parser = EnumValueParser::
::new(), )] + address: Option
, } }

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

主函数

其实,这步的操作和之前是差不多的,我们还是利用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,
    address: Option
) -> 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进行对应值的匹配。

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

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

图片

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

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。它支持单选、多选、选择日历等功能:

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

图片


来源:前端柒八九内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯