rust 前端npm依赖工具rsup升级日志

rsup是使用 rust 编写的一个前端 npm 依赖包管理工具,可以获取到项目中依赖包的最新版本信息,并通过 web 服务的形式提供查看、升级操作等一一系列操作。

在前一篇文章中,记录初始的功能设计,自己的想法实现过程。在自己的使用过程功能中,也会发现一些存在的问题,有一些问题值得记录的再次标记,供大家参考。

rsup 工具安装

在上一篇文章中描写的安装rsup工具部分错误,因为我本地是 macos 系统,

rust 默认执行cargo build构建的是适合 macos 的可执行文件,对于 windows、linux 是不能直接用;还有一个问题,就是rsup-web静态服务资源是不会被编译进工具包的,我本地能用也仅仅是我本地有源代码,它指向静态资源路径的就是我电脑的绝对地址。

可以采取将静态资源链接打包进二进制文件中。

  1. 使用include_bytes!rust 内置的宏将静态文件的内容嵌入到二进制文件
  2. 使用第三方 crate,比如embed-resource或者rust-embed

但是为了方便控制 web 静态资源,比如可以单独更新。采取了静态文件和可执行文件分离的方式,提供下载器同时下载rsup可执行文件和rsup-webweb 静态资源。针对不同的系统定义默认的下载路径,然后通过配置文件读取 web 静态资源提供 web 服务。

rsup工具包包含了配置文件、可执行文件、web 服务文件等。根据不同的系统,提供了三种安装工具包包括 linux、macos、windows。

macos installer

ubuntu instanller

windows instanller

提供了安装脚本文件sh一键下载解压、安装。无需手动配置环境变量。

sh 复制代码
curl -fsSL https://github.com/ngd-b/rsup/blob/main/install.sh | sh

windows用户需要手动下载安装包,解压后执行installer.exe即可,并且需要手动配置环境变量。

installer子包下载资源

这是为了解决上述问题新增的一个安装器,更友好的交互方式进行安装。也方便后面对下载方式进行更友好的优化。

执行安装器需要使用管理员权限。windows右键以管理员身份执行 exe;类 linux 系统需要使用sudo执行。

提供了从 github 或者 gitee 下载资源两种方式。使用第三方库 cratedialoguer进行交互选择。 目前只提供了从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()获取配置数据了。

配置文件中包含的配置项有:

toml 复制代码
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字段指定名称,这样不需要去调整代码里的引用。

toml 复制代码
[package]
utils = { version = "0.1.0", path = "../utils", package = "rsup_utils" }

下载文件时展示进度条

之前的文件下载时,控制台会陷入长时间的阻塞状态,没有任何反应,为了提供更好的交互体验,使用indicatif展示进度条。

要采用进度条,在下载文件时就要使用流式读取文件,以便更新进度条。

增加两个新的lib库,futures-util提供对于stream的扩展函数。

sh 复制代码
cargo add indicatif
cargo add futures-util

修改请求reqwest增加特性支持stream

toml 复制代码
[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),
}

UpdateParamsRemoveParams存在字段重叠,UpdateParams包含了RemoveParams的所有字段,要想匹配到UpdateParams,需要将RemoveParams放在前面。

最后

部署了rsup文档服务网站rsup|Npm Helper

往期rsup文章:

  1. 模式匹配、trait 特征行为、必包、宏
  2. 多线程任务执行
  3. 并发线程间的数据共享
  4. 包、模块,引用路径
  5. 开发一个命令行工具
  6. rust 命令行工具rsup管理前端npm依赖
相关推荐
anOnion2 分钟前
构建无障碍组件之Menu Button pattern
前端·html·交互设计
用户479492835691519 分钟前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
zhangxingchao3 小时前
Kotlin常用的Flow 操作符整理
前端
IT_陈寒5 小时前
React的useState居然还有这种坑?我差点删库跑路
前端·人工智能·后端
Pedantic6 小时前
SwiftUI 手势笔记
前端·后端
橙子家6 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user20585561518136 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州6 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic8 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端