rsup
是使用 rust 编写的一个前端 npm 依赖包管理工具,可以获取到项目中依赖包的最新版本信息,并通过 web 服务的形式提供查看、升级操作等一一系列操作。
在前一篇文章中,记录初始的功能设计,自己的想法实现过程。在自己的使用过程功能中,也会发现一些存在的问题,有一些问题值得记录的再次标记,供大家参考。
rsup 工具安装
在上一篇文章中描写的安装rsup
工具部分错误,因为我本地是 macos 系统,
rust 默认执行cargo build
构建的是适合 macos 的可执行文件,对于 windows、linux 是不能直接用;还有一个问题,就是rsup-web
静态服务资源是不会被编译进工具包的,我本地能用也仅仅是我本地有源代码,它指向静态资源路径的就是我电脑的绝对地址。
可以采取将静态资源链接打包进二进制文件中。
但是为了方便控制 web 静态资源,比如可以单独更新。采取了静态文件和可执行文件分离的方式,提供下载器同时下载rsup
可执行文件和rsup-web
web 静态资源。针对不同的系统定义默认的下载路径,然后通过配置文件读取 web 静态资源提供 web 服务。
rsup
工具包包含了配置文件、可执行文件、web 服务文件等。根据不同的系统,提供了三种安装工具包包括 linux、macos、windows。
macos installer
ubuntu instanller
windows instanller
提供了安装脚本文件sh
一键下载解压、安装。无需手动配置环境变量。
curl -fsSL https://github.com/ngd-b/rsup/blob/main/install.sh | sh
windows
用户需要手动下载安装包,解压后执行installer.exe
即可,并且需要手动配置环境变量。
installer
子包下载资源
这是为了解决上述问题新增的一个安装器,更友好的交互方式进行安装。也方便后面对下载方式进行更友好的优化。
执行安装器需要使用管理员权限。windows
右键以管理员身份执行 exe;类 linux 系统需要使用sudo
执行。
提供了从 github 或者 gitee 下载资源两种方式。使用第三方库 crate 目前只提供了从dialoguer
进行交互选择。github
下载资源。
rust">use clap::{Parser, ValueEnum};
use dialoguer::{theme::ColorfulTheme, Select};
#[derive(Parser, Debug, Clone, ValueEnum)]
pub enum Origin {
Github,
Gitee,
}
impl Origin {
// ...
pub fn as_str(&self) -> &'static str {
match self {
Origin::Github => "github",
Origin::Gitee => "gitee",
}
}
/// 将枚举
pub fn choices() -> Vec<&'static str> {
vec![Origin::Github.as_str(), Origin::Gitee.as_str()]
}
}
/// 提示用户选择下载源
/// @return 下载源
pub fn prompt_origin() -> Origin {
let select = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Please select download source...")
.default(0)
.items(Origin::choices().as_slice())
.interact()
.unwrap();
match select {
0 => Origin::Github,
1 => Origin::Gitee,
_ => unreachable!(),
}
}
使用reqwest
下载资源,并将资源保存到默认路径。文件路径output
的目录必须要提前创建,而fs::File::create(output)
创建了资源文件,如果文件已经存在会直接覆盖。
rust">use reqwest::Client;
use tokio::fs;
/// 下载文件
///
async fn download_file(client: &Client, url: &str, output: &str) -> Result<(), Box<dyn Error>> {
// 下载地址
let res = client.get(url).send().await?;
if res.status().is_success() {
// 下载成功
// 保存文件到指定目录
// 文件路径
let mut file = fs::File::create(output).await?;
// 保存文件
let bytes = res.bytes().await?;
file.write_all(&bytes).await?;
Ok(())
} else {
let error_message = format!("Request failed with status code: {}", res.status());
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
error_message,
)))
}
}
文件下载完成后需要解压。所有的资源文件都是.tar.gz
格式的,使用flate2
解压文件,并且需要使用tar
进行解包提取到指定目录。
rust">use flate2::read::GzDecoder;
use tar::Archive;
/// 解压文件
///
/// @param url 下载地址
/// @param target_dir 保存目录
async fn decompress_file(url: &str, target_dir: &str) -> Result<(), Box<dyn Error>> {
let tar_gz = File::open(url)?;
let decomppress = GzDecoder::new(tar_gz);
let mut archive = Archive::new(decomppress);
// 处理解压目录,不存在则创建目录
if !Path::new(target_dir).exists() {
fs::create_dir_all(target_dir).await?;
}
archive.unpack(target_dir)?;
Ok(())
}
所需要的资源下载解压完成后,现在默认目录下(类 linux 系统下是/opt/rsup
)有三个文件
rsup
可执行文件config.toml
配置文件web
web 静态资源
可以直接去执行rsup
可执行文件。但是当前目录下没有package.json
文件,我们可以指定参数--dir
去访问指定目录下的package.json
。为了方便命令的使用,安装时经将命令添加到环境变量中。
针对不同的操作系统,环境变量的配置文件不一样。windows
系统需要用户自行配置,macos
系统下是.zshrc
;其他类系统默认为.bashrc
rust">use std::io::Write;
use std::{error::Error, fs::OpenOptions};
/// 提示用户是否添加命令到环境变量
/// 默认添加
pub fn prompt_add_to_env(path: &str) -> Result<(), Box<dyn Error>> {
// ... 省略部分代码
let home_dir = std::env::var("HOME")?;
// 确定系统使用的shell
let shell_file_name = match os {
"macos" => ".zshrc",
_ => ".bashrc",
};
// 环境变量配置目录
let shell_config_path = format!("{}/{}", home_dir, shell_file_name);
// 写入配置
let mut file = OpenOptions::new().append(true).open(shell_config_path)?;
writeln!(file, "\n# Add rsup to PATH\nexport PATH=\"{}:$PATH\"", path)?;
}
写入配置文件后,需要重新加载配置文件。执行source ~/.zshrc
或者.bashrc
,这样就可以全局使用rsup
命令了。
config
子包管理配置文件
配置文件的读取和写入使用config
子包,提供配置文件读写操作。installer
安装时会默认生成配置文件,在rsup
执行时会读取配置文件。为了方便配置文件管理,新增config
子包。
使用了 crate toml
对配置文件config.toml
进行读写序列化和反序列化。
rust">use std::{
error::Error,
fs::{self, File},
io::{self, Write},
path::Path,
};
impl Config {
/// 读取配置文件
///
pub async fn read_config() -> Result<(), Box<dyn Error>> {
// 读取配置文件
let config_dir = Config::get_url();
let config_file_dir = format!("{}/config.toml", config_dir);
// ... 省略部分代码
let config_content = fs::read_to_string(&config_file_dir)?;
let config: Config = toml::from_str(&config_content)?;
Ok(())
}
/// 写入配置文件
pub async fn write_config() -> Result<Config, Box<dyn Error>> {
let config_dir = Config::get_url();
// ... 省略部分代码
// 配置文件
let config_url = format!("{}/config.toml", config_dir);
let mut file = File::create(config_url.clone())?;
let mut config = Config::default();
// 配置文件路径
config.dir = config_dir.clone();
// 静态文件目录
config.web.static_dir = format!("{}/web", &config_dir);
let config_content = toml::to_string(&config)?;
file.write_all(config_content.as_bytes())?;
Ok(config)
}
}
在主入口main
中执行读取配置文件,然后可以在各个子包中读取。为了方便使用,在config
中提供了静态全局变量CONFIG
,使用了第三方 crateonce_cell
实现。
rust">use once_cell::sync::OnceCell;
// 全局共享配置
pub static CONFIG: OnceCell<Config> = OnceCell::new();
impl Config {
pub async fn read_config() -> Result<(), Box<dyn Error>> {
// ... 省略部分代码
// 保存配置数据共享
CONFIG.set(config).unwrap();
}
/// 父级包获取配置
pub fn get_config() -> &'static Config {
CONFIG.get().unwrap()
}
}
这样就可以在其他子包中直接使用config::Config::get_config()
获取配置数据了。
配置文件中包含的配置项有:
name = "rsup"
version = "0.3.0"
dir = "/opt/rsup"
[web]
port = 8888
static_dir = "/opt/rsup/web"
[pkg]
npm_registry = "https://registry.npmmirror.com"
配置文件中的dir
字段是安装目录,默认安装在/opt/rsup
;web.port
字段是 web 服务的端口号,默认8888;pkg.npm_registry
字段是 npm 依赖源地址,默认为国内镜像。通常只建议修改pkg.npm_registry
设置源地址,方便请求依赖包。
command
子包提供命令行交互
提供了新的子包command
,用于解析命令行参数。统一管理命令行参数,方便使用。并且提供了一些方法使用。
在使用rsup
命令时,可以指定目录使用前端 npm 依赖管理web服务;也可以通过输入自命令进行交互式操作。
子命令包含了两部分:Config
配置命令;Update
更新命令。新创建了command
子包,在主包解析参数时进行逻辑判断,如果输入命令则执行对应的子命令;未输入子命令则默认执行 web 服务;
rust">#[tokio::main]
async fn main() {
let args = Cli::parse();
match args.command {
Some(Commands::Config { .. }) | Some(Commands::Update { .. }) => {
run().await;
}
_ => {
let package = Package::new();
// 默认启动pkg解析服务
let package_clone = package.clone();
task::spawn(async move {
pkg::run(args.pkg_args, package_clone).await;
});
web::run(package.clone()).await;
}
}
}
执行run()
方法调用了子包command
中的方法,并解析命令行参数,根据参数执行对应的操作。
rust">pub async fn run() {
let cli = Commands::parse();
let _ = match cli {
Commands::Config { config } => match config {
ConfigOptions::List => ConfigOptions::list_config().await,
ConfigOptions::Set { key, value } => ConfigOptions::set_config_value(&key, value).await,
ConfigOptions::Get { key } => ConfigOptions::get_config_value(&key).await,
ConfigOptions::Delete => todo!(),
},
Commands::Update { update } => {
// 获取最新的包地址
let (rsup_url, rsup_web_url) = utils::get_pkg_url(None);
// 获取命令安装目录
let config = external_config::Config::get_config().await;
match update {
UpdateOptions::Rsup => UpdateOptions::rsup_update(rsup_url, &config.dir).await,
UpdateOptions::Web => {
UpdateOptions::rsup_web_update(rsup_web_url, &config.dir).await
}
}
}
};
}
Config
配置命令
Config
配置命令用来管理配置文件,提供交互式操作。我们之前在installer
安装时,默认生成配置文件。通过config
命令可以查看、修改、删除配置项。
config list
可以展示出配置文件config.toml
,在我们安装好rsup
命令后,执行rsup config list
可以看到配置文件内容。
config set key value
可以修改配置文件中的值,例如:rsup config set web.port 9999
修改web服务端口号。
对于配置文件的访问、修改,主要是使用了子包config
中的方法。为了方便修改,对于子包config
的实现进行了调整,文章上面提到的实现为第一版实现,可以做对比差异。
初始实现的需要在core
主入口中调用一次读取配置文件,然后在其他子包中通过config::Config::get_config()
获取。这种方式在config
子包中不方便直接修改配置文件,需要重新读取。
使用tokio::sync::RwLock
实现读写锁,它是线程安全的。使用once_cell::sync::Lazy
实现懒加载,在首次使用时才去读取配置文件。
rust">pub static CONFIG: Lazy<RwLock<Config>> = Lazy::new(|| {
// 这里调用初始化
let config = Config::read_config().unwrap();
RwLock::new(config)
});
在使用set
设置配置项时,需要管理员权限,配置更新后会同步更新config.toml
配置文件
Update
更新命令
rsup
工具包含自身和web
服务两部分,提供了更新命令,可以更新rsup
工具和web
服务。
通过rsup update rsup
更新工具,通过rsup update web
更新web服务。
utils
子包提供公共方法
为了方便子包之间的共用方法的服用,提供了utils
子包,提供了一些公共方法。
遇到的问题
记录一下遇到的问题,方便后续查阅。
在使用本地config
模块与配置文件config
发生命名冲突
通过extern
明确导入外部模块
rust">// 引入外部crate
extern crate config as external_config;
发布包到crates-io
时名称重复,本地引用修改名称
本地开发时使用的名称utils
,为了发布到crates-io
时,需要修改名称rsup_utils
,避免名称重复。然后本地引用时使用package
字段指定名称,这样不需要去调整代码里的引用。
[package]
utils = { version = "0.1.0", path = "../utils", package = "rsup_utils" }
下载文件时展示进度条
之前的文件下载时,控制台会陷入长时间的阻塞状态,没有任何反应,为了提供更好的交互体验,使用indicatif
展示进度条。
要采用进度条,在下载文件时就要使用流式读取文件,以便更新进度条。
增加两个新的lib库,futures-util
提供对于stream
的扩展函数。
cargo add indicatif
cargo add futures-util
修改请求reqwest
增加特性支持stream
[dependencies]
reqwest = { version = "0.12.9", features = ["stream"] }
修改之前的下载函数download_file
,不再使用write_all
一次性写入文件,通过分批次读取写入,并同步更新进度条。
rust">/// 下载文件
///
async fn download_file(client: &Client, url: &str, output: &str) -> Result<(), Box<dyn Error>> {
// 下载地址
let res = client.get(url).send().await?;
if res.status().is_success() {
// 获取文件大小
let content_size = res.content_length().ok_or("无法获取文件大小")?;
// 下载成功
// 保存文件到指定目录
// 文件路径
let mut file = fs::File::create(output).await?;
// 创建进度条
let pb = ProgressBar::new(content_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{msg} [{elapsed_precise}] {bar:80} {percent}%")?
.progress_chars("##-"),
);
// 创建流式响应体
let mut downloaded = 0;
let mut stream = res.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item?;
file.write_all(&chunk).await?;
let len = chunk.len() as u64;
downloaded += len;
pb.set_position(downloaded);
}
pb.finish_with_message("下载完成");
// 保存文件
// let bytes = res.bytes().await?;
// file.write_all(&bytes).await?;
Ok(())
} else {
let error_message = format!("Request failed with status code: {}", res.status());
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
error_message,
)))
}
}
解决web
服务自动后刷新页面加载不到的问题
这是典型的SPA的问题,由于我们使用的是history路由模式,路由由前端控制。我们刷新页面比如http://localhost:8888/home
时,会请求http://localhost:8888/home
,但是web
服务没有这个路由,所以会返回404
,导致刷新页面加载不到。
为了处理这个问题,需要增加通配符路由处理跳转route("/{tail:.*}", web::get().to(index))
,{tail:.*}
是一个路径参数,它可以匹配任何路径。
rust">let server = HttpServer::new(move || {
//...
App::new()
.app_data(web::Data::new(ms.clone()))
.route("/", web::get().to(index))
.wrap(cors)
.service(web::scope("/api").configure(api::api_config))
.service(
Files::new("/static", format!("{}/static/", &static_file_path)).prefer_utf8(true),
)
.route("/ws", web::get().to(socket_index))
// SPA fallback route
.route("/{tail:.*}", web::get().to(index))
})
windos
系统下不同的命令执行名称
在windows
系统下,我们执行npm -v
时,实际内部执行的是npm.cmd -v
,而在mac系统下,执行npm -v
时,实际内部执行的是npm -v
,所以需要根据系统类型,使用不同的命令。
rust">// 判断系统,如果是windows,则使用npm.cmd
let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
如果安装时是.exe
的话就不需要添加后缀了,直接使用即可。比如node
web
服务API参数映射处理
在处理API请求参数时,通过枚举定义了参数类型,然后通过解析匹配到指定的数据结构。
rust">async fn update_pkg(
req: web::Json<ReqParams>,
data: web::Data<Ms>,
) -> Result<impl Responder, Error> {
match &*req {
ReqParams::UpdatePkg(params) => {
}
err => {
// ...
}
}
如果定义的数据结构字段存在重叠,某个结构完全包含另一个结构的字段,在匹配时就需要将完全包含的结构放在前面,否则可能会匹配到错误的结构。
rust">#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
pub enum ReqParams {
UpdatePkg(UpdateParams),
// 删除
// 目前接受一个name
RemovePkg(RemoveParams),
}
UpdateParams
和RemoveParams
存在字段重叠,UpdateParams
包含了RemoveParams
的所有字段,要想匹配到UpdateParams
,需要将RemoveParams
放在前面。
最后
部署了rsup
文档服务网站rsup|Npm Helper
往期rsup
文章: