本文目标 该文章将带你使用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 #[derive(Subcommand)] enum Commands { 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
第三步: 从vite脚手架中复制项目模板 我们并没有采用从网络获取前端项目模板的实现方法, 而是使用将前端项目模板保存到目录, 当创建项目时再复制到目标目录的实现方式, 这种方式当然有坏处, 比如会让项目过大, 要手动将目录放到build后的文件夹等, 但是好处是实现起来比较简单方便
使用vite脚手架创建项目 我们先用vite脚手架创建项目, 然后再将项目复制到我们项目的public文件夹中
我们只创建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 3 4 5 6 7 8 9 10 11 12 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 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).expect ("创建dst目录失败" ); 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 ()))?; } } 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 use std::{fs, path::Path, env::current_exe};pub fn current_exe_pkg () -> String { let pkg_name = env! ("CARGO_PKG_NAME" ); 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 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #[derive(Debug)] enum FrameworkType { React, Vue, } #[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 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 } _ => { FrameworkType::React } }; let variant_type = match variant { "javascript" => { VariantType::Javascript } "typescript" => { VariantType::Typescript } "js" => { VariantType::Javascript } "ts" => { VariantType::Typescript } _ => { 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, } } 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)); 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 pub fn create_vite_project () { 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)); 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)); 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 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 来创建前端项目了.
感谢观看