news 2026/3/21 14:11:33

vue2框架下如何实现内网大文件的秒传功能?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vue2框架下如何实现内网大文件的秒传功能?

大文件上传方案设计与实现(政府信创环境兼容)

方案背景

作为北京某软件公司的开发人员,我负责为政府客户实现一个兼容主流浏览器和信创国产化环境的大文件上传系统。当前需求是支持4GB左右文件的上传,后端使用PHP,前端使用Vue.js框架。之前尝试的百度WebUploader在国产化环境中存在兼容性问题,因此需要重新设计解决方案。

技术选型分析

方案考虑因素

  1. 国产化兼容:需支持信创环境(如麒麟、UOS等操作系统,飞腾、鲲鹏等CPU架构)
  2. 浏览器兼容:需支持Chrome、Firefox及国产浏览器(如360安全浏览器、红芯等)
  3. 开源合规:必须提供完整源代码供审查
  4. 稳定性:大文件上传的可靠性和断点续传能力
  5. 性能:4GB文件上传的效率和资源占用

最终方案

采用基于分片上传+断点续传的自定义实现,结合以下技术:

  • 前端:Vue.js + 原生HTML5 File API + Axios
  • 后端:PHP(原生或Laravel框架)
  • 分片算法:固定大小分片 + MD5校验
  • 进度管理:Web Worker处理哈希计算

前端实现(Vue组件示例)

1. 安装必要依赖

npminstallspark-md5 axios

2. 大文件上传组件 (FileUploader.vue)

import SparkMD5 from 'spark-md5' import axios from 'axios' export default { name: 'FileUploader', data() { return { file: null, chunkSize: 5 * 1024 * 1024, // 5MB每片 uploadProgress: 0, isUploading: false, isPaused: false, fileHash: '', worker: null, currentChunk: 0, totalChunks: 0, uploadId: '', abortController: null } }, methods: { triggerFileInput() { this.$refs.fileInput.click() }, handleFileChange(e) { const files = e.target.files if (files.length === 0) return this.file = files[0] this.uploadProgress = 0 this.calculateFileHash() }, // 使用Web Worker计算文件哈希(避免主线程阻塞) calculateFileHash() { this.$emit('hash-progress', 0) this.worker = new Worker('/hash-worker.js') this.worker.postMessage({ file: this.file, chunkSize: this.chunkSize }) this.worker.onmessage = (e) => { const { type, data } = e.data if (type === 'progress') { this.$emit('hash-progress', data) } else if (type === 'result') { this.fileHash = data this.totalChunks = Math.ceil(this.file.size / this.chunkSize) this.worker.terminate() } } }, async startUpload() { if (!this.file || !this.fileHash) return this.isUploading = true this.isPaused = false this.currentChunk = 0 // 1. 初始化上传,获取uploadId try { const initRes = await this.request({ url: '/api/upload/init', method: 'post', data: { fileName: this.file.name, fileSize: this.file.size, fileHash: this.fileHash, chunkSize: this.chunkSize } }) this.uploadId = initRes.data.uploadId // 2. 开始分片上传 await this.uploadChunks() // 3. 合并文件 await this.mergeChunks() this.$emit('upload-success', initRes.data) } catch (error) { console.error('上传失败:', error) this.$emit('upload-error', error) } finally { this.isUploading = false } }, async uploadChunks() { return new Promise((resolve, reject) => { const uploadNextChunk = async () => { if (this.currentChunk >= this.totalChunks) { return resolve() } if (this.isPaused) return const start = this.currentChunk * this.chunkSize const end = Math.min(start + this.chunkSize, this.file.size) const chunk = this.file.slice(start, end) const formData = new FormData() formData.append('file', chunk) formData.append('chunkNumber', this.currentChunk) formData.append('totalChunks', this.totalChunks) formData.append('uploadId', this.uploadId) formData.append('fileHash', this.fileHash) try { await this.request({ url: '/api/upload/chunk', method: 'post', data: formData, onUploadProgress: (progressEvent) => { // 计算整体进度 const chunkProgress = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) const totalProgress = Math.round( ((this.currentChunk * 100) + chunkProgress) / this.totalChunks ) this.uploadProgress = totalProgress } }) this.currentChunk++ this.$emit('chunk-uploaded', this.currentChunk) // 使用setTimeout避免堆栈溢出 setTimeout(uploadNextChunk, 0) } catch (error) { reject(error) } } uploadNextChunk() }) }, async mergeChunks() { await this.request({ url: '/api/upload/merge', method: 'post', data: { uploadId: this.uploadId, fileHash: this.fileHash, fileName: this.file.name, chunkSize: this.chunkSize } }) }, pauseUpload() { this.isPaused = true if (this.abortController) { this.abortController.abort() } }, resumeUpload() { this.isPaused = false this.uploadChunks() }, request(config) { // 创建新的AbortController用于取消请求 this.abortController = new AbortController() return axios({ ...config, signal: this.abortController.signal, headers: { ...config.headers, 'Authorization': 'Bearer ' + localStorage.getItem('token') } }).finally(() => { this.abortController = null }) }, formatFileSize(bytes) { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } }, beforeDestroy() { if (this.worker) { this.worker.terminate() } if (this.abortController) { this.abortController.abort() } } } .progress-container { margin-top: 10px; width: 100%; } progress { width: 80%; height: 20px; }

3. Web Worker脚本 (public/hash-worker.js)

// 使用SparkMD5计算文件哈希(在Web Worker中运行)self.importScripts('https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js')self.onmessage=function(e){const{file,chunkSize}=e.dataconstchunks=Math.ceil(file.size/chunkSize)constspark=newSparkMD5.ArrayBuffer()constfileReader=newFileReader()letcurrentChunk=0fileReader.onload=function(e){spark.append(e.target.result)currentChunk++// 报告进度self.postMessage({type:'progress',data:Math.floor((currentChunk/chunks)*100)})if(currentChunk<chunks){loadNextChunk()}else{consthash=spark.end()self.postMessage({type:'result',data:hash})}}functionloadNextChunk(){conststart=currentChunk*chunkSizeconstend=Math.min(start+chunkSize,file.size)fileReader.readAsArrayBuffer(file.slice(start,end))}loadNextChunk()}

后端PHP实现

1. 初始化上传接口

// api/upload/initpublicfunctioninitUpload(Request$request){$data=$request->only(['fileName','fileSize','fileHash','chunkSize']);// 验证参数$validator=Validator::make($data,['fileName'=>'required|string','fileSize'=>'required|integer','fileHash'=>'required|string','chunkSize'=>'required|integer']);if($validator->fails()){returnresponse()->json(['code'=>400,'msg'=>'参数错误']);}// 生成唯一uploadId$uploadId=md5(uniqid());// 创建临时目录$tempDir=storage_path("app/uploads/temp/{$uploadId}");if(!file_exists($tempDir)){mkdir($tempDir,0755,true);}// 保存上传信息(实际项目中应该存入数据库)$uploadInfo=['upload_id'=>$uploadId,'file_name'=>$data['fileName'],'file_size'=>$data['fileSize'],'file_hash'=>$data['fileHash'],'chunk_size'=>$data['chunkSize'],'total_chunks'=>ceil($data['fileSize']/$data['chunkSize']),'uploaded_chunks'=>[],'created_at'=>now()];file_put_contents("{$tempDir}/upload_info.json",json_encode($uploadInfo));returnresponse()->json(['code'=>200,'msg'=>'success','data'=>['uploadId'=>$uploadId,'tempDir'=>$tempDir]]);}

2. 分片上传接口

// api/upload/chunkpublicfunctionuploadChunk(Request$request){$uploadId=$request->input('uploadId');$chunkNumber=$request->input('chunkNumber');$fileHash=$request->input('fileHash');if(!$request->hasFile('file')||!$uploadId||$chunkNumber===null){returnresponse()->json(['code'=>400,'msg'=>'参数错误']);}$tempDir=storage_path("app/uploads/temp/{$uploadId}");if(!file_exists($tempDir)){returnresponse()->json(['code'=>404,'msg'=>'上传会话不存在']);}// 读取上传信息$uploadInfo=json_decode(file_get_contents("{$tempDir}/upload_info.json"),true);// 验证文件哈希if($uploadInfo['file_hash']!==$fileHash){returnresponse()->json(['code'=>400,'msg'=>'文件哈希不匹配']);}// 保存分片$chunkFile=$request->file('file');$chunkPath="{$tempDir}/{$chunkNumber}.part";$chunkFile->move(dirname($chunkPath),basename($chunkPath));// 记录已上传的分片$uploadInfo['uploaded_chunks'][]=$chunkNumber;file_put_contents("{$tempDir}/upload_info.json",json_encode($uploadInfo));returnresponse()->json(['code'=>200,'msg'=>'分片上传成功']);}

3. 合并分片接口

// api/upload/mergepublicfunctionmergeChunks(Request$request){$data=$request->only(['uploadId','fileHash','fileName']);$validator=Validator::make($data,['uploadId'=>'required|string','fileHash'=>'required|string','fileName'=>'required|string']);if($validator->fails()){returnresponse()->json(['code'=>400,'msg'=>'参数错误']);}$tempDir=storage_path("app/uploads/temp/{$data['uploadId']}");if(!file_exists($tempDir)){returnresponse()->json(['code'=>404,'msg'=>'上传会话不存在']);}// 读取上传信息$uploadInfo=json_decode(file_get_contents("{$tempDir}/upload_info.json"),true);// 验证文件哈希if($uploadInfo['file_hash']!==$data['fileHash']){returnresponse()->json(['code'=>400,'msg'=>'文件哈希不匹配']);}// 检查是否所有分片都已上传$totalChunks=$uploadInfo['total_chunks'];$uploadedChunks=$uploadInfo['uploaded_chunks'];if(count($uploadedChunks)!==$totalChunks){returnresponse()->json(['code'=>400,'msg'=>'还有分片未上传完成']);}// 创建最终文件$finalDir=storage_path('app/uploads/final');if(!file_exists($finalDir)){mkdir($finalDir,0755,true);}$finalPath="{$finalDir}/{$data['fileHash']}_{$data['fileName']}";$fp=fopen($finalPath,'wb');// 按顺序合并分片for($i=0;$i<$totalChunks;$i++){$chunkPath="{$tempDir}/{$i}.part";$chunkContent=file_get_contents($chunkPath);fwrite($fp,$chunkContent);unlink($chunkPath);// 删除分片文件}fclose($fp);// 清理临时目录rmdir($tempDir);returnresponse()->json(['code'=>200,'msg'=>'文件合并成功','data'=>['filePath'=>$finalPath,'fileUrl'=>asset("storage/uploads/final/".basename($finalPath))]]);}

国产化环境适配说明

  1. 浏览器兼容

    • 使用原生HTML5 File API,兼容所有现代浏览器
    • 对于不支持的浏览器(如旧版IE),可添加降级提示
  2. 信创环境适配

    • 前端代码不依赖任何特定浏览器API
    • 后端PHP使用原生文件操作函数,无系统相关调用
    • 测试通过:麒麟V10、UOS等国产操作系统+飞腾/鲲鹏CPU环境
  3. 安全考虑

    • 文件哈希验证防止篡改
    • 分片上传避免内存溢出
    • 临时文件及时清理

部署注意事项

  1. PHP配置

    • 确保upload_max_filesizepost_max_size大于分片大小
    • 调整max_execution_time避免超时
  2. Nginx配置(如使用):

    client_max_body_size 100M; client_body_timeout 300s;
  3. 存储路径权限

    • 确保storage/app/uploads目录有写入权限

总结

本方案通过分片上传和断点续传技术,解决了大文件上传的稳定性问题,同时完全满足政府客户的国产化兼容和源代码审查要求。前端采用Vue.js+原生API实现,后端使用纯PHP处理,不依赖任何闭源组件,确保了代码的完全可控性。

将组件复制到项目中

示例中已经包含此目录

引入组件

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de

处理事件

启动测试

启动成功

效果

数据库

下载示例

点击下载完整示例

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/20 18:44:38

2026 年人才战略新趋势:智慧人力系统的数据洞察与预测分析应用

在企业竞争日益聚焦于人才的当下&#xff0c;人才战略的科学性直接影响企业的长远发展。然而&#xff0c;传统人力管理中依赖经验判断的模式&#xff0c;往往导致人才布局与业务需求脱节。智慧人力系统的出现&#xff0c;通过数据洞察与预测分析&#xff0c;为企业人才战略提供…

作者头像 李华
网站建设 2026/3/14 14:25:59

OpenCode+Oh-my-opencode插件(国内友好,免费模型)——筑梦之路

https://blog.csdn.net/qq_34777982/article/details/157651712?spm1011.2124.3001.6209 之前使用ClaudeCode调用本地模型&#xff0c;效果不是太好&#xff0c;试用了下opencode效果还行&#xff0c;比较推荐&#xff0c;这里记录下环境搭建过程。 前置条件 nodejs 22.10…

作者头像 李华
网站建设 2026/3/20 7:28:28

智能设备锁屏密码忘记?手表、电视等官方解决方案

除了手机、平板、电脑&#xff0c;智能手表、智能电视、智能音箱等设备也常设置锁屏/登录密码&#xff0c;忘记后同样无需慌张&#xff0c;各大品牌均有官方解锁方法&#xff0c;操作简单&#xff0c;无需第三方工具&#xff0c;兼顾设备安全和使用便捷。注意&#xff1a;智能设…

作者头像 李华
网站建设 2026/3/15 18:37:15

HTML DOM 访问

HTML DOM 访问 引言 HTML DOM(文档对象模型)是现代Web开发的基础。它允许开发者通过JavaScript与HTML文档进行交互,从而实现丰富的网页功能。本文将深入探讨HTML DOM的访问方法,帮助开发者更好地理解和运用DOM。 什么是HTML DOM HTML DOM是一种将HTML文档表示为树形结构…

作者头像 李华
网站建设 2026/3/17 23:13:33

2026健身器材出海新思路:海外红人营销如何提前完成心智占位

健身器材的购买&#xff0c;很少始于“我现在要买一台器械”。更多时候&#xff0c;它来自更早期、也更模糊的心理触发点——对健康状态的隐性焦虑、对身材变化的自我察觉、对居家空间功能性的重新审视。这些触发并不直接指向某个产品&#xff0c;而是以情绪、状态或生活场景的…

作者头像 李华