手工制作头饰,昆明排名seo公司,wordpress 2.8,网站域名商rsup是使用 rust 编写的一个前端 npm 依赖包管理工具#xff0c;可以获取到项目中依赖包的最新版本信息#xff0c;并通过 web 服务的形式提供查看、升级操作等一一系列操作。
在前一篇文章中#xff0c;记录初始的功能设计#xff0c;自己的想法实现过程。在自己的使用过…rsup是使用 rust 编写的一个前端 npm 依赖包管理工具可以获取到项目中依赖包的最新版本信息并通过 web 服务的形式提供查看、升级操作等一一系列操作。
在前一篇文章中记录初始的功能设计自己的想法实现过程。在自己的使用过程功能中也会发现一些存在的问题有一些问题值得记录的再次标记供大家参考。
rsup 工具安装
在上一篇文章中描写的安装rsup工具部分错误因为我本地是 macos 系统
rust 默认执行cargo build构建的是适合 macos 的可执行文件对于 windows、linux 是不能直接用还有一个问题就是rsup-web静态服务资源是不会被编译进工具包的我本地能用也仅仅是我本地有源代码它指向静态资源路径的就是我电脑的绝对地址。
可以采取将静态资源链接打包进二进制文件中。
使用include_bytes!rust 内置的宏将静态文件的内容嵌入到二进制文件使用第三方 crate比如embed-resource或者rust-embed
但是为了方便控制 web 静态资源比如可以单独更新。采取了静态文件和可执行文件分离的方式提供下载器同时下载rsup可执行文件和rsup-webweb 静态资源。针对不同的系统定义默认的下载路径然后通过配置文件读取 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 | shwindows用户需要手动下载安装包解压后执行installer.exe即可并且需要手动配置环境变量。
installer子包下载资源
这是为了解决上述问题新增的一个安装器更友好的交互方式进行安装。也方便后面对下载方式进行更友好的优化。
执行安装器需要使用管理员权限。windows右键以管理员身份执行 exe类 linux 系统需要使用sudo执行。 提供了从 github 或者 gitee 下载资源两种方式。使用第三方库 cratedialoguer进行交互选择。 目前只提供了从github下载资源。 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() - Vecstatic 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)创建了资源文件如果文件已经存在会直接覆盖。
use reqwest::Client;
use tokio::fs;/// 下载文件
///
async fn download_file(client: Client, url: str, output: str) - Result(), Boxdyn 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进行解包提取到指定目录。
use flate2::read::GzDecoder;
use tar::Archive;/// 解压文件
///
/// param url 下载地址
/// param target_dir 保存目录
async fn decompress_file(url: str, target_dir: str) - Result(), Boxdyn 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
use std::io::Write;
use std::{error::Error, fs::OpenOptions};/// 提示用户是否添加命令到环境变量
/// 默认添加
pub fn prompt_add_to_env(path: str) - Result(), Boxdyn Error {// ... 省略部分代码let home_dir std::env::var(HOME)?;// 确定系统使用的shelllet 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进行读写序列化和反序列化。
use std::{error::Error,fs::{self, File},io::{self, Write},path::Path,
};impl Config {/// 读取配置文件///pub async fn read_config() - Result(), Boxdyn 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() - ResultConfig, Boxdyn 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实现。
use once_cell::sync::OnceCell;// 全局共享配置
pub static CONFIG: OnceCellConfig OnceCell::new();impl Config {pub async fn read_config() - Result(), Boxdyn 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 服务的端口号默认8888pkg.npm_registry字段是 npm 依赖源地址默认为国内镜像。通常只建议修改pkg.npm_registry设置源地址方便请求依赖包。
command子包提供命令行交互
提供了新的子包command用于解析命令行参数。统一管理命令行参数方便使用。并且提供了一些方法使用。
在使用rsup命令时可以指定目录使用前端 npm 依赖管理web服务也可以通过输入自命令进行交互式操作。
子命令包含了两部分Config 配置命令Update更新命令。新创建了command子包在主包解析参数时进行逻辑判断如果输入命令则执行对应的子命令未输入子命令则默认执行 web 服务
#[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中的方法并解析命令行参数根据参数执行对应的操作。
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 实现懒加载在首次使用时才去读取配置文件。
pub static CONFIG: LazyRwLockConfig 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 明确导入外部模块
// 引入外部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一次性写入文件通过分批次读取写入并同步更新进度条。
/// 下载文件
///
async fn download_file(client: Client, url: str, output: str) - Result(), Boxdyn 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:.*}是一个路径参数它可以匹配任何路径。
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所以需要根据系统类型使用不同的命令。
// 判断系统如果是windows则使用npm.cmd
let npm_cmd if cfg!(windows) { npm.cmd } else { npm };如果安装时是.exe的话就不需要添加后缀了直接使用即可。比如node
web服务API参数映射处理
在处理API请求参数时通过枚举定义了参数类型然后通过解析匹配到指定的数据结构。
async fn update_pkg(req: web::JsonReqParams,data: web::DataMs,
) - Resultimpl Responder, Error {match *req {ReqParams::UpdatePkg(params) {}err {// ...}
}如果定义的数据结构字段存在重叠某个结构完全包含另一个结构的字段在匹配时就需要将完全包含的结构放在前面否则可能会匹配到错误的结构。
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
pub enum ReqParams {UpdatePkg(UpdateParams),// 删除// 目前接受一个nameRemovePkg(RemoveParams),
}UpdateParams 和RemoveParams存在字段重叠UpdateParams包含了RemoveParams的所有字段要想匹配到UpdateParams需要将RemoveParams放在前面。
最后
部署了rsup文档服务网站rsup|Npm Helper
往期rsup文章
模式匹配、trait 特征行为、必包、宏多线程任务执行并发线程间的数据共享包、模块引用路径开发一个命令行工具rust 命令行工具rsup管理前端npm依赖