当前位置: 首页 > news >正文

WEB前端开发中如何实现大文件上传?

大文件上传是个非常普遍的场景,在面试中也会经常被问到,大文件上传的实现思路和流程。在日常开发中,无论是云存储、视频分享平台还是企业级应用,大文件上传都是用户与服务器之间交互的重要环节。随着现代网络应用的日益复杂化,大文件上传已经成为前端开发中不可或缺的一部分。

然而,在实现大文件上传时,我们通常会面临以下几个挑战:

  1. 上传超时一般前端请求都会限制最大请求时长,比如axios设置timeout,或者是 nginx(或其它代理/网关) 限制了最大请求时长。

  2. 服务器压力:大文件上传会给服务器带来较大的压力,甚至可能导致服务器崩溃。

  3. 文件大小超限:一般后端都会对上传文件的大小做限制,比如nginx和server都会限制。

  4. 用户体验:上传过程中用户需要等待较长时间,用户体验差。

  5. 网络波动各种网络原因导致上传失败,比如网络不稳定可能导致上传过程中断,且失败之后需要从头开始。

对于前三点,虽说可以通过一定的配置来解决,但有时候也相当麻烦,或者服务器就规定不允许上传大型文件,需要兼顾实际场景。上传慢的话倒是无伤大雅,忍一忍是可以接受的,只是体验不好,但是失败后在重头开始上传,在网络环境差的时候简直就是灾难。为了应对以上挑战,我们就需要用到切片上传、断点续传等技术手段。

二、实现思路分析

整体流程图如下:

思路如下:

  1. 每个文件要有自己唯一的标识,因此在进行分片上传前,需要对整个文件进行MD5加密,生成MD5码,在后面上传文件每次调用接口时以formData格式上传给后端。可以使用spark-md5 计算文件的内容hash,以此来确定文件的唯一性将文件hash发送到服务端进行查询。以此来确定该文件在服务端的存储情况,这里可以分为三种:未上传、已上传、上传部分。

  2. 根据服务端返回的状态执行不同的上传策略。已上传:执行秒传策略,即快速上传,实际上没有对该文件进行上传,因为服务端已经有这份文件了。未上传、上传部分:执行计算待上传分块的策略并发上传还未上传的文件分块。当传完最后一个文件分块时,向服务端发送合并的指令,即完成整个大文件的分块合并,实现在服务端的存储。

上传过程:

  1. 分割文件:将要上传的文件切割成多个小文件片段。主要使用JavaScript的File API中的slice方法来实现。

  2. 上传文件分片:使用XMLHttpRequest或者Fetch API将分片信息以formData格式,并携带相关信息,如文件名、文件ID、当前片段序号等参数传给分片接口。

  3. 后端接收并保存文件片段:后端接收到每个文件片段后,将其保存在临时位置,并记录文件片段的序号、文件ID和文件MD5 hash值等信息。

  4. 续传处理:如果上传过程中断,下次继续上传时,通过查询后端已保存的文件片段信息,得知需要上传的文件片段,从断点处继续上传剩余的文件片段。

  5. 合并文件:当所有文件片段都上传完成后,后端根据文件ID将所有片段合并成完整的文件。

三、切片上传

切片上传原理:通过使用JavaScript的File API中的slice方法将大文件分割成多个小片段(chunk),然后逐个上传每个片段,在上传完切片后,前端通知后台再将文件片段拼接为一个完整的文件。

这样做的优点是可以并行多个请求一起上传文件,提高上传效率,并且在上传过程中如果某个片段因为某些原因上传失败,也不会影响其它文件切片,只需要重新上传该失败片段即可,不必重新上传整个文件。

实现思路:

在JavaScript中,文件File对象是Blob对象的子类,Blob对象包含了slice方法,通过这个方法,可以对二进制文件进行拆分。循环发送多个上传请求,然后返回结果后计数,当计数达到file片段长度后终止上传。

<input type="file" name="file" id="file" />
const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);uploadChunk(chunk);currentPosition += chunkSize;}
})function uploadChunk(chunk) {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 根据项目实际情况axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {// 上传成功console.log('分片上传成功', res)}).catch(error => {// 上传失败console.log('分片上传失败', error)})
}

四、并发上传

并发上传相对要优雅一下,将文件分割成小片段后,使用Promise.all()把所有请求都放到一个Promise.all里,它会自动判断所有请求都完成然后触发 resolve 方法。并发上传可以同时上传多个片段而不是依次上传,进一步提高效率。

实现思路:

1、使用slice方法对二进制文件进行拆分,并把拆分的片段放到chunkList里面。

2、使用map将chunkList里面的每个chunk映射到一个Promise上传方法。

3、把所有请求都放到一个Promise.all里,它会自动判断所有请求都完成然后触发 resolve 方法,上传成功后通知后端合并分片文件。

代码实现如下:

const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;// 存储文件的分片let chunkList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);chunkList.push(chunk);currentPosition += chunkSize;}uploadChunk(chunkList, file.name)
})function uploadChunk(chunkList, fileName) {const uploadPromiseList = chunkList.map((chunk, index) => {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 根据项目实际情况return axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})})Promise.all(uploadPromiseList).then(res => {// 上传成功并通知后端合并分片文件axios.post('/api/oss/file/merge', {message: fileName},{headers: { 'Content-Type': 'application/json' },timeout: 600000,}).then(data => {console.log('文件合并成功', data)})}).catch(error => {// 上传错误console.log('上传失败', error)})
}

五、断点续传之1

断点续传允许在网络中断或其它原因导致上传失败时,从上次上传中断的位置继续上传,而不是重新从头上传整个文件。

实现断点续传需要后端配合记录上传的进度,并且在前端重新上传时,需要先查询已上传的进度,让后从断点处继续上传。

const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;// 存储文件的分片let chunkList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);chunkList.push(chunk);currentPosition += chunkSize;}axios.post('/api/upload/file/history',{fileName: file.name},{headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {const historyChunks = res.uploadedChunks;const remainChunks = chunkList.filter((item, index) => !historyChunks.includes(index));// 并发上传剩余分片uploadChunk(remainChunks, file.name)})
})function uploadChunk(chunkList, fileName) {const uploadPromiseList = chunkList.map((chunk, index) => {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 根据项目实际情况return axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})})Promise.all(uploadPromiseList).then(res => {// 剩余分片上传成功并通知后端合并分片文件axios.post('/api/oss/file/merge', {message: fileName},{headers: { 'Content-Type': 'application/json' },timeout: 600000,}).then(data => {console.log('文件合并成功', data)})}).catch(error => {// 上传错误console.log('上传失败', error)})
}

以上是一个简易版的断点续传实现流程代码,但在实际场景应用中我们还需要更严谨的处理来实现断点续传功能。不如,上传文件前通常需要生成文件的唯一标识,比如文件名与文件大小的组合、文件的hash值或者文件hash值与文件大小的组合来支持断点续传的逻辑。请继续看下面的代码实现!!!

六、断点续传之2

已上传的执行秒传策略,即快速上传,实际上没有对该文件进行上传,因为服务端已经有这份文件了。

秒传的关键在于计算文件的唯一性标识。文件的不同不是命名的差异,而是内容的差异,所以我们将整个文件的二进制码作为入参,计算 Hash 值,将其作为文件的唯一性标识。一般而言,这样做就够了,但是摘要算法是存在碰撞概率的,我们如果想要再严谨点的话,可以将文件大小也作为衡量指标,只有文件摘要和文件大小同时相等,才认为是相同的文件。

<input type="file" name="file" id="file" @change="changeFile" />

计算文件hash值可以使用spark-md5。

import SparkMD5 from 'spark-md5'

通过input的change事件获取要上传的文件。

function changeFile(event) {const file = event.target.files[0];handleUploadFile(file, 1)
}

接下来对文件进行分片和hash计算:

/*** @param {File} file 目标上传文件* @param {number} size 上传分块大小,单位Mb* @returns {filelist:ArrayBuffer,fileHash:string}*/
async function handleSliceFile(file, size = 1) {return new Promise((resolve, reject) => {// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * size;// 分片数const totalChunkCount = file && Math.ceil(file.size / chunkSize);// 当前已执行分片数位置let currentChunkCount = 0;// 存储文件的分片let fileList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;// 文件读取对象const fileReader = new FileReader();// spark-md5 计算文件hash值SparkMD5对象const spark = new SparkMD5.ArrayBuffer();// 存储计算后的文件hash值let fileHash = "";// 错误fileReader.onerror = function () {reject('Error reading file');};fileReader.onload = (e) => {//当前读取的分块结果 ArrayBufferconst curChunk = e.target.result;//将当前分块追加到spark对象中spark.append(curChunk);currentChunkCount++;fileList.push(curChunk);//判断分块是否完成if (currentChunkCount >= totalChunkCount) {// 全部读取,获取文件hashfileHash = spark.end();resolve({ fileList, fileHash });} else {readNext();}};//读取下一个分块const readNext = () => {//计算分片的起始位置和终止位置const start = chunkSize * currentChunkCount;let end = start + chunkSize;if (end > file.size) {end = file.size}//读取文件,触发onLoadfileReader.readAsArrayBuffer(blobSlice.call(file, start, end))}readNext()})
}

文件上传,首选调用接口获取需要上传的文件index,返回的集合length等于0执行秒传,如果返回的集合length不等于0执行需要过滤得到需要上传的remainingChunks,使用map将remainingChunks里面的每个chunk映射为一个Promise上传方法,把所有请求都放到一个Promise.all里,上传成功后通知后端合并分片文件。

sync function handleUploadFile(file, chunkSize) {const { fileList, fileHash } = await handleSliceFile(file, chunkSize);// 存放切片let chunkList = fileList;// 显示上传的进度条let process = 0;// 获取文件上传状态const { data } = await axios.post('/api/upload/file/history', {fileHash,totalCount: chunkList.length,extname: file.name,})// 返回已经上传的const { needUploadChunks } = data;// 已上传,无待上传文件,秒传if (!needUploadChunks.length) {process = 100;return;} // 此处包含了未上传和上传部分的情况// 过滤剩余需要上传的分片序列const remainingChunks = chunkList.filter((item, index) => needUploadChunks.includes(index + 1));// 同步上传进度,断点续传情况下progress = ((chunkList.length - needUploadChunks.length) / chunkList.length) * 100;// 上传if (remainingChunks.length) {const uploadPromiseList = remainingChunks.map(async (chunk, index) => {const response = await uploadChunk(chunk, index + 1, fileHash);//更新进度progress += Math.ceil(100 / allChunkList.length);if (progress >= 100) progress = 100;return response;});Promise.all(uploadPromiseList).then(() => {// 清空已上传的切片chunkList = [];//发送请求,通知后端进行合并axios.post('/api/file/merge', {fileHash,extname: 'fileName.mp4'}, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {console.log('合并完成', res)}).catch(error => {// 合并错误console.log('合并错误', error)})}).catch(error => {// 上传错误console.log('上传错误', error)})}
}

上传函数返回一个promise,参数为formData。

function uploadChunk(chunk, index, fileHash) {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', new Blob([chunk]));// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 文件的标识hash值formData.append('fileHash', fileHash);// 根据项目实际情况return axios.post('/api/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})
}

我们在 fileReader 里面使用了 readAsArrayBuffer 方法做转换并分割,因此传入的chunk的类型是ArrayBuffer,而formData中文件的类型应该是Blob,所以需要时用new Blob() 将每一个chunk转为Blob类型。

七、总结

断点续传的重点是文件的切片与合并,整个上传流程需要前后端配合好,细节较多。

注意事项:

  1. 计算整个文件的 MD5 值,当大文件比较大时会比较慢,耗时,更好地做法是将这部分任务放在 Web Worker 中执行。Web Worker 是 HTML5 标准的一部分,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。这样计算任务就不会影响到当前线程的渲染任务。可以和当前线程间使用 postMessage 的方式进行通讯。

  2. 可以根据文件切片的状态,发送上传请求,由于存在并发限制,需要限制 request 创建个数,避免页面卡死。

  3. 在上传大文件时,应提供适当的进度反馈和错误处理以确保良好的用户体验。

  4. 对于文件切片、并发上传和断点续传,后端需要能够接受文件片段,并能够处理并发请求和断点数据,因此需要合后端人员密切配合。

相关文章:

WEB前端开发中如何实现大文件上传?

大文件上传是个非常普遍的场景&#xff0c;在面试中也会经常被问到&#xff0c;大文件上传的实现思路和流程。在日常开发中&#xff0c;无论是云存储、视频分享平台还是企业级应用&#xff0c;大文件上传都是用户与服务器之间交互的重要环节。随着现代网络应用的日益复杂化&…...

ts给vue中props设置指定类型

interface IBaseObject {[key: string | number]: any; }export default defineComponent({name:xx,props:{data:{type:Object as PropType<IBaseObject>,default:()>({}),required:true},}, })...

模拟实现c++中的list模版

☺☺☺☺☺☺☺☺☺☺ 点击 进入杀马特的主页☺☺​​​​​​​☺​​​​​​​☺​​​​​​​☺​​​​​​​☺​​​​​​​☺​​​​​​​☺​​​​​​​☺​​​​​​​☺ 目录 一list简述&#xff1a; 二库内常用接口函数使用&#xff1a; 1reverse(): 2.s…...

从信息论的角度看微博推荐算法

引言 在数字时代&#xff0c;推荐系统已成为社交媒体和其他在线服务平台的核心组成部分。它们通过分析用户行为和偏好&#xff0c;为用户提供个性化的内容&#xff0c;从而提高用户满意度和平台的参与度。推荐系统不仅能够增强用户体验&#xff0c;还能显著提升广告投放的效率…...

CISC(复杂指令集)与RISC(精简指令集)的区别

RISC(Reduced Instruction Set Computer)和CISC(complex instruction set computer)是当前CPU的两种架构。 它们的区别在于不同的CPU设计理念和方法。 早期的CPU全部是CISC架构&#xff0c;它的设计目的是要用最少的机器语言指令来完成所需的计算任务。比如对于乘法运算&#x…...

自定义数据库连接的艺术:Laravel中配置多数据库连接详解

自定义数据库连接的艺术&#xff1a;Laravel中配置多数据库连接详解 在现代Web应用开发中&#xff0c;经常需要连接到多个数据库。Laravel&#xff0c;作为PHP界最受欢迎的框架之一&#xff0c;提供了强大的数据库抽象层&#xff0c;支持多种数据库系统&#xff0c;并且允许开…...

力扣高频SQL 50题(基础版)第八题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第八题1581. 进店却未进行过交易的顾客题目说明思路分析实现过程准备数据&#xff1a;实现方式&#xff1a;结果截图&#xff1a;总结&#xff1a; 力扣高频SQL 50题&#xff08;基础版&#xff09;第八题 1581. 进店…...

【C++20】从0开始自制协程库

文章目录 参考 很多人对协程的理解就是在用户态线程把CPU对线程的调度复制了一遍&#xff0c;减少了线程的数量&#xff0c;也就是说在一个线程内完成对协程的调度&#xff0c;不需要线程切换导致上下文切换的开销。但是线程切换是CPU行为&#xff0c;就算你的程序只有一个线程…...

Docker 深度解析:从入门到精通

引言 在当今的软件开发领域&#xff0c;容器化技术已经成为一种趋势。Docker 作为容器化技术的代表&#xff0c;以其轻量级、可移植性和易用性&#xff0c;被广泛应用于各种场景。本文将从 Docker 的基本概念入手&#xff0c;详细介绍 Docker 的安装、基本操作、网络配置、数据…...

[C++] 模板编程-02 类模板

一 类模板 template <class T或者typename T> class 类名 { .......... } 1.1 两种不同的实现 在以下的两种实现中,其实第一种叫做成员函数模板&#xff0c;并不能称为类模板因为这种实现,我们在调用时,并不需要实例化为Product这个类指定指定特定类型。 // 实现1 clas…...

嵌入式C++、STM32、树莓派4B、OpenCV、TensorFlow/Keras深度学习:基于边缘计算的实时异常行为识别

1. 项目概述 随着物联网和人工智能技术的发展,智能家居安全系统越来越受到人们的关注。本项目旨在设计并实现一套基于边缘计算的智能家居安全系统,利用STM32微控制器和树莓派等边缘设备,实时分析摄像头数据,识别异常行为(如入侵、跌倒等),并及时发出警报,提高家庭安全性。 系…...

C++ //练习 15.30 编写你自己的Basket类,用它计算上一个练习中交易记录的总价格。

C Primer&#xff08;第5版&#xff09; 练习 15.30 练习 15.30 编写你自己的Basket类&#xff0c;用它计算上一个练习中交易记录的总价格。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块&#xff1a; /********************…...

3个方法快速找回忘记的PDF文件密码

为确保PDF文件的重要信息不轻易外泄&#xff0c;很多人都会给PDF文件设置打开密码&#xff0c;但伴随着时间的推移&#xff0c;让我们忘记了原本设置的密码&#xff0c;但这时&#xff0c;我们又非常急需要打开编辑这份文件&#xff0c;这时我们该怎么办呢&#xff1f;下面小编…...

排序算法:选择排序,golang实现

目录 前言 选择排序 代码示例 1. 算法包 2. 选择排序代码 3. 模拟排序 4. 运行程序 5. 从大到小排序 循环细节 外层循环 内层循环 总结 选择排序的适用场景 1. 数据规模非常小 2. 稳定性不重要 3. 几乎全部数据已排序 4. 教育目的 前言 在实际场景中&#xf…...

【测试】博客系统的测试报告

项目背景 个人博客系统采用了 SSM 框架与 Redis 缓存技术的组合 &#xff0c;为用户提供了一个功能丰富、性能优越的博客平台。 在技术架构上 &#xff0c;SSM 框架确保了系统的稳定性和可扩展性。Spring 负责管理项目的各种组件 &#xff0c;Spring MVC 实现了清晰的请求处理…...

PointCLIP: Point Cloud Understanding by CLIP

Abstract 近年来&#xff0c;基于对比视觉语言预训练(CLIP)的零镜头和少镜头学习在二维视觉识别中表现出了令人鼓舞的效果&#xff0c;该方法在开放词汇设置下学习图像与相应文本的匹配。然而&#xff0c;通过大规模二维图像-文本对预训练的CLIP是否可以推广到三维识别&#x…...

搜索(剪枝)

定义&#xff1a; 剪枝&#xff0c;就是减少搜索树的规模、尽早排除搜索树中不必要分支的一种手段。 在深度优先搜索中&#xff0c;有以下几类常见的剪枝方法: 优化搜索顺序排除等效冗余可行性剪枝最优性剪枝记忆化剪枝 例题1&#xff1a;AcWing 167.木棒 题目&#xff1a;…...

python基础知识点

最近系统温习了一遍python基础语法&#xff0c;把自己不熟知的知识点罗列一遍&#xff0c;便于查阅~~ python教程 Python 基础教程 | 菜鸟教程 1、python标识符 以单下划线开头 _foo 的代表不能直接访问的类属性&#xff0c;需通过类提供的接口进行访问&#xff0c;不能用 f…...

Android SurfaceFlinger——GraphicBuffer获取内存信息(三十一)

上一篇文章介绍了 GraphicBuffer 初始化的 initWithSize() 函数中的申请内存流程,这里我们看一下另一个比较重要的函数,GraphicBufferMapper. getTransportSize 获取内存信息。该函数通常在需要了解缓冲区的实际内存占用情况时调用,例如在调试内存使用情况或优化性能时。 一…...

基于 SASL/SCRAM 让 Kafka 实现动态授权认证

一、说明 在大数据处理和分析中 Apache Kafka 已经成为了一个核心组件。然而在生产环境中部署 Kafka 时&#xff0c;安全性是一个必须要考虑的重要因素。SASL&#xff08;简单认证与安全层&#xff09;和 SCRAM&#xff08;基于密码的认证机制的盐化挑战响应认证机制&#xff…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法

深入浅出&#xff1a;JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中&#xff0c;随机数的生成看似简单&#xff0c;却隐藏着许多玄机。无论是生成密码、加密密钥&#xff0c;还是创建安全令牌&#xff0c;随机数的质量直接关系到系统的安全性。Jav…...

聊聊 Pulsar:Producer 源码解析

一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台&#xff0c;以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中&#xff0c;Producer&#xff08;生产者&#xff09; 是连接客户端应用与消息队列的第一步。生产者…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略

本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装&#xff1b;只需暴露 19530&#xff08;gRPC&#xff09;与 9091&#xff08;HTTP/WebUI&#xff09;两个端口&#xff0c;即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

有限自动机到正规文法转换器v1.0

1 项目简介 这是一个功能强大的有限自动机&#xff08;Finite Automaton, FA&#xff09;到正规文法&#xff08;Regular Grammar&#xff09;转换器&#xff0c;它配备了一个直观且完整的图形用户界面&#xff0c;使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

LRU 缓存机制详解与实现(Java版) + 力扣解决

&#x1f4cc; LRU 缓存机制详解与实现&#xff08;Java版&#xff09; 一、&#x1f4d6; 问题背景 在日常开发中&#xff0c;我们经常会使用 缓存&#xff08;Cache&#xff09; 来提升性能。但由于内存有限&#xff0c;缓存不可能无限增长&#xff0c;于是需要策略决定&am…...

Caliper 配置文件解析:fisco-bcos.json

config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

在树莓派上添加音频输入设备的几种方法

在树莓派上添加音频输入设备可以通过以下步骤完成&#xff0c;具体方法取决于设备类型&#xff08;如USB麦克风、3.5mm接口麦克风或HDMI音频输入&#xff09;。以下是详细指南&#xff1a; 1. 连接音频输入设备 USB麦克风/声卡&#xff1a;直接插入树莓派的USB接口。3.5mm麦克…...

华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)

题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...