
1. 信创环境下的上传需求比你想的复杂做信创项目的人都有体会表面上是个上传功能实际牵一发动全身。浏览器从Chrome换成了国产化套件服务器从CentOS换成了麒麟或统信数据库从Oracle换成了人大金仓或达梦连JDK都可能是毕昇或龙芯的。在这种环境下做一个大文件上传Demo很多人第一反应是“不就是个上传嘛”结果一上手就发现处处碰壁。这个Demo的核心诉求是在信创技术栈下用Vue前端实现一个能稳定处理GB级别文件的上传功能。它不是一个简单的input typefile加axios.post而是涉及文件切片、并发控制、断点续传、服务端合并、国产化适配等多个环节。适合正在做信创适配改造的团队、需要投标演示的供应商以及刚接手信创项目的前端新手参考。我最初做这个Demo时踩了不少坑。最典型的是用常规上传方式发一个2GB的文件浏览器直接卡死服务器抛出连接超时。也试过引入某个开源的OSS直传SDK结果在国产浏览器上运行时报错——因为底层用了浏览器不支持的API。这些问题的根源在于信创环境的浏览器内核版本普遍低于主流ChromeNode层的基础库版本也偏旧很多“现代前端方案”在信创环境下根本跑不动。所以这篇文章我会从技术选型、前端实现、服务端处理、问题排查四个维度完整拆解这个Demo从零到落地的全过程。不绕弯子直接上干货。2. 整体设计思路为什么必须走“切片并发”这条路2.1 常规上传为什么撑不住大文件很多人在非信创环境用普通上传也没出过大问题因为Chrome对HTTP请求体大小没有硬性限制大部分后台服务默认也能接收几百MB的请求。但超过1GB的单个请求问题就来了浏览器内存占用飙升。读取文件到FormData时文件二进制会进入内存2GB的文件直接吃掉2GB内存轻则卡顿重则崩溃。网络不稳定时整个文件重传浪费带宽不说用户体验极差。服务器在长时间接收大请求时容易被网关拦截Nginx默认client_max_body_size为1MB需要单独调也容易因中间件超时断连。信创环境下这些问题被放大了。国产服务器上的Nginx版本、网关中间件配置往往和标准发行版有差异很多默认参数偏保守出问题的概率远高于普通环境。2.2 “切片并发合并”三步走的核心逻辑大文件上传的标准解法就是切片。把一个500MB的文件按固定大小切成几十个块逐个上传。这么做有几个直接收益单个请求体积小通常1MB到20MB不会触发网关限制也不会把浏览器内存打满。某个分片失败时只需要重传那一片不需要整个文件重来。支持并发上传。浏览器对同一域名的并发连接数有限制HTTP/1.1一般是6个但我们可以控制为3到5个并发在带宽允许的情况下显著提速。切片的技术实现很简单File.prototype.slice()方法。TypeScript里是file.slice(start, end)返回一个Blob对象这个Blob可以直接作为FormData的二进制字段提交。前端只需要记录每个分片的索引和大小传给后端合并即可。合并动作放在服务端完成。前端把分片全部传完后通知后端“合片”后端按索引顺序读取分片文件顺序写入最终文件。2.3 信创环境的三个特殊约束通用的大文件上传方案网上很多但直接抄作业会翻车原因在于信创环境有三个绕不开的约束第一个约束是浏览器兼容性。信创终端上常用的国产浏览器如360安全浏览器、奇安信浏览器、红莲花浏览器内核版本通常停留在Chromium 70到85之间。这个范围内的浏览器对Web Worker、Blob.slice()等现代API的支持基本没问题但对某些新的语法特性比如可选链?.、空值合并??支持不完整。打包时要把Babel的target设置得保守一些。第二个约束是服务器资源有限。信创服务器往往是基于ARM或龙芯架构的性能方面与x86服务器存在差距。如果每个分片都落盘合并时再读出来IO压力会比较大。我建议把分片大小从常见的2MB调大到8MB甚至10MB减少分片数量降低IO次数。第三个约束是对外部SDK的依赖要谨慎。很多大文件上传方案直接集成阿里云OSS、腾讯云COS的SDK但信创环境是企业内网充分考虑了数据安全因素不可能把文件传到公有云。所以Demo必须走自建后端存储不能依赖任何公有云SDK。注意信创项目中的数据合规要求通常很严格。不能因为图省事在Demo里调用任何非授权的第三方存储服务。自建后端是唯一合规路线。3. 前端实现Vue 3 TypeScript Worker组合方案3.1 项目初始化和依赖选型Demo的前端部分我用的是Vue 3组合式API Vite TypeScript。Vue 2虽然在信创项目里存量很大但新做的Demo没必要再踩旧坑Vue 3的响应式系统和组合式API在上传场景下写起来更顺。不上状态管理库Pinia/Vuex这个场景用不上反而增加概念负担。需要明确一点Vite打包后的产物对旧内核浏览器的兼容性不如Webpack。如果目标浏览器是Chromium 70以下的极老内核建议改用Webpack并配置babel/preset-env。如果目标是Centerm Kylin自带的浏览器或更新版本的国产浏览器Vite的默认构建目标调到es2018基本够用。// vite.config.ts export default defineConfig({ build: { target: es2018, // 兼容国产浏览器 }, server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, });为什么这里强调target: es2018因为打包后的代码如果包含ES2020以上的语法比如Promise.allSettled、可选链老内核浏览器会直接语法报错页面白屏。es2018对应的语法兼容性相对安全。3.2 文件选择与元数据采集上传的第一步是让用户选择文件并展示基本信息。这里除了文件名和大小还要算出一个唯一标识通常是文件的MD5值用来做断点续传的索引。input typefile changeonFileChange accept*/* async function onFileChange(e: Event) { const input e.target as HTMLInputElement; if (!input.files?.length) return; const file input.files[0]; state.file file; state.fileMeta { name: file.name, size: file.size, totalChunks: Math.ceil(file.size / CHUNK_SIZE), // hash先置空在Worker里计算 hash: , }; }计算文件MD5是整个流程里最耗时的一步。如果文件是1GB在主线程里做哈希计算页面直接卡死——因为渲染进程被阻塞了。解法是把计算放到Web Worker里。3.3 用Web Worker做MD5计算与切片预处理Worker的主要职责是两个计算文件哈希以及在计算过程中同步做切片。这里有一个很实用的设计不需要先算完哈希再重新读文件做切片而是在Worker里边读边切读到的数据块累加进SparkMD5的哈希算法中。文件读一遍哈希和切片都完成了。// worker.ts /// reference libwebworker / const ctx self as unknown as Worker; const CHUNK_SIZE 10 * 1024 * 1024; // 10MB ctx.onmessage async (e: MessageEvent) { const { file, chunkSize, type } e.data; if (type hash) { // 计算MD5并切片 const chunks: Blob[] []; const spark new SparkMD5.ArrayBuffer(); const reader new FileReader(); for (let i 0; i file.size; i chunkSize) { const blob file.slice(i, Math.min(i chunkSize, file.size)); const buf await readAsArrayBuffer(reader, blob); spark.append(buf); chunks.push(blob); const percent Math.min(100, Math.round(i / file.size * 100)); ctx.postMessage({ type: hashProgress, percent }); } const hash spark.end(); ctx.postMessage({ type: hashDone, hash, chunks }); } }; function readAsArrayBuffer(reader: FileReader, blob: Blob): PromiseArrayBuffer { return new Promise((resolve, reject) { reader.onload () resolve(reader.result as ArrayBuffer); reader.onerror reject; reader.readAsArrayBuffer(blob); }); }主线程里创建一个Worker实例接收消息并更新界面状态const worker new Worker(new URL(./worker.ts, import.meta.url)); worker.onmessage (e: MessageEvent) { const { type, percent, hash, chunks } e.data; if (type hashProgress) { state.hashing true; state.hashProgress percent; } else if (type hashDone) { state.hashing false; state.fileMeta.hash hash; state.chunks chunks; uploadChunks(); // 进入上传阶段 } };这里有个信创环境下特别容易踩的坑部分国产浏览器的Worker加载方式与标准不一致特别是从file协议访问页面时new Worker(new URL(./worker.ts, import.meta.url))可能无法正确加载。项目需要确保实际部署时通过HTTP/HTTPS协议访问页面不能直接双击HTML文件。我在一次演示中直接双击页面文件结果Worker一直不触发排查了半天才发现是协议问题。这个细节在和后端联调时尤其坑因为后端接口能通但哈希计算始终不完成。3.4 分片上传与并发控制分片上传不能所有分片同时发不然带宽打满后反而变慢更会导致服务器并发连接过多、内存溢出。我用一个简单的并发池来控制。async function uploadChunks() { const chunks state.chunks; const total chunks.length; let uploaded 0; let active 0; const MAX_CONCURRENT 5; return new Promise((resolve, reject) { for (let i 0; i Math.min(MAX_CONCURRENT, total); i) { startOne(i); } async function startOne(index: number) { if (index total) return; active; const chunk chunks[index]; const formData new FormData(); formData.append(file, chunk, ${state.fileMeta.hash}-${index}); formData.append(index, String(index)); formData.append(hash, state.fileMeta.hash); formData.append(totalChunks, String(total)); formData.append(fileName, state.fileMeta.name); try { await request(/api/upload, formData); uploaded; state.uploadProgress Math.round(uploaded / total * 100); startOne(active index); // 补充一个新任务 } catch (err) { // 失败分片重试一次仍失败则中断 try { await request(/api/upload, formData); uploaded; } catch (err2) { reject(err2); return; } } active--; if (uploaded total) { resolve(true); } } }); }这里的request函数不能直接用axios的默认配置需要设置很长的超时时间function request(url: string, formData: FormData, timeout 120000) { return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(POST, url, true); xhr.timeout timeout; xhr.onload () { if (xhr.status 200) resolve(xhr.response); else reject(new Error(HTTP ${xhr.status})); }; xhr.onerror () reject(new Error(network error)); xhr.ontimeout () reject(new Error(timeout)); xhr.send(formData); }); }为什么不直接用axios不是不能用但axios在国产浏览器上的兼容性问题并不比XHR低而且XHR API更底层、可控性更强。大文件上传场景下我们需要精确掌握每个分片的超时和重试逻辑直接用XHR写反而更稳。另外XHR天然支持upload.onprogress事件可以用来做上传进度的精细展示。分段并发数选5的选择依据是信创服务器的并发处理能力偏弱5个并发连接在大多数千兆内网环境下能跑满带宽又不会压垮服务器。如果文件特别大10GB以上可以调到8但需要观察服务器的CPU和内存占用。实操心得并发数一定要做成可配置的不要让用户填而是放在.env文件里。信创环境网络差异极大有的现场千兆内网有的现场百兆都不到。我在给现场调优时通常先把并发数提到8试跑如果服务器IO明显增高就降回5。3.5 进度反馈与UI呈现用户界面上需要反馈三个维度的信息当前阶段计算哈希中/上传中/合并中/已完成上传百分比已完成分片数/总分片数单个分片的状态排队中/传输中/已完成/失败进度条用Vue的计算属性实时驱动div classprogress-wrapper div classprogress-bar :style{ width: overallPercent % }/div span{{ overallPercent }}%/span /divconst overallPercent computed(() { if (state.hashing) return Math.round(state.hashProgress / 2); if (state.uploading) return Math.round(50 state.uploadProgress / 2); if (state.merging) return 99; return 100; });这里把哈希阶段占比50%、上传阶段占比50%避免用户等待哈希计算时觉得进度卡住了。4. 后端处理Spring Boot接收切片与合并实现4.1 后端技术栈选型Demo的后端用的是Spring Boot。原因很简单信创项目里Java系后端占比最高。如果项目现场是.NET或者Go代码逻辑也可以平移但Spring Boot在信创适配方面的成熟度高和国产数据库、国产中间件东方通TongWeb的兼容案例更多。后端项目需要的最小依赖只有两个spring-boot-starter-web和spring-boot-starter-validation。4.2 分片上传接收接口分片上传接口负责接收单片数据。接收后写入磁盘的临时目录文件名用{hash}_{index}格式方便后续合并。RestController RequestMapping(/api) public class UploadController { Value(${upload.tmp-dir:/data/upload/tmp}) private String tmpDir; Value(${upload.merge-dir:/data/upload/merge}) private String mergeDir; PostMapping(/upload) public Result receiveChunk( RequestParam(file) MultipartFile file, RequestParam(index) int index, RequestParam(hash) String hash, RequestParam(totalChunks) int totalChunks, RequestParam(fileName) String fileName) throws IOException { String chunkFileName hash _ index; File chunkFile new File(tmpDir, chunkFileName); if (!chunkFile.getParentFile().exists()) { chunkFile.getParentFile().mkdirs(); } file.transferTo(chunkFile); return Result.success(); } }关键点一hash字段是整个文件级的唯一标识如果两个用户上传同名文件用fileName区分没问题但判断“这个文件是否已经上传过”必须用hash因为同名文件可能内容不同。关键点二file.transferTo()并不保证完整写入。在信创服务器上如果磁盘空间不足或分区写满这个方法可能抛出异常。建议在写入后检查文件大小if (chunkFile.length() ! file.getSize()) { chunkFile.delete(); throw new RuntimeException(chunk size mismatch); }这个检查每片都会做一次开销可以忽略但能避免很多磁盘半满导致的不明bug。4.3 合并接口与断点续传逻辑所有分片上传完成后前端调用合并接口PostMapping(/merge) public Result merge(RequestParam(hash) String hash, RequestParam(fileName) String fileName, RequestParam(totalChunks) int totalChunks) throws IOException { File mergeFile new File(mergeDir, fileName); if (mergeFile.exists()) { return Result.success(file already exists); } FileOutputStream out new FileOutputStream(mergeFile); for (int i 0; i totalChunks; i) { File chunkFile new File(tmpDir, hash _ i); if (!chunkFile.exists()) { out.close(); mergeFile.delete(); throw new RuntimeException(missing chunk: i); } Files.copy(chunkFile.toPath(), out); chunkFile.delete(); // 合并后删除切片释放磁盘 } out.close(); // 清理临时目录中残留切片 File[] leftovers tmpDir.listFiles((dir, name) - name.startsWith(hash)); if (leftovers ! null) { for (File f : leftovers) { f.delete(); } } return Result.success(merge done); }合并完后要不要校验MD5建议校验。前端计算好的MD5值在后端重新计算一次最终文件的MD5如果一致说明合并过程无损坏。MessageDigest md MessageDigest.getInstance(MD5); try (InputStream is new FileInputStream(mergeFile)) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { md.update(buffer, 0, len); } } String finalMd5 toHex(md.digest()); if (!hash.equalsIgnoreCase(finalMd5)) { mergeFile.delete(); throw new RuntimeException(md5 mismatch); }这一步不要省。合并过程如果出现分片顺序错乱、切片缺失MD5校验是最后一道防线。4.4 断点续传怎么落地断点续传的本质是“跳过已经上传的分片”。前端在拿到文件hash后调用查询接口带上hash参数后端返回该hash下已存在的分片索引集合。GetMapping(/uploaded-chunks) public ResultListInteger uploadedChunks(RequestParam(hash) String hash) { ListInteger uploaded new ArrayList(); File dir new File(tmpDir); File[] files dir.listFiles((d, name) - name.startsWith(hash _)); if (files ! null) { for (File f : files) { String numStr f.getName().substring((hash _).length()); uploaded.add(Integer.parseInt(numStr)); } } return Result.success(uploaded); }前端在上传前先调用这个接口过滤掉已存在的分片只传缺失的。这样刷新页面或者网络中断后重新上传不需要重传整个文件体验提升明显。5. 常见问题与排查技巧实录5.1 国产浏览器下页面白屏或控制台报语法错误现象页面打开后白屏F12控制台报SyntaxError: Unexpected token .之类的错误。原因打包产物中包含了目标浏览器不支持的ES新语法通常是在某些依赖的代码里而不是你自己的代码。排查思路打开控制台看报错的具体文件如果报错在assets/index-xxx.js里大概率是构建目标问题。检查vite.config.ts的build.target配置不要低于es2018。如果项目用了Vue 3.5版本它的SFC编译产物对浏览器要求稍高需要留意Vue版本。解决将build.target设为es2018如果仍报错可以改用Webpack babel/preset-env并设置browserslist为Chrome 70。5.2 Web Worker 无法创建或消息不返回现象页面状态一直停留在“计算哈希中”控制台无报错。原因排查页面是否以file://协议访问。如果是Worker加载会被浏览器安全策略拦截。验证码或路由守卫是否拦截了Worker请求。某些国产浏览器的安全策略较严对Worker脚本执行了同源策略之外的限制。Worker脚本中是否有未捕获异常。在Worker内加try/catch并手动postMessage({type:workerError, error})能快速定位。解决部署到HTTP环境后再测必要时将Worker逻辑内联到主线程中牺牲一部分性能换兼容性。5.3 大文件上传到一半提示“无法连接到服务器”现象分片上传过程中突然全部请求失败界面显示网络错误。排查方向查看服务器Nginx或网关的超时配置proxy_read_timeout。查看服务器的/var/log目录下的错误日志。检查信创服务器防火墙是否中断了长连接。解决如果是Nginx超时增大proxy_read_timeout至300s以上如果是防火墙断连调整keepalive参数。这个问题的根源通常不在代码但排查起来最费时。5.4 合并时提示“missing chunk”现象前端明明上报所有分片都成功但合并接口报缺少第N片。原因最常发生在磁盘空间不足时某片写入虽然接口返回成功但文件实际未落盘写入缓冲区但同步失败。或者上传过程中页面被用户关闭前端误判所有分片完成。解决合并前先校验所有分片文件是否存在且大小非0后端在接收分片时立即检查file.getSize()大于0。关键要提醒用户上传过程中不要关闭标签页并在前端发起合并前做一次后端确认。5.5 上传速度远低于预期场景100MB的文件上传需要十几分钟传到怀疑人生。原因分析分片数太多导致请求频繁、HTTP开销过大。2MB分片500MB文件就是250个请求在信创服务器上每个请求的握手开销都不小。并发数不够带宽利用率低。服务器上磁盘IO差切片写入慢。解决将分片大小从2MB调大到10MB分片数量立减80%。同时确认并发控制在5左右。实测中10MB分片5并发在信创服务器上100MB文件大约3分钟左右传完同条件下2MB分片3并发需要8分钟左右。5.6 信创数据库存文件元数据时字符集报错现象上报文件名时如果包含中文或特殊字符数据库插入报错或者显示乱码。原因国产数据库的默认字符集与MySQL的utf8mb4不完全一致或JDBC连接串没配置字符集。解决在JDBC连接串中追加characterEncodingutf8并在建表时指定字段编码。这个坑主要是配置问题和代码关系不大但排查起来容易走弯路。6. 实际部署形态与扩展方向Demo做完之后我们最终以三个微服务的形式落地Vue前端Nginx托管、Spring Boot上传服务、信创数据库实例。前端通过/api反向代理访问上传服务数据库仅存储文件元数据如文件名、大小、MD5、上传时间、所属人。文件本体与元数据分离存储既方便备份也方便未来对接对象存储。几个后续扩展方向供你参考断点续传的状态管理。Demo里的断点续传依赖服务端查分片是否存在如果访问量大每次查询扫描目录会慢。可以加一层Redis缓存key是hashvalue是已上传分片索引集合。注意信创环境可能没有Redis可以用国产化的版本替换。大文件的安全校验。目前只做了MD5校验如果文件涉及机密信息建议在传输过程中启用加密通道避免明文传输。Demo里HTTP改为HTTPS后Worker加载、XHR上传都不需要改代码。多用户隔离。Demo没有区分用户实际项目中必须加上用户ID否则A用户上传的分片会被B用户的合并请求用掉。最简单的做法是hash字段改为userId_md5hash的拼接形式。文件秒传。当服务端已存在相同hash的文件时直接返回“上传完成”前端的秒传体验就是这样实现的。这个功能在信创环境下很容易做因为内网里重复文件比例其实不低。7. 写在最后的经验总结做完这个Demo我最大的体会是信创环境下的前端开发难点不在于“实现什么功能”而在于“用什么方式实现才能不翻车”。大文件上传本身是成熟方案切片、并发、合并这些技术早就有公开实践但放在国产化技术栈里每一个环节都要重新验证一遍兼容性。我个人建议如果你是在公司做信创项目的投标演示这个Demo做到断点续传和进度展示就够用了不必追求过度设计。如果是要长期交付生产使用后端存储这一层要尽早考虑替代方案因为文件量上来之后本地磁盘目录的管理会非常痛苦届时需要一个真正的对象存储服务来解耦。最后一个小技巧联调时不要用太小的测试文件至少准备一个200MB以上的真实文件。很多问题比如网关超时、并发崩溃、合并异常在小文件时根本不出现等到现场用大文件演示时才爆出来那时就来不及了。这个习惯帮我避开了不少尴尬场面分享给你。