努力做好自己,不要好高骛远
大家好,我是柒八九 。一个专注于前端开发技术/Rust
及AI
应用知识分享 的Coder
。
有感而发
最近,在和前端小伙伴
聊天发现,在2024
年,她们都有打算入局Rust
学习的行列。毕竟前端现在太卷了,框架算是走到穷途末路了,无非就是在原有基础上修修补补。所有他们想在新的赛道弯道超车。但是,苦于各种原因,迟迟找不到入门之法。
确实如她们所言,Rust
由于学习路径比较陡峭,加之和前端语言可以说是交集很少。然后,给大家一种学了马上就会忘记的感觉。并且,由于现在Rust
在前端领域的应用少之又少。除了字节跳动的Rspack
,还有Vivo
的 Vivo Blue OS
(我们在国货之光?用Rust编写的Vivo Blue OS有过介绍),就很少听说其他国内互联网公司有相关的产品和应用。
相比国外,我们的道路还任重而道远。像国外很多耳熟能详的公司都早已布局Rust
开发。最明显的就是PhotoShop
,它已经将只能在桌面运行的PS
搬入了浏览器上。(这个我们也在之前的师夷长技以制夷:跟着PS学前端技术中有过相关介绍)
不过,从最新的招聘网站中搜索Rust
相关岗位,相比前几年有了很好的改观。并且很多岗位都和前端相关。这说明,Rust
在国内已经有了自己的市场,也意味着在前端领域也有了一席之地。那么作为职业前端,不想在红海中继续卷,那势必就需要选择蓝海,方可在千军万马之中,杀出一条光明之路。
其实,像我在学习Rust
也遇到很她们一样的困境。知识点看了,也理解了。但是隔断时间就会忘记。周而复始,就会对这门语言产生一种抗拒感。毕竟,编程也算是一种技术工种,唯手熟尔。
后面,我就转变思路,那就是动手做一些自己认为可以解决前端痛点的事。哪怕做这个事情,其他语言也可以胜任,但是为什么我们不做更进一步的尝试呢。现阶段,Rust
在前端赋能的场景,大部分都是提高编译效率方向。像Rspack/OXC。
既然,大方向已经定了,然后就有了我们新的尝试。从那开始,就有了我们下面的尝试方向
- Rust 开发命令行工具(上)
- Rust 开发命令行工具(中)
- Rust 编译为 WebAssembly 在前端项目中使用
- Game = Rust + WebAssembly + 浏览器
- Rust 赋能前端-开发一款属于你的前端脚手架
就是基于上面的不断试错和尝试,到现在我们已经有了像f_cli的npm
包,并且已经部署到公司私库,并投入生产开发了。
同时,在最近的项目开发中,还利用Rust
编写WebAssembly
进行前端功能的处理。这块等有机会写一篇相关的文章。
前言
耽误了大家几分钟的时间,在上面絮叨了半天,其实就是想传达一个思想。Rust
其实不可怕,可怕的是学了但是你没用到工作中。就是想着法都要让它贴切工作,应用于工作。
我们回到正题,其实Rust
赋能前端这个方向我也在摸索,然后现阶段自我感觉能用到前端项目中的无非就两点
- 写一个脚手架,将一些繁琐操作工具化
- 写
wasm
模块,嵌入到前端逻辑中
大家不管是从哪个方面获取Rust
知识点,想必大家尝试的第一个Rust
应用就是Cli
了。
那我们今天就来聊聊在Rust
开发Cli
时的神器 -clap。
今天,我们只要是讲相关的概念,针对如何用Rust
构建一个CLI
,可以翻看我们之前的文章。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 项目初始化
- 编写子命令
- 添加命令标志
- 交互式cli
- 其他有用的库
1. 项目初始化
首先,让我们通过运行以下命令来初始化我们的项目:cargo init clap_demo
。随后我们再配置一下项目的基础信息。(description
等)
toml
[package]
name = "clap_demo"
version = "0.1.0"
edition = "2021"
description = "front789带你学习clap"
我们可以通过运行以下命令将 clap
添加到我们的程序中:
shell
cargo add clap -F derive
这样在Cargo.toml
中的[dependencies]
中就有了相关的信息。
toml
[dependencies]
clap = { version = "4.5.1", features = ["derive"] }
其中-F
表示,我们只需要clap
中的derive
特性。
上述流程中,我们使用的clap
的版本是最新版,有些和大家用过的语法有区别的话,需要大家甄别。
这里多说一嘴,如果对前端开发熟悉的同学是不是感觉到上述流程很熟悉。当我们创建一个前端项目时,是不是会遇到下面的步骤。
shell
npm init
yarn add xx
项目实现
和前端开发类似,当我们把包下载到本地后,我们就需要在对应的入口文件
中引入并执行。在前端开发中我们一般挑选的是项目根目录下的index.js
。而对于Rust
项目来讲,它的入口文件是src/main.rs
。(作为二进制项目(Binary Projects
)而言)
rust
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
有两种类型的宏:
- 声明式宏(
Declarative Macros
):
- 这些是Rust内置的,使用
macro_rules
定义,例如vec!
、println!
等。- 它们主要用于元编程(
metaprogramming
),在编译期执行代码生成。- 过程宏(
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
设定一个默认值,这样在没有提供参数的情况下,也能合理运行。
rust
#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
#[arg(default_value = "front789")]
name: String
}
现在,尝试仅使用 cargo run
而不添加其他任何东西,它应该会打印出 Hello, front789!
。
当然,我们也可以像在f_cli
中一样为参数添加更多的配置,来增强我们的Cli
。
如果想了解更多关于参数配置,可以翻看clap_command-attributes
2. 编写子命令
作为一个功能强大的CLI
,我们有时候需要通过定义一些子命令来让我们的目的更加明确。
如果大家用过我们的f_cli
,那就心领神会了。
下图是我们f_cli
的根据用户提供的参数,默认构建前端项目的命令。
在f_cli
的实现中,我们就用到了子命令的操作。
下面我们来简单实现一个拥有子命令的cli
。在之前代码的基础上,我们只需要将刚才结构体中再新增一个参数 - command
并且其类型为实现sumcommad
trait的枚举
rust
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
所用到的必要信息。
rust
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
中的匹配是穷举式的:必须穷举到最后的可能性来使代码有效
为了节约代码量,我们通过_
占位符来处理其他的逻辑。
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
时,提供给用户的帮助信息。
对应的代码如下:
rust
#[derive(Subcommand, Debug, Clone)]
enum Commands {
Create{
#[arg(
short = 'n',
long="name",
help = "用户信息",
default_value = "front789"
)]
name: String,
#[arg(
short = 'a',
long="address",
help = "地址信息",
requires = "name",
default_value = "山西"
)]
address: String,
},
Replace,
Update,
Delete
}
4. 交互式cli
在上一节中我们通过对CLI
枚举进行改造,让其能够拥有了子命令的功能。其实到这步已经能够获取到cli
中用户输入的值,并且能够进行下一步的操作了。
但是呢,你是一个精益求精的人。见多识广的你突然有一个想法,为什么不能像vite/create/next
一样。在触发对应的构建和更新操作后,有一个人机交互 的过程。然后,用户可以根据自己的喜好来选择我们cli
的内置功能。这样是不是显的更加友好。
像我们的f_cli
就是这种交互流程。用户通过人机交互的方式可以选择内置功能。
那我们就再次用一个简单的例子来介绍一下哇。
安装新的包
首先,我们需要安装几个用于交互的包。
csharp
cargo add anyhow
cargo add dialoguer
cargo add console
随后,就他们就会自动被注入到Cargo.toml
中了。关于anyhow
/dialoguer
/console
我们就不在这里过多介绍了。大家感兴趣可以去对应的官网查找.
现在,我们需要在src/main.rs
中引入相关的功能,同时我们在处理cli
变量的时候,用的是枚举值,所以我们需要引入clap
中针对这类的操作。
diff
use clap::{
+ builder::EnumValueParser,
Parser,
Subcommand,
+ ValueEnum
};
+use dialoguer::{
+ console::Term,
+ theme::ColorfulTheme,
+ Select
+};
+use console::style;
新增枚举信息
前面说过,我们想通过人机交互的方式,在cli
运行过程中让用户自己选择我们内置的功能点。所以,这些内置功能我们可以需要事先设定好。
rust
#[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
中的参数必须是这些内置参数中值。
diff
#[derive(Subcommand, Debug, Clone)]
enum Commands {
Create{
#[arg(
short = 'n',
long="name",
help = "用户信息",
+ value_parser = EnumValueParser::<Name>::new(),
ignore_case = true
)]
+ name: Option<Name>,
#[arg(
short = 'a',
long="address",
help = "地址信息",
requires = "name",
+ value_parser = EnumValueParser::<Address>::new(),
)]
+ address: Option<Address>,
}
}
上面的配置,见名知意,就是从对应的枚举中解析对应的值。
主函数
其实,这步的操作和之前是差不多的,我们还是利用match
对cli.command
进行匹配处理。不过我们这里又进一步的做了容错处理。
- 首先判断是否提供子命令
- 在提供子命令的情况下,再判断是否是
Craete
因为,在进行操作中我们会有错误抛出,所以我们对main
的返回值也做了处理。(anyhow::Result<()>
)
rust
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
中参数的,而此时我们还需要根据参数做进一步的处理。我们把这个逻辑提取到了一个函数中了。
rust
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 是一款跨终端的crate
。 它具有各种很酷的功能,如能够更改背景和文本颜色、操作终端本身和光标,以及捕获键盘和其他事件。
comfy-table
comfy-table 是一个设计用于在终端中创建漂亮表格的 crate
。
以下是其官网的案例。用仅仅几句话就可以实现一个在终端展示的表格。
rust
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}");
}
执行后的效果如下:
vbnet
+----------------------+----------------------+------------------------+
| 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 是一个用于构建终端上交互式提示的 crate。它支持单选、多选、选择日历等功能:
下面的动图是其官网的案例。其中最吸引我的就是那个多选。哈哈。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。