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依赖
相关推荐
❆VE❆1 小时前
vue3: directive自定义指令防止重复点击
前端·javascript·vue.js·自定义指令·directive
布兰妮甜1 小时前
Fetch API 与 XMLHttpRequest:深入剖析异步请求的利器
前端·javascript·xmlhttprequest·fetch api
巴巴博一2 小时前
vue-i18n国际化插件安装教程(Vue3篇)
前端·javascript·vue.js·typescript
Merlyn103 小时前
npm : 无法加载文件 E:\ProgramFiles\Nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js
一个打工仔的笔记3 小时前
npm i 失败权限问题
前端·npm·node.js
咔咔库奇3 小时前
【react】进阶教程02
前端·react.js·前端框架
七灵微3 小时前
【前端】react大全一本通
前端·react.js·前端框架
初遇你时动了情3 小时前
react使用react-quill 富文本插件、加入handlers富文本不显示解决办法
前端·javascript·react.js