Rust 寫(xiě)腳手架,Clap你應(yīng)該知道的二三事
有感而發(fā)
最近,在和前端小伙伴聊天發(fā)現(xiàn),在2024年,她們都有打算入局Rust學(xué)習(xí)的行列。畢竟前端現(xiàn)在太卷了,框架算是走到「窮途末路」了,無(wú)非就是在原有基礎(chǔ)上修修補(bǔ)補(bǔ)。所有他們想在新的賽道彎道超車(chē)。但是,苦于各種原因,遲遲找不到入門(mén)之法。
確實(shí)如她們所言,Rust由于學(xué)習(xí)路徑比較陡峭,加之和前端語(yǔ)言可以說(shuō)是交集很少。然后,給大家一種學(xué)了馬上就會(huì)忘記的感覺(jué)。并且,由于現(xiàn)在Rust在前端領(lǐng)域的應(yīng)用少之又少。除了字節(jié)跳動(dòng)的Rspack,還有Vivo的Vivo Blue OS(我們?cè)趪?guó)貨之光?用Rust編寫(xiě)的Vivo Blue OS有過(guò)介紹),就很少聽(tīng)說(shuō)其他國(guó)內(nèi)互聯(lián)網(wǎng)公司有相關(guān)的產(chǎn)品和應(yīng)用。
相比國(guó)外,我們的道路還任重而道遠(yuǎn)。像國(guó)外很多耳熟能詳?shù)墓径荚缫巡季諶ust開(kāi)發(fā)。最明顯的就是PhotoShop,它已經(jīng)將只能在桌面運(yùn)行的PS搬入了瀏覽器上。(這個(gè)我們也在之前的師夷長(zhǎng)技以制夷:跟著PS學(xué)前端技術(shù)中有過(guò)相關(guān)介紹)
不過(guò),從最新的招聘網(wǎng)站中搜索Rust相關(guān)崗位,相比前幾年有了很好的改觀。并且很多崗位都和前端相關(guān)。這說(shuō)明,Rust在國(guó)內(nèi)已經(jīng)有了自己的市場(chǎng),也意味著在前端領(lǐng)域也有了一席之地。那么作為職業(yè)前端,不想在紅海中繼續(xù)卷,那勢(shì)必就需要選擇藍(lán)海,方可在千軍萬(wàn)馬之中,殺出一條光明之路。
其實(shí),像我在學(xué)習(xí)Rust也遇到很她們一樣的困境。知識(shí)點(diǎn)看了,也理解了。但是隔斷時(shí)間就會(huì)忘記。周而復(fù)始,就會(huì)對(duì)這門(mén)語(yǔ)言產(chǎn)生一種抗拒感。畢竟,編程也算是一種技術(shù)工種,唯手熟爾。
后面,我就轉(zhuǎn)變思路,那就是動(dòng)手做一些自己認(rèn)為可以解決前端痛點(diǎn)的事。哪怕做這個(gè)事情,其他語(yǔ)言也可以勝任,但是為什么我們不做更進(jìn)一步的嘗試呢?,F(xiàn)階段,Rust在前端賦能的場(chǎng)景,大部分都是提高編譯效率方向。像Rspack[1]/OXC[2]。
既然,大方向已經(jīng)定了,然后就有了我們新的嘗試。從那開(kāi)始,就有了我們下面的嘗試方向
- Rust 開(kāi)發(fā)命令行工具(上)
- Rust 開(kāi)發(fā)命令行工具(中)
- Rust 編譯為 WebAssembly 在前端項(xiàng)目中使用
- Game = Rust + WebAssembly + 瀏覽器
- Rust 賦能前端-開(kāi)發(fā)一款屬于你的前端腳手架
就是基于上面的不斷試錯(cuò)和嘗試,到現(xiàn)在我們已經(jīng)有了像f_cli[3]的npm包,并且已經(jīng)部署到公司私庫(kù),并投入生產(chǎn)開(kāi)發(fā)了。
同時(shí),在最近的項(xiàng)目開(kāi)發(fā)中,還利用Rust編寫(xiě)WebAssembly進(jìn)行前端功能的處理。這塊等有機(jī)會(huì)寫(xiě)一篇相關(guān)的文章。
前言
耽誤了大家?guī)追昼姷臅r(shí)間,在上面絮叨了半天,其實(shí)就是想傳達(dá)一個(gè)思想。Rust其實(shí)不可怕,可怕的是學(xué)了但是你沒(méi)用到工作中。就是想著法都要讓它貼切工作,應(yīng)用于工作。
我們回到正題,其實(shí)Rust賦能前端這個(gè)方向我也在摸索,然后現(xiàn)階段自我感覺(jué)能用到前端項(xiàng)目中的無(wú)非就兩點(diǎn)
- 寫(xiě)一個(gè)腳手架,將一些繁瑣操作工具化
- 寫(xiě)wasm模塊,嵌入到前端邏輯中
大家不管是從哪個(gè)方面獲取Rust知識(shí)點(diǎn),想必大家嘗試的第一個(gè)Rust應(yīng)用就是Cli了。
那我們今天就來(lái)聊聊在Rust開(kāi)發(fā)Cli時(shí)的神器 -clap[4]。
今天,我們只要是講相關(guān)的概念,針對(duì)如何用Rust構(gòu)建一個(gè)CLI,可以翻看我們之前的文章。
好了,天不早了,干點(diǎn)正事哇。
我們能所學(xué)到的知識(shí)點(diǎn)
- 項(xiàng)目初始化
- 編寫(xiě)子命令
- 添加命令標(biāo)志
- 交互式cli
- 其他有用的庫(kù)
1. 項(xiàng)目初始化
首先,讓我們通過(guò)運(yùn)行以下命令來(lái)初始化我們的項(xiàng)目:cargo init clap_demo。隨后我們?cè)倥渲靡幌马?xiàng)目的基礎(chǔ)信息。(description等)
[package]
name = "clap_demo"
version = "0.1.0"
edition = "2021"
description = "front789帶你學(xué)習(xí)clap"
我們可以通過(guò)運(yùn)行以下命令將 clap 添加到我們的程序中:
cargo add clap -F derive
這樣在Cargo.toml中的[dependencies]中就有了相關(guān)的信息。
[dependencies]
clap = { version = "4.5.1", features = ["derive"] }
其中-F表示,我們只需要clap中的derive特性。
圖片
上述流程中,我們使用的clap的版本是最新版,有些和大家用過(guò)的語(yǔ)法有區(qū)別的話(huà),需要大家甄別。
這里多說(shuō)一嘴,如果對(duì)前端開(kāi)發(fā)熟悉的同學(xué)是不是感覺(jué)到上述流程很熟悉。當(dāng)我們創(chuàng)建一個(gè)前端項(xiàng)目時(shí),是不是會(huì)遇到下面的步驟。
npm init
yarn add xx
項(xiàng)目實(shí)現(xiàn)
和前端開(kāi)發(fā)類(lèi)似,當(dāng)我們把包下載到本地后,我們就需要在對(duì)應(yīng)的入口文件中引入并執(zhí)行。在前端開(kāi)發(fā)中我們一般挑選的是項(xiàng)目根目錄下的index.js。而對(duì)于Rust項(xiàng)目來(lái)講,它的入口文件是src/main.rs。(作為二進(jìn)制項(xiàng)目(Binary Projects)而言)
use clap::Parser;
#[derive(Parser)]
#[command(version, about)]
struct Cli {
name: String
}
fn main() {
let cli = Cli::parse();
println!("Hello, {}!", cli.name);
}
我們來(lái)簡(jiǎn)單解釋一下上面的代碼。
在前端開(kāi)發(fā)中我們一般使用import/require進(jìn)行第三方庫(kù)的引入,而在Rust中我們使用use來(lái)導(dǎo)入第三方庫(kù)clap中的Parser trait。也就是說(shuō),通過(guò)use xx我們就可以使用clap中的特定功能。也就是把對(duì)應(yīng)的功能引入到該作用域內(nèi)。
定義了一個(gè)結(jié)構(gòu)體,它使用 clap::Parser 的 derive 宏和command宏,并且只接受一個(gè)參數(shù),即 name。
#[derive(Parser)]/#[command(version, about)]不是Rust內(nèi)置的宏,它們是由clap庫(kù)自定義的過(guò)程宏(procedural macros)。
Rust有兩種類(lèi)型的宏:
- 聲明式宏(Declarative Macros):
這些是Rust內(nèi)置的,使用macro_rules定義,例如vec!、println!等。
它們主要用于元編程(metaprogramming),在編譯期執(zhí)行代碼生成。
- 過(guò)程宏(Procedural Macros):
- 這些是由外部crate定義的,在編譯期間像函數(shù)一樣被調(diào)用。
- 它們可以用來(lái)實(shí)現(xiàn)自定義的代碼生成、lint檢查、trait派生,解析、操作和生成 AST等操作。
#[derive(Parser)]它使用 derive 屬性來(lái)自動(dòng)為 Cli 結(jié)構(gòu)體實(shí)現(xiàn) Parser trait。這意味著 Cli 結(jié)構(gòu)體將獲得解析命令行參數(shù)的功能,而無(wú)需手動(dòng)實(shí)現(xiàn) Parser trait。
圖片
#[command(version, about)]用于配置命令行應(yīng)用程序的元數(shù)據(jù)。
- version: 設(shè)置應(yīng)用程序的版本信息。
- about: 設(shè)置應(yīng)用程序的簡(jiǎn)短描述。這里的信息就是我們?cè)贑argo.toml中配置的description的信息。
最后,我們可以通過(guò)cargo run -- --help來(lái)查看對(duì)應(yīng)的信息。
圖片
總的來(lái)說(shuō),這段代碼使用 clap 庫(kù)定義了一個(gè)命令行應(yīng)用程序,它接受一個(gè)名為 name 的字符串參數(shù)。當(dāng)運(yùn)行這個(gè)應(yīng)用程序時(shí),它會(huì)打印出 "Hello, {name}"。#[derive(Parser)] 和 #[command(...)] 這兩個(gè)屬性分別用于自動(dòng)實(shí)現(xiàn) Parser trait 和配置應(yīng)用程序的元數(shù)據(jù)。
當(dāng)我們加載程序并使用 Cli::parse() 時(shí),它將從 std::env::args 中獲取參數(shù)(這個(gè)概念我們之前在環(huán)境變量:熟悉的陌生人有過(guò)介紹)。
- 如果你嘗試運(yùn)行 cargo run front789,它應(yīng)該會(huì)打印出 Hello, front789!
- 但如果嘗試不添加任何額外值運(yùn)行它,它將打印出幫助菜單。Clap 在默認(rèn)特性中包含了一個(gè)幫助功能,當(dāng)輸入的命令無(wú)效時(shí)會(huì)自動(dòng)顯示幫助菜單。
當(dāng)然,如果想讓我們的程序更加健壯,我們可以給name設(shè)定一個(gè)默認(rèn)值,這樣在沒(méi)有提供參數(shù)的情況下,也能合理運(yùn)行。
#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
#[arg(default_value = "front789")]
name: String
}
現(xiàn)在,嘗試僅使用 cargo run 而不添加其他任何東西,它應(yīng)該會(huì)打印出 Hello, front789!。
圖片
當(dāng)然,我們也可以像在f_cli中一樣為參數(shù)添加更多的配置,來(lái)增強(qiáng)我們的Cli。
圖片
如果想了解更多關(guān)于參數(shù)配置,可以翻看clap_command-attributes[5]
圖片
2. 編寫(xiě)子命令
作為一個(gè)功能強(qiáng)大的CLI,我們有時(shí)候需要通過(guò)定義一些子命令來(lái)讓我們的目的更加明確。
如果大家用過(guò)我們的f_cli,那就心領(lǐng)神會(huì)了。
下圖是我們f_cli的根據(jù)用戶(hù)提供的參數(shù),默認(rèn)構(gòu)建前端項(xiàng)目的命令。
圖片
在f_cli的實(shí)現(xiàn)中,我們就用到了子命令的操作。
圖片
下面我們來(lái)簡(jiǎn)單實(shí)現(xiàn)一個(gè)擁有子命令的cli。在之前代碼的基礎(chǔ)上,我們只需要將剛才結(jié)構(gòu)體中再新增一個(gè)參數(shù) - command并且其類(lèi)型為實(shí)現(xiàn)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);
}
這樣,我們就在上面的基礎(chǔ)上擁有了一組子命令(CRUD)。這樣我們就可以在cli中調(diào)用對(duì)應(yīng)的子命令然后執(zhí)行對(duì)應(yīng)的操作了。
圖片
3. 添加命令標(biāo)志
我們可以繼續(xù)豐富我們子命令。上面的我們不是通過(guò)一個(gè)枚舉Commands夠了一個(gè)組件命令(Create/Replace/Update/Delete)嗎。
有時(shí)候,在某一個(gè)子命令下,還需要收集更多的用戶(hù)選擇。那么我們就可以將枚舉中的值關(guān)聯(lián)成一個(gè)「匿名結(jié)構(gòu)體」。這樣,我們就可以針對(duì)某個(gè)子命令做更深的操作了。
還是舉我們之前的f_cli的例子,在我們通過(guò)f_cli create xxx構(gòu)建項(xiàng)目時(shí),我們可以通過(guò)-x來(lái)像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
}
這樣我們就對(duì)Create進(jìn)一步處理,并且在create的時(shí)候,它會(huì)從命令行中尋找對(duì)應(yīng)的name/address信息,并且收集到clap實(shí)例中。
隨后,我們就可以在主函數(shù)中通過(guò)match來(lái)匹配枚舉信息,然后執(zhí)行相對(duì)應(yīng)的操作。
Rust 中的匹配是窮舉式的:必須窮舉到最后的可能性來(lái)使代碼有效
為了節(jié)約代碼量,我們通過(guò)_占位符來(lái)處理其他的邏輯。
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Create{name,address} => {
println!("我是{},來(lái)自:{}", name,address);
},
_=>(),
}
}
當(dāng)我們運(yùn)行cargo run create時(shí),由于我們提供了默認(rèn)值,在控制臺(tái)就會(huì)輸出對(duì)應(yīng)的信息。當(dāng)然,我們也可以通過(guò)-- name xx -- address xx來(lái)進(jìn)行操作。
有人會(huì)覺(jué)得輸入較長(zhǎng)的子命令不是很友好,我們可以通過(guò)short = 'n'來(lái)為子命令提供一個(gè)別名。同時(shí)我們還可以通過(guò)help="xxx"設(shè)置對(duì)應(yīng)在--help時(shí),提供給用戶(hù)的幫助信息。
圖片
對(duì)應(yīng)的代碼如下:
#[derive(Subcommand, Debug, Clone)]
enum Commands {
Create{
#[arg(
short = 'n',
lnotallow="name",
help = "用戶(hù)信息",
default_value = "front789"
)]
name: String,
#[arg(
short = 'a',
lnotallow="address",
help = "地址信息",
requires = "name",
default_value = "山西"
)]
address: String,
},
Replace,
Update,
Delete
}
4. 交互式cli
在上一節(jié)中我們通過(guò)對(duì)CLI枚舉進(jìn)行改造,讓其能夠擁有了子命令的功能。其實(shí)到這步已經(jīng)能夠獲取到cli中用戶(hù)輸入的值,并且能夠進(jìn)行下一步的操作了。
但是呢,你是一個(gè)精益求精的人。見(jiàn)多識(shí)廣的你突然有一個(gè)想法,為什么不能像vite/create/next一樣。在觸發(fā)對(duì)應(yīng)的構(gòu)建和更新操作后,有一個(gè)「人機(jī)交互」的過(guò)程。然后,用戶(hù)可以根據(jù)自己的喜好來(lái)選擇我們cli的內(nèi)置功能。這樣是不是顯的更加友好。
像我們的f_cli就是這種交互流程。用戶(hù)通過(guò)人機(jī)交互的方式可以選擇內(nèi)置功能。
圖片
f_cli 選擇UI庫(kù)
那我們就再次用一個(gè)簡(jiǎn)單的例子來(lái)介紹一下哇。
安裝新的包
首先,我們需要安裝幾個(gè)用于交互的包。
cargo add anyhow
cargo add dialoguer
cargo add console
隨后,就他們就會(huì)自動(dòng)被注入到Cargo.toml中了。關(guān)于anyhow/dialoguer/console我們就不在這里過(guò)多介紹了。大家感興趣可以去對(duì)應(yīng)的官網(wǎng)查找.
- dialoguer[6]
- console[7]
- anyhow[8]
現(xiàn)在,我們需要在src/main.rs中引入相關(guān)的功能,同時(shí)我們?cè)谔幚韈li變量的時(shí)候,用的是枚舉值,所以我們需要引入clap中針對(duì)這類(lèi)的操作。
use clap::{
+ builder::EnumValueParser,
Parser,
Subcommand,
+ ValueEnum
};
+use dialoguer::{
+ console::Term,
+ theme::ColorfulTheme,
+ Select
+};
+use console::style;
新增枚舉信息
前面說(shuō)過(guò),我們想通過(guò)人機(jī)交互的方式,在cli運(yùn)行過(guò)程中讓用戶(hù)自己選擇我們內(nèi)置的功能點(diǎn)。所以,這些內(nèi)置功能我們可以需要事先設(shè)定好。
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Name {
N1,
N2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Address {
A1,
A2
}
處理結(jié)構(gòu)體中參數(shù)的默認(rèn)值
既然,已經(jīng)有了對(duì)應(yīng)的默認(rèn)值,那么我們就需要限制我們cli中的參數(shù)必須是這些內(nèi)置參數(shù)中值。
#[derive(Subcommand, Debug, Clone)]
enum Commands {
Create{
#[arg(
short = 'n',
lnotallow="name",
help = "用戶(hù)信息",
+ 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>,
}
}
上面的配置,見(jiàn)名知意,就是從對(duì)應(yīng)的枚舉中解析對(duì)應(yīng)的值。
主函數(shù)
其實(shí),這步的操作和之前是差不多的,我們還是利用match對(duì)cli.command進(jìn)行匹配處理。不過(guò)我們這里又進(jìn)一步的做了容錯(cuò)處理。
- 首先判斷是否提供子命令
- 在提供子命令的情況下,再判斷是否是Craete
因?yàn)?,在進(jìn)行操作中我們會(huì)有錯(cuò)誤拋出,所以我們對(duì)main的返回值也做了處理。(anyhow::Result<()>)
fn main() ->anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
// - 如果有子命令,則根據(jù)子命令執(zhí)行相應(yīng)的邏輯;
Some(command) => {
match command {
Commands::Create {
name,
address,
} =>
operation_params(
name,
address
)?,
}
},
_ => panic!("Fatal: cli為提供參數(shù),退出處理."),
}
Ok(())
}
operation_params
在main中我們通過(guò)match是可以獲取到cli中參數(shù)的,而此時(shí)我們還需要根據(jù)參數(shù)做進(jìn)一步的處理。我們把這個(gè)邏輯提取到了一個(gè)函數(shù)中了。
fn operation_params (
name: Option<Name>,
address: Option<Address>
) -> anyhow::Result<()> {
let n = match name {
Some(na) => na,
None => {
multiselect_msg("選擇一個(gè)姓名:");
message("使用上/下箭頭進(jìn)行選擇,使用空格或回車(chē)鍵確認(rèn)。");
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: 用戶(hù)信息制定錯(cuò)誤."),
}
}
};
let a = match address {
Some(na) => na,
None => {
multiselect_msg("選擇一個(gè)地址:");
message("使用上/下箭頭進(jìn)行選擇,使用空格或回車(chē)鍵確認(rèn)。");
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: 地址信息制定錯(cuò)誤."),
}
}
};
println!("name:{:?},地址:{:?}",n,a);
Ok(())
}
其實(shí)上面的邏輯也是比較簡(jiǎn)單明了的。 我們接收cli中的參數(shù)name/address。因?yàn)樗麄兌际敲杜e類(lèi)型,所以我們繼續(xù)用match進(jìn)行對(duì)應(yīng)值的匹配。
雖然,我們對(duì)兩個(gè)枚舉值都做了處理,但是他們的邏輯都是相同的。
上面的邏輯就是當(dāng)我們運(yùn)行子命令時(shí)候
- 當(dāng)提供對(duì)應(yīng)的參數(shù)的話(huà),那就原封不動(dòng)的返回對(duì)應(yīng)的值
- 當(dāng)沒(méi)有提供對(duì)應(yīng)的參數(shù)的話(huà),我們就調(diào)用dialoguer::Select進(jìn)行我們預(yù)設(shè)值的選擇。
圖片
這樣,不管我們上面那種情況,我們最后都可以拿到對(duì)應(yīng)的值。這樣我們方便我們后期進(jìn)行其他操作。
5. 其他有用的庫(kù)
上面我們通過(guò)幾個(gè)例子,講了很多clap的應(yīng)用例子,其中我們還配合dialoguer進(jìn)行人機(jī)交互的處理。如果我們想實(shí)現(xiàn)功能更加強(qiáng)大的cli我們還可以借助其他的工具。下面我們就來(lái)簡(jiǎn)單介紹幾種。
Crossterm
crossterm[9] 是一款跨終端的crate。 它具有各種很酷的功能,如能夠更改背景和文本顏色、操作終端本身和光標(biāo),以及捕獲鍵盤(pán)和其他事件。
圖片
comfy-table
comfy-table[10] 是一個(gè)設(shè)計(jì)用于在終端中創(chuàng)建漂亮表格的 crate。
以下是其官網(wǎng)的案例。用僅僅幾句話(huà)就可以實(shí)現(xiàn)一個(gè)在終端展示的表格。
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}");
}
執(zhí)行后的效果如下:
+----------------------+----------------------+------------------------+
| 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] 是一個(gè)用于構(gòu)建終端上交互式提示的 crate。它支持單選、多選、選擇日歷等功能:
下面的動(dòng)圖是其官網(wǎng)的案例。其中最吸引我的就是那個(gè)多選。哈哈。
圖片