本文目标

该文章将带你使用rust和clap库实现一个创建前端vite项目的脚手架工具

git仓库

本文基于我的开源项目rust-init-cli改编

第一步: 初始化项目

安装

1
2
3
4
cargo init rust-vite-cli
cd rust-vite-cli
# 如果你用vscode, 可以用这个命令打开
code .

依赖安装

1
2
3
cargo add clap
# or
cargo add clap -F derive

第二步: clap读取命令行

Args结构体

创建一个结构体来接收命令行参数

1
2
3
4
5
6
#[derive(Parser)]
#[command()]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
}

关于subcommand, 大概就是可以用在结构体和枚举等类型上

1
2
3
4
5
6
7
8
9
10
11
/// 从源代码找的注释
/// Parse a sub-command into a user-defined enum.
///
/// Implementing this trait lets a parent container delegate subcommand behavior to `Self`.
/// with:
/// - `#[command(subcommand)] field: SubCmd`: Attribute can be used with either struct fields or enum
/// variants that impl `Subcommand`.
/// - `#[command(flatten)] Variant(SubCmd)`: Attribute can only be used with enum variants that impl
/// `Subcommand`.
///
/// **NOTE:** Deriving requires the `derive` feature flag

Commands结构体

我们实现Commands结构体, 结构体的属性会对应命令

1
2
3
4
5
6
// clap可以通过反射宏读取注释, 在help时打印出来
#[derive(Subcommand)]
enum Commands {
/// create-vite
CreateVite
}

接下来, 在main函数中读取

1
2
3
4
5
6
7
8
9
10
fn main() {
let cli = Args::parse();

match cli.command {
Some(Commands::CreateVite) => println!("create vite!"),
None => {
println!("Run with --help to see instructions.")
}
}
}

运行!

1
2
3
cargo run -- create-vite
# or
cargo run --release create-vite
如果运行help命令
如果运行help命令

第三步: 从vite脚手架中复制项目模板

我们并没有采用从网络获取前端项目模板的实现方法, 而是使用将前端项目模板保存到目录, 当创建项目时再复制到目标目录的实现方式,
这种方式当然有坏处, 比如会让项目过大, 要手动将目录放到build后的文件夹等, 但是好处是实现起来比较简单方便

使用vite脚手架创建项目

我们先用vite脚手架创建项目, 然后再将项目复制到我们项目的public文件夹中

1
pnpm create vite

我们只创建vue+js/vue+ts/react+js/react+ts这4个项目

创建示例
创建示例

创建完后, 复制到项目的public文件夹下

目录结构如图
目录结构如图

第四步: 编写需要用到的方法

创建common.rs, 我们在其中存放通用的工具类方法

1
2
3
src/
common.rs
main.rs

将common模块引入main.rs

1
2
// main.rs
mod common;

读取命令行输入的方法

1
2
3
4
5
6
7
8
9
10
11
12
// common.rs

// 读取输入
pub fn read_line() -> String {
// 用来存放用户输入的字符串
let mut input = String::new();
// 读取命令行输入
std::io::stdin()
.read_line(&mut input)
.expect("Stdin not working");
input.trim().to_string()
}

复制文件到指定目录的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// common.rs
use std::{fs, io, path::Path};

// 复制文件夹到指定路径
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
// fs::create_dir_all(&dst)?;
fs::create_dir_all(&dst).expect("创建dst目录失败");

// println!("遍历");
fs::read_dir(&src).expect("找不到src目录");
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}

// println!("copy dir done.");
Ok(())
}

获取运行时目录位置的方法

我原本希望能让程序在运行时找到src下的public文件夹, 但是发现不行, 后面找到可以获取运行时目录的方法,
这样就可以把public文件复制到target下的debug或release里, 让程序不管在什么地方运行都可以找到public里的文件,
并开始复制

1
2
3
4
5
6
7
8
9
src/
main.rs
target/
cargo run运行时的current_exe目录
debug/
public/...
cargo build之后运行exe时的current_exe目录
release/
public/...
1
2
3
4
5
6
7
8
9
10
11
12
13
// common.rs
use std::{fs, path::Path, env::current_exe};

// 获取运行时目录
pub fn current_exe_pkg() -> String {
let pkg_name = env!("CARGO_PKG_NAME");
// println!("{pkg_name}.exe");
let pkg_name = pkg_name.to_string() + ".exe";

// 获取当前目录的路径
let current_exe = current_exe().unwrap();
current_exe.display().to_string().replace(&pkg_name, "")
}

第五步: 创建项目的逻辑

在做完了前面的准备工作后, 终于要开始编写了创建项目的逻辑了. 这一步的内容都放在vite.rs里

1
2
3
src/
main.rs
vite.rs
1
2
// main.rs
mod vite;

保存用户输入的结构体

为了保存用户输入, 以便后续根据输入的选项创建不同的项目, 我们需要一个结构体来保存这些数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vite.rs

// 用户使用什么框架
#[derive(Debug)]
enum FrameworkType {
React,
Vue,
}

// 用户是否使用ts
#[derive(Debug)]
enum VariantType {
Javascript,
Typescript,
}

#[derive(Debug)]
struct UserSelected {
framework_type: FrameworkType,
variant_type: VariantType,
project_name: String, // 项目名称
}

为结构体添加方法

我们需要两个方法, 一个是创建结构体的方法, 一个是创建项目的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// vite.rs
use std::path::Path;
use crate::common::{copy_dir_all, current_exe_pkg, read_line};

impl UserSelected {
fn new(project_name: &str, framework: &str, variant: &str) -> Self {
let framework_type = match framework {
"react" => {
FrameworkType::React
}
"vue" => {
FrameworkType::Vue
}
_ => {
// panic!("No such framework type")
// default
FrameworkType::React
}
};

let variant_type = match variant {
"javascript" => {
VariantType::Javascript
}
"typescript" => {
VariantType::Typescript
}
"js" => {
VariantType::Javascript
}
"ts" => {
VariantType::Typescript
}
_ => {
// panic!("No such variant type")
VariantType::Typescript
}
};

let project_name = if project_name.is_empty() {
"vite-project"
} else { project_name };

UserSelected {
project_name: project_name.to_string(),
framework_type: framework_type,
variant_type: variant_type,
// file_name: "".to_string(),
}
}

// 创建文件
fn init(&self) {
let mut path = "public/vite/".to_string();

match self.framework_type {
FrameworkType::React => {
path += "react";
}
FrameworkType::Vue => {
path += "vue";
}
}

path += "-";

match self.variant_type {
VariantType::Javascript => {
path += "js";
}
VariantType::Typescript => {
path += "ts";
}
}

println!("复制 {}", &(current_exe_pkg() + &path));

// todo: 从网络上下载 或 调用cmd git clone

copy_dir_all(
Path::new(
&(current_exe_pkg() + &path)
),
Path::new(&self.project_name),
).unwrap();
}
}

询问并接收用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// vite.rs

pub fn create_vite_project() {
// project name
println!("your project name? {}", "vite-project");
let project_name = read_line();
let project_name = if project_name.is_empty() {
"vite-project"
} else {
&project_name
};
println!("{}", (&project_name));

// select a framework
// react vue ...
println!("select a framework: (default: react)");
println!("react");
println!("vue");
let mut framework = read_line().to_lowercase();
framework = match framework.as_str() {
"r" => "react".to_string(),
"react" => "react".to_string(),
"v" => "vue".to_string(),
"vue" => "vue".to_string(),
_ => "react".to_string(),
};
println!("{}", (&framework));

// select a variant
// javascript typescript ...
println!("select a variant: (default: ts)");
println!("typescript(ts)");
println!("javascript(js)");
let mut variant = read_line().to_lowercase();
variant = match variant.as_str() {
"ts" => "typescript".to_string(),
"typescript" => "typescript".to_string(),
"js" => "javascript".to_string(),
"javascript" => "javascript".to_string(),
_ => "typescript".to_string(),
};
println!("{}", (&variant));

let user_select = UserSelected::new(&project_name, &framework, &variant);

user_select.init();
}

第六步: 测试

修改main.rs

1
2
3
4
5
6
7
8
9
10
11
12
// main.rs

fn main() {
let cli = Args::parse();

match cli.command {
Some(Commands::CreateVite) => create_vite_project(),
None => {
println!("Run with --help to see instructions.")
}
}
}

测试cargo run

将public目录移动到target/debug目录

测试cargo run时能否成功

1
cargo run -- create-vite

可以看到在项目根目录成功创建了vite-project目录

测试cargo build后的exe

先复制public目录到target/release目录

1
2
cargo build --release
.\target\release\rust-vite-cli.exe create-vite

end

接下来, 你可以将release目录的文件复制到系统的任意目录, 然后将其配置到系统环境变量, 就可以随时随地运行 rust-vite-cli
create-vite 来创建前端项目了.

感谢观看


本站总访问量