Vue 大文件切片上传实现指南包会,含【并发上传切片,断点续传,服务器合并切片,计算文件MD5,上传进度显示,秒传】等功能
Vue 大文件切片上传实现指南
背景
在Web开发中,文件上传是一个常见的功能需求,尤其是当涉及到大文件上传时,为了提高上传的稳定性和效率,文件切片上传技术便显得尤为重要。通过将大文件切分成多个小块(切片)进行上传,不仅可以有效减少单次上传的数据量,降低网络波动对上传过程的影响,还能实现如断点续传、秒传等高级功能。本文将以Vue
为框架,配合 Axios
进行 HTTP
请求,详细介绍如何实现一个支持文件切片上传的功能。
前端准备工作
在开始编码之前,请确保你的项目中已经安装了 axios
和 spark-md5
两个库。axios
用于发起网络请求,spark-md5
用于计算文件的 MD5
值,从而支持秒传和断点续传功能。
前端需要实现的功能
-
文件选择和限制:
通过
<input type="file" @change="handleFileChange" accept="video/*" />
实现了文件的选择,同时限制了用户只能选择视频文件进行上传。 -
计算文件的MD5值
在
computeFileHash
方法中,利用SparkMD5
库计算用户选中文件的MD5
值。这一步是为了之后能够校验文件的完整性和唯一性。 -
校验文件是否需要上传
在
checkFile
方法中,通过向服务器查询文件的MD5
值,判断该文件是否已经上传过,以此实现秒传功能,避免重复上传相同文件。 -
文件切片
在
sliceFileAndUpload
方法中,将大文件切割成多个小片段(切片),这样做的目的是为了支持大文件的分块上传,提高上传效率,同时也便于出错时重新上传单个切片而不是整个文件。 -
并发上传切片
通过
processPool
和uploadChunk
方法实现切片的并发上传,限制了最大并发数(MAX_REQUEST)
,以免过多并发请求压崩服务器。 -
上传进度反馈
通过
uploadProgress
数据和模板中的进度显示,用户可以实时看到文件上传的进度。 -
服务器通知合并切片
在所有切片上传完成后,通过
notifyServerToMerge
方法向服务器发送通知,请求服务器端进行切片的合并,以重建原始文件。
后端需要支持的API接口
为了支持前端的大文件上传和处理逻辑,后端需要提供以下API接口:
- 文件校验API
- 功能:检查文件的完整性和上传状态。这通常通过文件的唯一标识(如MD5哈希值)来实现。
- 输入参数:文件唯一标识(如MD5哈希值)。
- 返回值:告知客户端该文件是否已经存在,如果存在,是否完整。如果文件已经存在且不完整,则返回已上传的切片信息。
- 切片上传API
- 功能:接收文件的单个切片,并保存到服务器的临时存储位置。
- 输入参数:文件的唯一标识,切片内容,切片的序号。
- 返回值:确认切片上传成功或失败的状态。
- 切片合并API
- 功能:将所有上传的切片合并成一个完整的文件。
- 输入参数:文件的唯一标识,可能还包括文件名、总切片数等信息。
- 返回值:合并操作的成功或失败状态,以及最终文件的访问URL(可选)。
- 上传进度查询API
- 功能:查询文件上传的进度,这对于恢复上传和提供用户反馈非常有用。
- 输入参数:文件的唯一标识。
- 返回值:已上传的切片列表或上传进度百分比。
这些API合起来支持了一个分块上传文件的完整流程,包括文件的校验
、切片的上传
、切片的合并
,以及上传进度查询
。这个流程可以有效地处理大文件上传,减少网络传输的负担,提高上传的可靠性,并允许上传过程中的暂停和恢复
。
执行流程
一开始用户通过界面选择一个文件进行上传,进行文件选择,用户通过文件选择框悬着一个大文件,比如视频文件,触发handleFileChange
方法,然后再计算这个大文件的MD5,使用computeFileHash
方法计算选中文件的MD5哈希值,计算完成后检查文件是否需要上传,向服务器发起请求,根据文件的MD5哈希值执行checkFile
方法检查文件是否已经存在,如果文件已经存在通知用户秒传功能
并将上传进度设为100%,如果文件需要上传,则使用sliceFileAndUpload
方法将文件切成很多个小块,每个切片及其索引都被添加到requestPool
请求池中,从requestPool
中并发上传切片processPool
方法,对每个切片调用uploadChunk
方法进行实际上传,通过MAX_REQUEST
控制并发上传的数量,没上传一个切片,uploadChunksCount
增加,并更新上传进度。所有切片上传完成后,通知服务器合并这些切片notifyServerToMerge
,当服务器成功合并所有切片成原始后,整个切片上传流程完成。
实现步骤
步骤一:用户选择文件
用户通过 <input type="file">
选择文件后,handleFileChange
事件被触发。在这个事件处理函数中,我们首先获取到用户选择的文件,然后计算文件的 MD5
值,以此作为文件的唯一标识。这一步是实现断点续传
和秒传
功能的关键。
<template><div><!-- 文件选择框,仅接受视频文件 --><input type="file" @change="handleFileChange" accept="video/*" /><!-- 上传按钮 --><button @click="handleUpload">Upload</button><!-- 上传进度显示 --><div v-if="uploadProgress > 0">Upload Progress: {{ uploadProgress }}%</div></div>
</template>
步骤二:计算文件 MD5
使用 spark-md5
库计算文件的 MD5
值。通过FileReader API
读取文件内容,然后计算其 MD5
值。这个过程可能会花费一些时间,因此使用 Promise
来异步处理。
async computeFileHash(file) {const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();return new Promise((resolve) => {fileReader.onload = (e) => {spark.append(e.target.result);const hash = spark.end();resolve(hash);};fileReader.readAsArrayBuffer(file);});
}
步骤三:检查文件状态,检查文件是否已经上传还是部分上传
在上传文件之前,先向服务器发送请求,检查这个文件是否已经部分或全部上传过。这一步是实现断点续传的关键。服务器根据文件的 MD5
值返回已上传的切片信息或表示文件完全上传的状态。
// 向服务器查询文件是否已经部分或完全上传
async checkFile(fileHash) { <--- 此处应替换为你的接口调用代码 --->// 假设接口返回 { shouldUpload: boolean, uploadedChunks: Array<number> }return { shouldUpload: true, uploadedChunks: [] };
},
步骤四:切片并准备上传
根据服务器返回的信息,如果文件未完全上传,我们将文件分割成多个切片。然后根据已上传的切片信息,跳过那些已经上传的切片,仅上传剩余的切片。
切片并准备上传在sliceFileAndUpload
方法中实现。这个方法首先计算了整个文件应该被分割成多少切片(基于设定的切片大小),然后根据服务器返回的已上传切片信息(uploadedChunks)
,它会跳过这些已经上传的切片,只将剩余的切片添加到请求池(requestPool)
中准备上传。
// 切片并准备上传
sliceFileAndUpload(fileHash, uploadedChunks) {const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MBthis.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数this.uploadProgress = 0; // 重置上传进度for (let i = 0; i < this.chunkCount; i++) {if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片this.requestPool.push({ chunk, index: i }); // 加入请求池}this.processPool(fileHash); // 开始处理请求池},
上面这段代码中,uploadedChunks
参数是一个数组,包含了所有已上传切片的索引。通过检查当前切片的索引是否包含在这个数组中,代码决定是否跳过当前切片的上传。如果索引不在uploadedChunks
中,这意味着该切片还没有被上传,因此需要将其添加到requestPool
中等待上传。这样,只有那些未上传的切片会被实际上传,从而实现了断点续传的功能。processPool
进行并发切片上传
步骤五:并发上传切片
为了提高上传效率,我们使用并发上传的方式。设置最大并发数,控制同时上传的切片数量。通过逐一上传切片,并监听每个上传请求的完成,从而动态调整并发请求。
并发上传切片的逻辑主要在processPool
方法中实现。这个方法负责管理并发请求,确保同时只有一定数量的上传请求在处理中。这通过一个简单的请求池(requestPool)
和控制最大并发数量(MAX_REQUEST)
来实现。
// 处理请求池中的切片上传
processPool(fileHash) {while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片this.uploadChunk(chunk, fileHash, index) // 上传切片.then(() => {this.uploadedChunksCount++; // 更新已上传切片数量this.uploadProgress = ((this.uploadedChunksCount / this.chunkCount) * 100).toFixed(2); // 更新上传进度if (this.requestPool.length > 0) {this.processPool(fileHash); // 继续处理请求池} else if (this.uploadedChunksCount === this.chunkCount) {// 所有切片都已上传,通知服务器合并this.notifyServerToMerge(fileHash);}}).finally(() => {this.MAX_REQUEST++; // 释放一个请求槽});this.MAX_REQUEST--; // 占用一个请求槽}
},
在这个方法中,while
循环检查请求池中是否还有待处理的切片,并且当前活跃的请求数量是否小于允许的最大并发数量MAX_REQUEST
。如果这两个条件都满足,它会从请求池中取出一个切片,并调用uploadChunk
方法来上传它,同时减少MAX_REQUEST
的值来反映一个新的请求已经开始。
当一个切片上传完成后,then
回调函数会增加已上传切片的计数并更新上传进度。如果请求池中还有待上传的切片,它会递归调用processPool
来处理下一个切片。一旦所有切片都上传完成,它会调用notifyServerToMerge
来通知服务器所有切片已经上传完毕,可以合并成一个完整的文件。通过这种方式,代码能够在保持最大并发限制的同时,高效地处理切片的上传。
步骤六:服务器合并切片
所有切片上传完成后,客户端向服务器发送一个合并切片的请求。服务器接收到请求后,将所有切片合并成原始文件,并返回合并结果。
// 通知服务器合并切片
notifyServerToMerge(fileHash) {// 通知服务器合并切片,应替换为真实的合并API调用console.log(`通知服务器将文件与哈希合并: ${fileHash}`);
},
一个API调用,向服务器发送一个请求来触发合并已上传切片的操作。这个请求通常会携带一些必要的信息,比如文件的唯一标识(在这个例子中是fileHash
),以及可能还有其他诸如文件名
、文件大小
、切片数量
等信息,这些信息取决于服务器端合并切片的具体要求。
服务器收到合并请求后,会根据提供的信息找到所有相关的切片,按正确的顺序将它们合并成一个完整的文件,并将该文件存储在服务器上的适当位置。完成这个过程后,服务器可能还会向客户端发送一个响应,通知合并操作的结果(成功或失败),以及可能的后续步骤或需要的信息。
通过上述步骤,实现了一个高效稳定的大文件上传功能,极大提升了用户体验。
全部代码
<template><div><!-- 文件选择框,仅接受视频文件 --><input type="file" @change="handleFileChange" accept="video/*" /><!-- 上传按钮 --><button @click="handleUpload">Upload</button><!-- 上传进度显示 --><div v-if="uploadProgress > 0">Upload Progress: {{ uploadProgress }}%</div></div>
</template><script>
import axios from "axios";
import SparkMD5 from "spark-md5"; // 引入SparkMD5用于计算文件的MD5值export default {data() {return {selectedFile: null, // 用户选择的文件uploadProgress: 0, // 上传进度requestPool: [], // 请求池,存储待上传的切片信息MAX_REQUEST: 6, // 最大并发请求数量chunkCount: 0, // 文件切片总数uploadedChunksCount: 0, // 已上传的切片数量};},methods: {// 处理文件选择事件async handleFileChange(event) {this.selectedFile = event.target.files[0];if (!this.selectedFile) return; // 未选择文件则返回// 可以在这里添加文件格式校验const fileHash = await this.computeFileHash(this.selectedFile); // 计算文件hashconst { shouldUpload, uploadedChunks } = await this.checkFile(fileHash); // 检查文件是否需要上传if (!shouldUpload) {alert("文件已存在,秒传成功!");this.uploadProgress = 100; // 直接设置进度为100%return;}this.sliceFileAndUpload(fileHash, uploadedChunks); // 切片并上传},// 计算文件的MD5computeFileHash(file) {return new Promise((resolve) => {const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();fileReader.onload = (e) => {spark.append(e.target.result);const hash = spark.end();resolve(hash); // 返回计算得到的hash值};fileReader.readAsArrayBuffer(file);});},// 检查文件是否已经上传过async checkFile(fileHash) {// 应替换为真实的API调用来检查文件状态return { shouldUpload: true, uploadedChunks: [] }; // 模拟返回值},// 切片并准备上传sliceFileAndUpload(fileHash, uploadedChunks) {const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MBthis.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数this.uploadProgress = 0; // 重置上传进度for (let i = 0; i < this.chunkCount; i++) {if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片this.requestPool.push({ chunk, index: i }); // 加入请求池}this.processPool(fileHash); // 开始处理请求池},// 处理请求池中的切片上传processPool(fileHash) {while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片this.uploadChunk(chunk, fileHash, index) // 上传切片.then(() => {this.uploadedChunksCount++; // 更新已上传切片数量this.uploadProgress = ((this.uploadedChunksCount / this.chunkCount) * 100).toFixed(2); // 更新上传进度if (this.requestPool.length > 0) {this.processPool(fileHash); // 继续处理请求池} else if (this.uploadedChunksCount === this.chunkCount) {// 所有切片都已上传,通知服务器合并this.notifyServerToMerge(fileHash);}}).finally(() => {this.MAX_REQUEST++; // 释放一个请求槽});this.MAX_REQUEST--; // 占用一个请求槽}},// 上传单个切片async uploadChunk(chunk, fileHash, index) {const formData = new FormData();formData.append("chunk", chunk);formData.append("hash", fileHash);formData.append("index", index);// 替换为真实的上传URL,并根据需要实现onUploadProgressawait axios.post("上传URL", formData);},// 通知服务器合并切片notifyServerToMerge(fileHash) {// 通知服务器合并切片,应替换为真实的合并API调用console.log(`通知服务器将文件与哈希合并: ${fileHash}`);},},
};
</script>
效果:
相关文章:

Vue 大文件切片上传实现指南包会,含【并发上传切片,断点续传,服务器合并切片,计算文件MD5,上传进度显示,秒传】等功能
Vue 大文件切片上传实现指南 背景 在Web开发中,文件上传是一个常见的功能需求,尤其是当涉及到大文件上传时,为了提高上传的稳定性和效率,文件切片上传技术便显得尤为重要。通过将大文件切分成多个小块(切片࿰…...

【VUE+ElementUI】el-table表格固定列el-table__fixed导致滚动条无法拖动
【VUEElementUI】el-table表格固定列el-table__fixed导致滚动条无法拖动 背景 当设置了几个固定列之后,表格无数据时,点击左侧滚动条却被遮挡,原因是el-table__fixed过高导致的 解决 在index.scss中直接加入以下代码即可 /* 设置默认高…...
重置gitlab root密码
gitlab-rails console -e production user User.where(id: 1).first user User.where(name: "root").first #输入重置密码命令 user.password"admin123!" #再次确认密码 user.password_confirmation"admin123!" #输入保存命令&am…...

v-text 和v-html
接下来,我讲介绍一下v-text和v-html的使用方式以及它们之间的区别。 使用方法 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-widt…...
学习笔记——C语言基本概念结构体共用体枚举——(10)
1、结构体 定义新的数据类型: 数据类型:char short int long float double 数组 指针 结构体 结构体: 新的自己定义的数据类型 格式: struct 名字{ 成员 1; 成员 2; 。 。 。 …...
VMware虚拟机三种网络模式
VMware虚拟机提供了三种主要的网络连接模式,它们分别是: 桥接模式(Bridged Mode)网络地址转换模式(NAT Mode)仅主机模式(Host-Only Mode) 1. 桥接模式(Bridged Mode&am…...

Ai音乐大师演示(支持H5、小程序)独立部署源码
Ai音乐大师演示(支持H5、小程序)独立部署源码...

Windows下Docker搭建Flink集群
编写docker-compose.yml 参照:https://github.com/docker-flink/examples/blob/master/docker-compose.yml version: "2.1" services:jobmanager:image: flink:1.14.4-scala_2.11expose:- "6123"ports:- "18081:8081"command: jobma…...

VGA显示器驱动设计与验证
1.原理 场同步信号的单位是像素点 场同步信号的单位是一行 60的含义是每秒钟刷新60帧图像 全0表示黑色 2.1 CLK_gen.v module CLK_gen(input wire sys_clk ,input wire sys_rst_n ,output wire CLK_out ,output wire locked );parameter STATE1b0; reg [1:0] cnt; r…...

jupyter notebook 配置默认文件路径
Jupyter是一种基于Web的交互式计算环境,支持多种编程语言,如Python、R、Julia等。使用Jupyter可以在浏览器中编写和运行代码,同时还可以添加Markdown文本、数学公式、图片等多种元素,非常适合于数据分析、机器学习等领域。 安装 …...

强大缓存清理工具 NetShred X for Mac激活版
NetShred X for Mac是一款专为Mac用户设计的强大缓存清理工具,旨在帮助用户轻松管理和优化系统性能。这款软件拥有直观易用的界面,即使是初次使用的用户也能快速上手。 软件下载:NetShred X for Mac激活版下载 NetShred X能够深入扫描Mac系统…...

在ssh 工具 Linux screen会话中使用鼠标进行上下滚动
经过几次发现 除xshell外, WindTerm finalshell MobaXterm 都是进入会话后,发现其界面无法滚动屏幕向上查看 如果想要在Linux screen会话中使用鼠标进行上下滚动。必须首先进入该screen的回滚(scrollback模式)才能进行上下滚动 第一步ÿ…...

Github2024-04-03 开源项目日报 Top10
根据Github Trendings的统计,今日(2024-04-03统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目4TypeScript项目2Jupyter Notebook项目2C++项目1Shell项目1Go项目1非开发语言项目1Rust项目1从零开始构建你喜爱的技术 创建周期:21…...

Linux笔记之制作基于ubuntu20.4的最小OpenGL C++开发docker镜像
Linux笔记之制作基于ubuntu20.4的最小OpenGL C开发docker镜像 —— 2024-04-03 夜 code review! 文章目录 Linux笔记之制作基于ubuntu20.4的最小OpenGL C开发docker镜像1.这里把这本书的例程代码放在了Dockerfile所在的文件夹内以使镜像预装例程代码2.创建Dockerfile3.构建Do…...
企业为什么选择高防服务器?
高防服务器顾名思义就是一种具有高度安全性的服务器,有着很高的防御能力,可以提供更加安全可靠的服务,能够有效地避免分布式拒绝服务攻击和其它的网络安全威胁,以下就是企业选择高防服务器的原因。 高防服务器在硬件安全方面有着很…...

OpenHarmony实战:轻量级系统之配置其他子系统
除上述子系统之外,还有一些必要但是无需进行移植的子系统。如:分布式任务调度子系统、DFX子系统。 这些子系统添加方式比较简单,在“vendor/MyVendorCompany/MyProduct/config.json”文件中进行如下配置即可: {"subsystem&…...
关于VueCli项目中如何加载调试Worker和SharedWorker
安装Webpack插件 VueCli 项目中默认是没有加载 worker 的配置,需要额外安装 webpack 插件来实现,让我们开始安装 worker-loader 插件 # npm npm install worker-loader # pnpm pnpm install worker-loader # yarn yarn add worker-loader配置Webpack插…...

Centos7安装单机版Kafka
下载 链接:https://pan.baidu.com/s/1W8lVEF6Y-xlg6zr3l9QAbg?pwdhbkt 提取码:hbkt 上传到服务器/opt目录 安装 # kafka安装目录为 /opt/kafka cd /opt; mkdir kafka; mv kafka_2.13-2.7.0.tgz ./kafka;cd kafka; #解压 tar -zxvf kafka_2.13-2.7.0…...

基于深度学习的钢材表面缺陷检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)
摘要:本文深入研究了基于YOLOv8/v7/v6/v5的钢材表面缺陷检测系统,核心采用YOLOv8并整合了YOLOv7、YOLOv6、YOLOv5算法,进行性能指标对比;详述了国内外研究现状、数据集处理、算法原理、模型构建与训练代码,及基于Strea…...

计算机网络:数据链路层 - 点对点协议PPP
计算机网络:数据链路层 - 点对点协议PPP PPP协议的帧格式透明传输字节填充法零比特填充法 差错检测循环冗余校验 对于点对点链路,PPP协议是目前使用最广泛的数据链路层协议。比如说,当用户想要接入互联网,就需要通过因特网服务提供…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...

【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...