rust 命令行工具rsup管理前端npm依赖

news2024/11/13 16:11:01

学习了一年的 rust 了,但是不知道用来做些什么,也没能赋能到工作中,现在前端基建都已经开始全面进入 rust 领域了,rust 的前端生态是越来越好。但是自己奈何水平不够,想贡献点什么,无从下手。

遂想自己捣鼓个什么东西,可以帮助到日常工作的。

记录一下在完成功能时遇到的一些问题,以及是怎么解决的。

解决的需求

公司有很多项目,都是依赖公司技术部门的一个框架,虽然说不行,但还是要用,里面有一些基础业务功能,也是避免了重复。公司框架的依赖常常更新不及时,依赖安装经常会报错,比如经常要使用--legacy-peer-deps,对于新项目使用框架创建项目后还需要二次调整。

但随着时间的推移,项目越来越多,依赖也会慢慢变成旧版本,一个一个查看升级实属是一个体力活。那就写个脚本吧,它能做什么:

  1. 解析package.json 文件,获取到dependencies/devDependencies依赖列表。
  2. 远程请求依赖包,获取依赖的版本信息,并从中过滤出当前版本的最新版本信息
  3. 通过 web 页面展示这些数据,并通过 tag 表示版本。
  4. 选择性升级或者批量升级依赖到最新版本。

使用

  1. 直接下载并安装

github 下载地址

gitee 下载地址

解压文件,使用解压工具解压,或者使用命令行工具解压,得到一个可以执行文件。

$> tar -xzvf rsup.tar.gz

在终端将执行文件移动到/usr/local/bin目录下,使得rsup命令全局可用。或者直接在解压后的文件夹中执行。

$> sudo mv rsup /usr/local/bin/
  1. 使用安装脚本安装

在终端执行命令

# github
$> curl -fsSL https://github.com/ngd-b/rsup/raw/main/install.sh | bash

# gitee
$> curl -fsSL https://gitee.com/hboot/rsup/raw/master/install.sh | bash

提示安装成功后,就可以在终端执行rsup命令。必须是当前目录下存在package.json文件或者通过参数设置绝对路径rsup -d $path

执行效果,展示项目下的依赖版本、最新版本;可以点击操作进行升级。
请添加图片描述

如何设计

命令行工具分为三个功能包:

  • core 主程序执行入口,根据参数执行不同的功能
  • pkg 解析指定目录下的package.json文件,获取依赖列表并远程请求依赖包信息,过滤出最新版本信息。
  • web 提供 web 页面展示数据,并提供升级功能。通过 websocket 实现数据交互。

为了各个子包之间的工作互不影响我们开启线程去处理异步任务,并通过channel实现线程之间的信息交互。

使用tokio 实现异步任务,就不开启线程了,因为我们每个功能都是异步任务,开启线程没必要。还需要去处理线程调用异步任务时需要异步运行时呢。

为了保持主线程的活跃性,我们将 web 服务运行在主线程中,通过tokio::task::spawn 开启异步任务,当然了我们是不需要任务阻塞主线程的。

使用到的主要 crate 以及其能力,具体可查看文档。包括但不限于:

  • clap 解析命令行参数
  • reqwest 处理网络请求
  • serde \ serde_derive \ serde_json 结构体数据序列化与反序列化
  • tokio 异步运行时
  • actix-web 提供 web 服务
  • actix-ws 实现 websocket 通信
  • actix_cors actix-web 中间键,设置 web 服务的响应头信息,设置跨域
  • actix_files 提供静态文件服务
  • futures_util 处理异步任务扩展库

遇到的问题

大致包括数据共享、传递。websocket 数据通信。结构体定义、数据序列化与反序列化。

使用clap 处理命令行参数

我们有多个子命令,在执行命令时通过指定某个子命令来执行不同的功能,比如cargo run --bin core pkg,就是指定执行pkg子命令。

可以通过clap提供的#[command(subcommand)] 属性来定义子命令。

#[derive(Parser, Debug)]
#[command(name = "rsup", author, version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands
}
#[derive(Subcommand, Debug)]
enum Commands {
    Pkg(pkg::Args),
}

然后在解析参数时,通过match 匹配子命令,执行不同的功能。并且针对于子命令的参数也可以被解析。

#[tokio::main]
async fn main() {
    let args = Cli::parse();

    // ... 省略其他代码

    match args.command {
        Commands::Pkg(args) => {
            let data_clone = data.clone();
            let tx_clone = tx.clone();
            task::spawn(async move {
                if let Err(e) = pkg::run(args, data_clone, tx_clone).await {
                    println!("Error run subcommand pgk  {}", e);
                };
            });
        }
    }
}

后来我需要默认执行该子命令pkg,不需要在运行时指定子命令,只需要在运行时指定参数即可。但是每一个子包如pkg都有自己需要接收的参数,但是我们执行的是core包,所以需要处理合并各个子包的参数。

通过clap提供的#[flatten] 属性,去合并各个子命令的参数。

#[derive(Parser, Debug)]
#[command(name = "rsup", author, version, about)]
struct Cli {
    #[clap(flatten)]
    pkg_args: pkg::Args,
}

#[tokio::main]
async fn main() {
    let args = Cli::parse();

    // ... 省略其他代码

    // 默认启动pkg解析服务
    let data_clone = data.clone();
    let tx_clone = tx.clone();
    task::spawn(async move {
        if let Err(e) = pkg::run(args.pkg_args, data_clone, tx_clone).await {
            println!("Error run subcommand pgk  {}", e);
        };
    });
}

使用Arc<Mutex<>>共享数据

在文章中rust 自动化测试、迭代器与闭包、智能指针、无畏并发 中提到过使用Arc<Mutex<>>共享数据。

文章里使用的是std::sync::Mutex也就是标准库提供的Mutext<>, 它是同步阻塞的,在阻塞式代码中更加高效,而我们的项目需要异步非阻塞,所以我们需要使用tokio::sync::Mutex来更好的处理异步代码。

我们在主包中定义共享数据,然后克隆引用传递给各个子包。

#[tokio::main]
async fn main() {
    // ... 省略其他代码

    let data: Arc<Mutex<pkg::Pkg>> = Arc::new(Mutex::new(pkg::Pkg::new()));
}

数据结构是在子包pkg中定义的,因为所有的数据操作、包括更新都在pkg中完成。通过clone()方法将数据传递给各个子包。我们的web子包主要是将数据传递给页面。

mpsc::channel创建通信通道

在解析pkg获取到package.json文件后,数据就需要去更新,并且需要通知web子包数据变更要向前端页面发送数据了。

在上面的文章 👆 也给出了如何在线程间传递信息。里面使用的std::sync::mpsc同样的,我们的任务是 I/O 密集型,使用异步编程更高效。我们采用了tokio::sync::mpsc

tokio::sync::mpsc 和标准库不同的是需要设置容量,防止数据溢出。通信通道是多生产者单消费者,web 服务就是消费者,它接收到数据更新消息后就像前端发送数据,而pkg子包就是生产者,负责更新数据。

#[tokio::main]
async fn main() {
    // ... 省略其他代码

   let (tx, rx) = channel(100);

    // web 服务 将rx 传递给 web 子包
   let _ = web::run(data.clone(), rx).await;
}

因为我们单数据对象、单数据源,所以不会发生数据锁死的情况,因为每次更新整个数据都会全部锁定,然后去做的更新。一旦我们的业务出现多数据源,互相依赖时就需要认真考虑锁的粒度,一旦数据全锁,其他数据有依赖时需要读取更新就会等待造成阻塞。

使用serde 、 serde_json序列化与反序列化

通过网络请求或者直接读取的package.json都是返回的 json 格式的数据,我们需要将数据反序列化成我们需要的结构体。

{
  "id": "",
  "name": ""
}

通过serde_derive#[derive(Deserialize, Serialize)] 属性,可以很方便的将 json 数据反序列化成结构体。

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PkgJson {
    // ... 省略其他字段
}

我们读取的package.json,直接使用serde_json::from_reader()进行反序列化。

pub fn read_pkg_json<P: AsRef<Path>>(
    path: P,
) -> Result<PkgJson, Box<dyn std::error::Error + Send + Sync>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    let package = serde_json::from_reader(reader)?;
    Ok(package)
}

通过网络读取的依赖包信息,通过使用serde_json::from_str()进行反序列化。

pub async fn fetch_pkg_info(
    client: &Client,
    pkg_name: &str,
) -> Result<PkgInfo, Box<dyn std::error::Error>> {
    // ... 省略其他代码

    if res.status().is_success() {
        let body = res.text().await?;

        let info: PkgInfo = serde_json::from_str(&body)?;

        Ok(info)
    } else {
        // ... 省略错误处理代码
    }
}

如何进行序列化操作呢,我们在接收到数据更新后,需要将数据序列化成 json 格式发送给前端页面。

通过serde_json::json 宏函数json!()将结构体序列化成 json 格式。

可以直接通过serde_json::to_string() 将 json 数据转为 json 字符串发送给前端。

// ... 省略其他代码
pub async fn send_message(&self, mut session: Session) {
    let locked_data = self.data.lock().await;

    if let Err(e) = session
        .text(serde_json::to_string(&locked_data.clone()).unwrap())
        .await
    {
        eprintln!("Failed to send message to client: {:?}", e);
    }
}

为了符合 rust 的字段命名风格,我们需要将一些驼峰式的命名转换成下划线命名。通过 #[serde(rename = "devDependencies")]属性定义

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PkgJson {
    // ... 省略其他字段
    #[serde(rename = "devDependencies")]
    pub dev_dependencies: Option<HashMap<String, String>>,
}

除了反序列化给定的数据为结构体,我们可能还需要自定义数据字段,这时如果转换的数据里没有这个字段,我们就需要给它默认值。通过使用#[serde(default)]属性定义该字段取默认值,我们需要为这个结构体实现Default trait。

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PkgInfo {
    // ... 省略其他字段
    #[serde(default)]
    pub is_finish: bool,
}

impl Default for PkgInfo {
    fn default() -> Self {
        Self {
            // ... 省略其他字段
            is_finish: false,
        }
    }
}

rsup-web前端页面

将前端部分独立一个项目rsup-web,使用了vite-vue3开发,配置了unocss减少 css 的编写。

项目打包后放在web包下的static目录,并提供静态资源访问服务。

/// 获取静态文件路径
pub fn static_file_path() -> String {
    format!("{}/src/static", env!("CARGO_MANIFEST_DIR"))
}

pub async fn run(
    data: Arc<Mutex<Pkg>>,
    rx: Receiver<()>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // ... 省略其他代码

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(Arc::clone(&data)))
            .service(index)
            .service(Files::new("/static", static_file_path()).prefer_utf8(true))
            .app_data(ms.clone())
            .route("/ws", web::get().to(socket_index))
    })
    .bind(format!("0.0.0.0:{}", port))?
    .run()
    .await?;
}

因为我们是在 core 主包中调用的 web 子包目录,需要处理资源路径文件,通过env!("CARGO_MANIFEST_DIR")获取当前项目路径。

记得在前端项目中配置base,资源是通过/static访问的。

前端 socket 服务连接后立即发送数据

在页面连接 socket 服务后,需要立即发送数据给前端。是为了处理这种情况:后台服务消息已接收处理完,前端连接后没有数据展示。

一种简单的方法就是前端连接后发送一条消息,然后后台接收到消息后再向前端发送数据。

我们想要实现的是后端监听前端连接,成功时发送数据给前端。socket_index函数处理 socket 连接服务,在通过 actix_web::rt::spawn 启动了一个异步任务,调用了Ms::handle_message处理消息。

async fn socket_index(
    req: HttpRequest,
    stream: web::Payload,
    ms: web::Data<Arc<Mutex<Ms>>>,
) -> Result<HttpResponse, Error> {
    let (res, session, msg_stream) = actix_ws::handle(&req, stream)?;

    let ms = ms.get_ref().clone();
    let client_ip = req.connection_info().realip_remote_addr().unwrap()
    actix_web::rt::spawn(async move {
        println!(
            "new connection client's ip : {} ",
            clinent_ip
        );
        Ms::handle_message(ms, session, msg_stream).await;
    });
    Ok(res)
}

Ms::handle_message处理消息时,通过loop { }语法循环检测是否有消息过来,当通道有消息时,rx.recv()接收数据更新,然后向前端发送数据。这就造成了ms_lock一直被锁定,我们想要在开始执行发送数据,但是由于ms数据对象一致被循环锁定,异步任务无法获取到数据对象,就无法发送数据。

pub async fn handle_message(
    ms: Arc<Mutex<Ms>>,
    mut session: Session,
    mut msg_stream: MessageStream,
) {
    // ... 省略其他代码
    // 向前端发送消息
    let ms_clone = ms.clone();
    let session_clone = session.clone();
    tokio::spawn(async move {
        let ms_lock = ms_clone.lock().await;
        ms_lock.send_message(session_clone).await;
    });

    loop {
        let mut ms_lock = ms.lock().await;

        // ... 省略其他代码
        Some(_) = ms_lock.rx.recv()=> {
            println!("Got message");

            drop(ms_lock);

            let ms_lock = ms.lock().await;

            ms_lock.send_message(session.clone()).await;
        }
    }
}

在不改变现有的逻辑下,采取超时没有接收到消息时,结束本次循环。这样就释放了当前数据锁,给了一段异步任务获取数据对象的时间,从而可以发送数据。

use tokio::time::{timeout, Duration};

pub async fn handle_message(
    ms: Arc<Mutex<Ms>>,
    mut session: Session,
    mut msg_stream: MessageStream,
) {
    // ... 省略其他代码

    loop {
        // ... 省略其他代码

        result = timeout(Duration::from_millis(100),ms_lock.rx.recv())=>{
            match result{
                Ok(Some(_))=>{
                    drop(ms_lock);

                    let ms_lock = ms.lock().await;
                    ms_lock.send_message(session.clone()).await;
                }
                Ok(None)=>{
                    break;
                }
                Err(_)=>{
                    continue;
                }
            }
        }
    }
}

设置了 100ms 的超时时间,没有消息时,结束本次循环。在释放ms_lock数据锁后,异步任务获取到数据对象,发送数据。

这一块的逻辑会导致很多问题。已完全重构。

数据更新

我们通过创建通信通道tokio::sync::channel发送数据更新的消息。共享数据data和通道tx\rx都是分开的,这就导致了在所有数据更新的地方都需要发送更新通知tx实例引用,需要同时传送多个参数。

#[tokio::main]
async fn main() {
    // ... 省略其他代码
    let data: Arc<Mutex<pkg::Pkg>> = Arc::new(Mutex::new(pkg::Pkg::new()));

    let (tx, rx) = channel(100);

    // ... 省略其他代码
}

为了方便,我们定义结构体Package,将datatx封装到结构体中。为了实现克隆,我们需要使用Arc<Mutex<T>>包装它们。并需要实现Clone特性。

pub struct Package {
    pub pkg: Arc<Mutex<Pkg>>,
    pub sender: Arc<Mutex<Sender<()>>>,
    pub receiver: Arc<Mutex<Receiver<()>>>,
}
impl Clone for Package {
    fn clone(&self) -> Self {
        Self {
            pkg: self.pkg.clone(),
            sender: self.sender.clone(),
            receiver: self.receiver.clone(),
        }
    }
}

这样在主入口中,我们就可以通过Package::new()创建实例,然后传递给需要更新的地方。

之前理解的channel通道,以为多生产单消费是不能引用receiver实例的,原来是可以通过Arc<Mutex<T>>包装引用的,只是在消费时,如果有多个地方消费,只会有一个地方收到消息。

依赖版本对比

获取到目录下的package.json文件以及通过请求https://registry.npmjs.org/{pkg_name}获取到依赖包信息后,怎么过滤出需要更新的版本呢。

使用semver crate 包进行版本对比。数据格式要求是MAJOR.MINOR.PATCH

  • MAJOR 主版本更新,不兼容的 API 修改
  • MINOR 次要版本更新,兼容的功能性新增
  • PATCH 补丁版本更新,兼容的 bug 修复
pub fn compare_version(
    current_v: &str,
    latest_v: &str,
    all_v: HashMap<String, VersionInfo>,
) -> HashMap<String, VersionInfo> {
    // ... 省略其他代码

    // 当前版本
    let c_v = Version::parse(&clear_current_v).unwrap();
    // 最新版本
    let l_v = Version::parse(&latest_v).unwrap();

    let mut vs: Vec<Version> = all_v
        .keys()
        .filter_map(|k| Version::parse(k).ok())
        .filter(|v| *v > c_v && *v <= l_v)
        .collect();

    // ... 省略其他代码
}

总结

文章中的某些设计逻辑可能现在已经优化改掉了,只是作为过程中的想法记录一下。

往期关联学习文章:

  1. 模式匹配、trait 特征行为、必包、宏
  2. 多线程任务执行
  3. 并发线程间的数据共享
  4. 包、模块,引用路径
  5. 开发一个命令行工具

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2109198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode3256. 放三个车的价值之和最大 I

Every day a Leetcode 题目来源&#xff1a;3256. 放三个车的价值之和最大 I 解法1&#xff1a;贪心 从大到下排序矩阵所有值, 记为数组v。 转化此题&#xff1a;从r*c个数中选取3个数分别给到车1&#xff0c;车2&#xff0c;和车3&#xff0c;使得符合条件的三数之和最大。…

rancher upgrade 【rancher 升级】

文章目录 1. 背景2. 下载3. 安装4. 检查5. 测试5.1 创建项目5.2 创建应用5.3 删除集群5.4 注册集群 1. 背景 rancher v2.8.2 升级 v2.9.1 2. 下载 下载charts helm repo add rancher-latest https://releases.rancher.com/server-charts/latest helm repo update helm fetc…

NIO、Reactor模式与直接内存

1.NIO NIO有三大核心组件&#xff1a;Selector选择器、Channel管道、buffer缓冲区。、 1.1Selector Selector的英文含义是“选择器”&#xff0c;也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。 Java NIO的选择器允许一个单独的线程来监视多个输…

鸿蒙MPChart图表自定义(四)短刻度线

对于图表中的x轴效果&#xff0c;我们有时想要实现如图所示的特定刻度线。若需绘制x轴的短刻度线&#xff0c;我们可以利用现有资源&#xff0c;将原本的网格线稍作修改&#xff0c;只需绘制一条简洁的短线即可达到目的。 具体的方法就是写一个类MyXAxisRender继承自XAxisRend…

iOS——runLoop

什么是runloop RunLoop实际上就是一个对象&#xff0c;这个对象管理了其需要处理的事件和消息&#xff0c;并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后&#xff0c;就会处于这个函数内部的循环中&#xff0c;直到循环结束&#xff0c;函数返回。 RunLoo…

【转载】golang内存分配

Go 的分配采用了类似 tcmalloc 的结构.特点: 使用一小块一小块的连续内存页, 进行分配某个范围大小的内存需求. 比如某个连续 8KB 专门用于分配 17-24 字节,以此减少内存碎片. 线程拥有一定的 cache, 可用于无锁分配. 同时 Go 对于 GC 后回收的内存页, 并不是马上归还给操作系…

Android13 Hotseat客制化--Hotseat修改布局、支持滑动、去掉开机弹动效果、禁止创建文件夹

需求如题&#xff0c;实现效果如下 &#xff1a; 固定Hotseat的padding位置、固定高度 step1 在FeatureFlags.java中添加flag,以兼容原生态代码 public static final boolean STATIC_HOTSEAT_PADDING true;//hotseat area fixed step2:在dimens.xml中添加padding值和高度值…

信息系统安全保障

关注这个证书的其他相关笔记&#xff1a;NISP 一级 —— 考证笔记合集-CSDN博客 0x01&#xff1a;信息系统 信息系统是具有集成性的系统&#xff0c;每一个组织中信息流动的综合构成一个信息系统。信息系统是根据一定的需要进行输入、系统控制、数据处理、数据存储与输出等活动…

职场关系课:职场上的基本原则(安全原则、进步原则、收益原则、逃生舱原则)

文章目录 引言安全原则进步原则收益原则逃生舱原则引言 职场上的王者,身体里都应该有三个灵魂: 一个文臣,谨小慎微,考虑风险; 一个武将,积极努力,谋求胜利; 一个商人,精打细算,心中有数。 安全原则 工作安全:保住自己的工作和位置信用安全:保住个人的信用,如果领…

《征服数据结构》差分数组

摘要&#xff1a; 1&#xff0c;差分数组的介绍 2&#xff0c;二维差分数组的介绍 1&#xff0c;差分数组的介绍 差分数组主要是操作区间的&#xff0c;关于区间操作的数据结构比较多&#xff0c;除了前面讲的《稀疏表》&#xff0c;还有树状数组&#xff0c;线段树&#xff0c…

高德地图SDK Android版开发 10 InfoWindow

高德地图SDK Android版开发 10 InfoWindow 前言相关类和方法默认样式Marker类AMap类AMap.OnInfoWindowClickListener 接口 自定义样式(视图)AMap 类AMap.ImageInfoWindowAdapter 接口 自定义样式(Image)AMap.ImageInfoWindowAdapter 接口 示例界面布局MapInfoWindow类常量成员变…

215篇【大模型医疗】论文合集(附PDF)

ChatGPT的横空出世引发了新一轮生成式大模型热潮&#xff0c;作为最新技术的"试验场"&#xff0c;医疗也成为众多大模型的热门首选。 我整理了215篇医疗和大模型的论文&#xff0c;供大家学习和参考。 领215篇医疗和大模型论文

欧拉下搭建第三方软件仓库—docker

1.创建新的文件内容 切换目录到etc底下的yum.repos.d目录&#xff0c;创建docker-ce.repo文件 [rootlocalhost yum.repos.d]# cd /etc/yum.repos.d/ [rootlocalhost yum.repos.d]# vim docker-ce.repo 编辑文件,使用阿里源镜像源&#xff0c;镜像源在编辑中需要单独复制 h…

vue3的学习(2)

属性绑定 1.将一个容器中的class和id使用vue用法赋上具体的值&#xff0c;这样就可以动态的给容器添加上自己想要给其添加的class或者id或者title。 2.关键语法&#xff0c;在容器中的class或者id或者title前面加上 "v-bind:"&#xff0c;当加上"v-bind关键语…

计算机网络基础 - 应用层(2)

计算机网络基础 应用层FTP 与 EMail文件传输协议 FTP电子邮件 EMail主要组成部分SMTP概述SMTP 与 HTTP1.1 邮件报文格式报文格式多媒体扩展 MIME 邮件访问协议概述POP3IMAP DNS概述域名结构工作机理集中式设计分布式、层次数据库根 DNS 服务器顶级域 DNS 服务器权威 DNS 服务器…

公司的企业画册如何制作?

企业画册是公司形象和产品服务展示的重要载体&#xff0c;一个制作精良的企业画册不仅能展示公司的实力&#xff0c;也能提升客户对公司专业度的认可。以下是制作企业画册的步骤和要点&#xff0c;帮助你的公司画册既美观又实用。 1.要制作电子杂志,首先需要选择一款适合自己的…

OpenCV结构分析与形状描述符(7)计算轮廓的面积的函数contourArea()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算轮廓的面积。 该函数计算轮廓的面积。与 moments 类似&#xff0c;面积是使用格林公式计算的。因此&#xff0c;返回的面积与你使用 drawCo…

Mysql中的隐式COMMIT以及Savepoints的作用以及MySQL的Innodb分空间存储、设计优化、索引等几个小知识点整理

一、Mysql中的隐式COMMIT以及Savepoints的作用 Mysql默认是自动提交的&#xff0c;如果要开启使用事务&#xff0c;首先要关闭自动提交后START TRANSACTION 或者 BEGIN 来开始一个事务&#xff0c;使用ROLLBACK/COMMIT来结束一个事务。但即使如此&#xff0c;也并不是所有的操作…

零知识证明在BSV网络上的应用

​​发表时间&#xff1a;2023年6月15日 2024年7月19日&#xff0c;BSV区块链主网上成功通过使用零知识证明验证了一笔交易。 零知识证明是一种技术&#xff0c;它允许一方&#xff08;证明者&#xff09;在不透露任何秘密的情况下&#xff0c;向另一方&#xff08;验证者&…

TP-link-路由器上网设置(已有路由器再连接新的网线)

一、192.168.0.1进入管理界面&#xff0c;比如密码&#xff1a;D804D804。 二、这是设置连接账户和密码&#xff08;比如账户&#xff1a;TP-LINK_F56C 比如密码&#xff1a;D804D804&#xff09;登录后台管理、移动设备连接。比较固定。 三、 有的网络是分&#xff1a;&#…