1. 项目背景:BIM模型文件上传到底难在哪
1.1 BIM文件的体积和结构决定了普通上传不可行
做工程行业信息化的人,十有八九都躲不过上传BIM模型文件这个需求。我最近接手一个内部项目,某工程集团要在 Web 端做设计模型的在线归档和校审,最核心的功能就是把 RVT、IFC 这类动辄大几百 MB、甚至几个 GB 的 BIM 模型文件从浏览器传到服务器。
BIM 模型文件有几个特点,决定了它不能按普通文档来对待。
第一是体积大。一个中等复杂程度的建筑信息模型,RVT 格式轻松超过 500MB;带材质贴图、点云扫描数据的模型,2GB 以上很常见。普通上传组件动辄把整个文件塞进一次 HTTP 请求里,服务器端要缓冲完整文件,网关和代理层又有请求体大小限制,直接干到超时或内存溢出。
第二是结构复杂。BIM 文件不是简单的文本或图片,里面嵌套了构件属性、几何网格、材质信息、关联关系。任何一个字节在传输过程中出错,都可能导致整个模型在 Revit、Navisworks 里打不开或者构件丢失。这就引出一个关键问题——上传过程不能只保证“传完”,必须保证“传对”。
第三是网络环境不理想。国企项目的用户遍布各个项目部,很多人用的是内网办公系统,带宽不稳定,代理层多,长连接容易被掐断。如果一次上传失败就要全部重来,别说用户受不了,项目经理第一个找你谈话。
传统方案里,FTP 或者共享盘能传大文件,但没法在 Web 端做统一管理;直接调整 Web 服务器的上传大小限制,又会把内存和超时参数调得极其夸张,治标不治本。所以,分片上传几乎是唯一合理的选择。
1.2 为什么选 WebUploader 而不是新出的上传组件
网络上现在有大把新的上传组件,功能花哨,文档也漂亮,但我在这种企业级项目里还是选了百度开源的 WebUploader 组件,理由其实挺朴素。
第一是稳定性。WebUploader 在 2015 年前后就大量应用于各种 OA、网盘、项目管理平台,踩过的坑都被前人填得差不多了。它支持 HTML5 和 Flash 双模式,老旧的 IE 浏览器环境也能降级干活。国企办公环境里,总有几个会议室老电脑还是老浏览器,这种兼容性存量很重要。
第二是 API 设计清晰。WebUploader 提供分片上传、并发控制、进度回调、文件队列等一套完整的能力,核心概念不复杂:把文件切成指定大小的分片,逐个或并发上传,最后由服务端合成。对于我们自己要做的校验逻辑,它有钩子可以插入自定义逻辑,不冲突。
第三是维护成本低。虽然 WebUploader 官方早就停止了大版本更新,但这个项目本身就足够简单稳定,核心代码就那么几个模块,出了问题自己看一眼源码就能定位。相比那些依赖 Node 生态、需要构建链路的组件,在无法随意使用公网依赖的内网部署环境里,WebUploader 只需要引入几个静态 JS 文件就能跑,非常干净。
当然,新组件我也不是没对比过。像一些基于 XMLHttpRequest 的分片上传方案,思路很先进,但要么需要 ES6+ 环境,要么依赖第三方存储接口,要么文档里全是宣传话术,真正到老项目里落地成本高。技术选型未必追新,能让业务稳定跑起来才是第一位。
2. 总体方案:分片上传叠加四层校验
2.1 整体流程设计
项目需求一句话能概括:Web 端把大型 BIM 模型文件可靠地传到服务端,过程中保证文件不损坏、可断点、可秒传。
我把整体方案拆成三个阶段,前端、校验、服务端各司其职。
第一个阶段是“上传前准备”。用户选择文件后,前端立即计算文件全文 MD5 值。这个值有两个用途:一是作为文件唯一标识,传给服务端判断是否已存在,实现秒传;二是作为最终合并完成后的比对基准,保证文件完整。
第二个阶段是“分片上传”。前端用 WebUploader 把文件切成固定大小的块,每个分片携带自己的 MD5 值、分片序号、总分片数和文件全文 MD5 值,按配置的并发数上传到服务端。服务端每收到一个分片,先校验该分片的 MD5 是否匹配,匹配才落盘。
第三个阶段是“合并与终验”。所有分片上传完成后,前端触发合并接口,服务端按分片序号把临时文件拼接成完整模型文件,然后再对整个文件计算一次 MD5,与前端最初提交的全文 MD5 进行比对。一致了,才算是上传成功。
这里我故意设计成四层校验:
- 第 1 层:文件扩展名白名单校验,只允许 .rvt、.ifc、.dwg、.3dm 等已知 BIM 格式。
- 第 2 层:前端计算全文 MD5,上传发起时提交给服务端备案。
- 第 3 层:每个分片独自校验 MD5,防止单个分片在传输中被篡改或损坏。
- 第 4 层:合并后的完整文件 MD5 比对,确保分片顺序、数量、拼接逻辑全部正确。
为什么要在“每个分片校验”之外再做“全文比对”这一层?这是我吃过亏的地方。分片各自正确,不代表合并后的整体正确,因为如果服务端落盘时某个分片重复覆盖了另一个分片,或者合并时排序逻辑错了,单看每个分片的 MD5 都没问题,只有最终文件整体校验才会暴露问题。所以在传输这种精密的工程模型文件时,少一层的信任感都不够。
2.2 关键的参数设计:分片大小、并发数、重试策略
这些参数我一开始也是照搬默认值,后来在真实网络环境里反复调整,才找到适合 BIM 大文件的组合。
WebUploader 默认分片大小是 4MB,但对 BIM 文件来说太小了。一个 2GB 的文件,4MB 分片意味着要上传 512 个分片,每个分片都是一次 HTTP 请求。请求数量太多,一方面服务端要处理的连接数激增,另一方面网络往返时间在总耗时里的占比变高,得不偿失。
我最终把分片大小设为 20MB,大文件也可以继续调大到 50MB。这样 2GB 的文件只需要 40~100 次请求,即使用户是在跨省专线上传,整体往返开销也可控。有人会担心分片大了,断点续传的粒度就粗了,网络波动时重传的数据多。这个矛盾确实存在,但实践中 20MB 是容量、性能和体验之间比较均衡的点。
并发数我也做了控制。WebUploader 默认并发 3 个线程,我调研后发现内网环境的带宽是稳定但有限的,并发开太高没什么用,反而会给老旧的服务器和网络代理带来压力。我的做法是并发数固定为 5,同一时间最多 5 个分片在传,既能让带宽跑满,又不至于把服务器连接数打爆。
重试策略同样重要。WebUploader 提供了 chunkRetry 参数,可以指定上传失败时的自动重试次数。我设为 3 次,重试间隔随次数递增,且重试时会重新计算该分片的 MD5,确保重传的数据也是新的。服务端则要做幂等处理,同一个分片重复上传时不报错,直接返回成功。
注意:不要单纯依赖组件自带的重试,服务端一定要做幂等校验。否则客户端重试上传同一个分片,而服务端已经保存了对应数据,可能出现同一分片被写入两次或者覆盖顺序错乱的问题。
3. 前端落地:基于 WebUploader 的初始化与校验实现
3.1 Uploader 的核心配置
前端部分我直接用原生 JavaScript 加 WebUploader 的静态文件,没有引入额外框架。初始化配置是核心,直接贴一份我项目里的精简版本。
// 使用WebUploader初始化上传器 var uploader = new WebUploader.Uploader({ // 选择按钮的容器选择器 pick: '#uploaderPicker', // 配合服务端的接受接口地址 server: '/api/bim/upload/chunk', // 允许选择文件的后缀名,做第一层过滤 accept: { title: 'BIM模型文件', extensions: 'rvt,ifc,dwg,3dm,skp', mimeTypes: '*.*' }, // 开启分片 chunked: true, // 分片大小:20MB chunkSize: 20 * 1024 * 1024, // 并发数:5 threads: 5, // 失败自动重试次数 chunkRetry: 3, // 上传控件内部不做md5计算,我们手动控制 md5: false, // 不自动上传,等我们执行uploader.upload()再开始 auto: false, // 多个文件排队上传,避免同时开多个大文件 fileNumLimit: 1, fileSizeLimit: 5 * 1024 * 1024 * 1024, // 单个最大5GB // 每个分片请求时携带的自定义参数 formData: { // 这里会在每个分片请求时带上,由uploader的sendMD5机制补充 source: 'bim-archiving' }, method: 'POST', // 是否压缩图片,对于模型文件必须关闭 compress: false });有几个配置项我需要特别说明一下。
compress必须设为 false。WebUploader 默认对图片类文件会做压缩处理,虽然我们传的是模型文件不影响,但保不齐某个用户选中的文件后缀在黑白名单里绕过去了,万一被它的内部逻辑识别成图片,那文件就被破坏掉了。直接关掉最稳妥。
fileNumLimit我设为 1,因为 BIM 合并归档场景里一次只处理一个模型文件是常态,这样也避免多个大文件并发带来的内存和带宽压力。
chunkSize前面说了用 20MB。注意单位是字节,所以20 * 1024 * 1024。有同事一开始写成20 * 1000 * 1000,也没报错,但分片大小和预期不一致,排查了半天。
3.2 增量 MD5 计算:两个关键细节
WebUploader 自带 md5 计算,内部使用 SparkMD5,它可以在分片上传过程中每传完一个分片就增量计算全文 hash。但我在实际项目里遇到一个麻烦:对于 2GB 以上的文件,这个增量计算过程仍然会在某些机型上把页面卡住,UI 线程长时间无响应。
于是我把 MD5 的计算和上传彻底分开,在上传前单独完成。这里有两个细节很关键。
第一个细节是分块读取文件计算 MD5。不要用FileReader一次性把文件读进内存,那对于 1GB 以上文件直接会导致浏览器崩溃或系统内存暴涨。应该按固定大小分块读取,每读一块就把 ArrayBuffer 交给 SparkMD5 的增量实例,读完后拿到最终 hash。
// 使用SparkMD5增量计算文件全文MD5 function calculateFileMd5(file) { return new Promise(function(resolve, reject) { var chunkSize = 4 * 1024 * 1024; // 读块大小4MB,与上传分片无关 var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; var chunks = Math.ceil(file.size / chunkSize); var currentChunk = 0; var spark = new SparkMD5.ArrayBuffer(); // 用于读取分块的FileReader var fileReader = new FileReader(); fileReader.onload = function(e) { spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve(spark.end()); } }; fileReader.onerror = function(e) { reject(e); }; function loadNext() { var start = currentChunk * chunkSize; var end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }); }这个计算过程对于 2GB 的文件,在现代浏览器上大约需要 5 到 15 秒,期间浏览器还是有短暂卡顿,所以我在 UI 上做了一个“正在计算校验值”的 loading 状态,避免用户误以为页面死了。
第二个细节是分片上传时把全文 MD5 一同提交。服务端需要这个值来做分片归属判断。我是用formData动态追加的:
uploader.on('uploadBeforeSend', function(file, data, header) { // 把全文MD5和当前分片的序号附加到请求参数中 data.fileMd5 = globalFileMd5; data.chunk = file.uploadedChunk; // 当前分片序号 data.chunks = file.uploadedChunkTotal; // 总分片数 });有些项目还会在每个分片请求里带上该分片自己的 MD5 值,这需要file.source的二进制数据算一次局部 hash。因为读取分片和上传之间有时间差,完整实现会复杂一些,但为了模型文件的绝对可靠,我建议加上。做法是监听chunkBeforeSend,在发请求前对分片二进制算一遍 MD5,追加到 data 里。
3.3 秒传和断点续传的落地方式
秒传和断点续传的核心都在服务端能不能给出准确的“已存在判定”,前端实现反而简单。
秒传的逻辑是:用户选中文件后,前端计算出全文 MD5,调用一个检查接口把 MD5 发给服务端。服务端查文件记录表,如果发现同一个 MD5 的文件已经存在,直接返回“已存在”,前端就提示用户“文件已在系统中”,并且不进入上传流程。这个功能对于设计院反复上传同一份模型做版本校审的场景非常有用,省下的带宽相当可观。
断点续传的逻辑类似:用户之前上传过一半,因为断网或关闭浏览器中断了,再次选中同一文件时,前端还是先算 MD5,调用检查接口。服务端除了返回“不存在完整文件”外,还要返回“已上传分片序号列表”,比如[1, 2, 3, 5, 6, 7]。前端拿到这个列表后,通知 WebUploader 跳过这些分片:
uploader.on('beforeFileQueued', function(file) { // 先在这触发check,拿到结果后决定是否继续 }); uploader.on('uploadBeforeSend', function(file, data) { var chunkIndex = file.uploadedChunk; // 如果这个分片序号已经在服务端已上传列表中,则中断该分片 if (uploadedChunkIndices.indexOf(chunkIndex) !== -1) { // 手动跳过:返回false阻止此次请求 return false; } });这里有个经验,服务端返回的“已上传分片列表”不能只按分片序号判断,还要验证分片文件的实际大小是否为 20MB。因为在极端场景下,上一次上传的分片可能是 0 字节的空文件或者半个分片,服务端还没来得及校验就记录了序号。如果前端盲目信任这个列表,就会把坏分片跳过,最后合并出损坏的模型文件。我的做法是,服务端在返回列表之前,先对临时目录里的每个分片做大小和 MD5 校验,通过才加入列表。
4. 后端落地:Java 服务端的分片接收与合并
4.1 接口设计:接收、检查、合并三分开
我负责的服务端用的是 Java Spring Boot,实际上用 C# 或者 Python 实现思路完全一样。接口我设计成三个:检查接口、分片接收接口、合并接口。
检查接口POST /api/bim/upload/check,参数是文件 MD5。
分片接收接口POST /api/bim/upload/chunk,接收 MultipartFile 和一堆元数据参数。
合并接口POST /api/bim/upload/merge,参数是文件 MD5 和总分片数。
分片接收接口的 Controller 代码大概长这样:
@PostMapping("/api/bim/upload/chunk") public Result uploadChunk( @RequestParam("file") MultipartFile chunkFile, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") Integer chunkIndex, @RequestParam("chunks") Integer totalChunks, @RequestParam(value = "chunkMd5", required = false) String chunkMd5) { // 1. 基本参数检查 if (chunkFile.isEmpty() || fileMd5 == null || chunkIndex == null) { return Result.error("参数不完整"); } // 2. 简单扩展名白名单校验(第一层校验) String originalFilename = chunkFile.getOriginalFilename(); if (!allowedSuffix.contains(getSuffix(originalFilename))) { return Result.error("不允许的文件类型"); } // 3. 可选:校验分片MD5(第三层校验) if (StringUtils.hasText(chunkMd5)) { String actualMd5 = Md5Util.calculate(chunkFile.getInputStream()); if (!actualMd5.equalsIgnoreCase(chunkMd5)) { return Result.error("分片校验失败,请重试"); } } // 4. 保存分片到临时目录 Path chunkDir = Paths.get(STORAGE_BASE, fileMd5, "chunks"); Files.createDirectories(chunkDir); Path chunkPath = chunkDir.resolve(chunkIndex + ".part"); chunkFile.transferTo(chunkPath.toFile()); return Result.success(); }在保存分片前校验分片 MD5 这一步,讲究一个顺序——必须先校验再落盘。如果先写文件再校验,坏数据已经在磁盘上了,即使删除,也会产生碎片写入,而且并发场景下还可能出现读到半截数据的情况。
4.2 分片合并的正确姿势
所有分片上传完成后,前端调用合并接口。服务端需要把所有.part文件按照分片序号拼成一个完整的模型文件。
这一步看起来简单,但我在项目里见过同事写出的“花式”错误代码。最典型的错误是把每个分片读成字节数组,然后用 ByteArrayOutputStream 把它们按顺序写到一起:
// 反面教材:不要这样做 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); for (int i = 0; i < totalChunks; i++) { byte[] bytes = Files.readAllBytes(chunkPath); outputStream.write(bytes); } Files.write(targetFile, outputStream.toByteArray());这段代码的问题有两个。第一,多 GB 的文件全部读进内存,直接堆内存溢出;第二,Files.readAllBytes对于 20MB 一个的分片虽然没问题,但合并时把所有分片一次性读进来依然会打爆 JVM。
正确做法是使用流式追加写,用 FileOutputStream 按序号逐个分片写入:
@PostMapping("/api/bim/upload/merge") public Result mergeFile( @RequestParam("fileMd5") String fileMd5, @RequestParam("chunks") Integer totalChunks, @RequestParam("fileName") String fileName) { Path chunkDir = Paths.get(STORAGE_BASE, fileMd5, "chunks"); Path targetPath = Paths.get(STORAGE_BASE, "done", fileName); // 1. 按序号打开目标文件,流式追加 try (FileOutputStream fos = new FileOutputStream(targetPath.toFile())) { for (int i = 0; i < totalChunks; i++) { Path chunkPath = chunkDir.resolve(i + ".part"); if (!Files.exists(chunkPath)) { return Result.error("分片缺失:" + i); } // 逐块复制,缓冲区大小可设为4MB Files.copy(chunkPath, fos); } fos.flush(); } catch (IOException e) { return Result.error("合并失败"); } // 2. 合并完成后,对整个文件计算MD5(第四层校验) String fullMd5 = Md5Util.calculate(Files.newInputStream(targetPath)); if (!fullMd5.equalsIgnoreCase(fileMd5)) { // 校验不通过则删除文件,并返回错误 Files.deleteIfExists(targetPath); return Result.error("文件完整性校验失败,请重新上传"); } // 3. 记录文件信息到数据库 bimFileMapper.insert(fileMd5, fileName, targetPath.toString(), totalChunks); // 4. 清理临时分片目录 deleteDirectory(chunkDir); return Result.success(); }这里有一个容易被忽视的细节:合并时用Files.copy(chunkPath, fos)而不是FileOutputStream.write(bytes)。前者的底层是操作系统的零拷贝或高效字节流复制,性能和可靠性都比手动读字节再写要好。而且Files.copy处理的是文件到流,不会把整个分片都加载到 JVM 内存里,它是边读边写的。
合并完成后的全文 MD5 校验非常重要。我特意在校验失败时直接删掉合并文件,而不是留在磁盘上。这样避免产生一个“存在但不可用”的脏数据,用户下次重新上传时也不会命中秒传逻辑,否则前端一看 MD5 已存在,跳过上传,结果用户拿到的还是一个坏文件。
4.3 幂等、并发和临时目录清理
由于 WebUploader 支持并发上传,服务端必须考虑并发场景下的分片写入冲突。同一个分片如果因为客户端重试被同时写入两个请求,可能出现文件被覆盖、文件锁冲突、写入半截等情况。
我的处理策略是给分片写入加一个“文件锁 + 写入后置校验”的组合手段。文件锁可以用一个简单的ConcurrentHashMap针对 fileMd5 加锁,或者用操作系统级的文件锁避免多线程互抢。写入后再校验分片的大小是否等于 chunkSize(最后一个分片可以小于 chunkSize),防止写入半截就返回成功。
// 加锁伪代码 String lockKey = fileMd5 + ":" + chunkIndex; ReentrantLock lock = chunkLock.computeIfAbsent(lockKey, k -> new ReentrantLock()); lock.lock(); try { chunkFile.transferTo(chunkPath.toFile()); } finally { lock.unlock(); }临时目录清理也是必须做的。分片上传失败的场景下,临时文件夹里会残留大量.part文件,时间久了会占满磁盘。我在项目里写了一个定时任务,每天凌晨扫描临时目录,删除超过 24 小时未更新的分片文件。这块不处理的话,我见过一个项目因为模型文件上传失败后不断重试,一周时间磁盘被塞满了。
5. 常见问题与排查实录
5.1 问题一:合并后的模型文件在建模软件里打不开
这是我遇到过最严重的问题。用户反馈一个 RVT 文件上传显示 100%,但下载下来后 Revit 打开直接报文件损坏。
排查过程让我印象很深。第一反应是分片丢失或者顺序不对,但检查日志发现所有分片都上传成功,MD5 校验也通过了。后来我手动打开合并后的文件,用文本编辑器看了一下文件头,发现文件头部的字节明显错乱,像是有一部分数据被覆盖了。
最后定位到原因:分片文件重名。两个不同请求上传的同一个分片序号写到了同一个.part文件,其中一个返回成功了,另一个因为网络延迟也返回成功,后写入的数据覆盖了先前的数据,但被覆盖的文件的 MD5 校验恰好是在覆盖之前做的,所以没有触发告警。这个问题的本质是并发写入同一个文件,且没有做“写入后校验”。
解决方式就是我前面说的:给分片写入加锁,并且落盘后重新检查文件大小,不匹配立即删掉这个分片返回重试提示。
5.2 问题二:浏览器在计算 MD5 时卡死
前端用 SparkMD5 计算 2GB 文件的 MD5 时,页面会卡住几秒甚至几十秒,用户以为浏览器死了,直接关闭页面。
这个问题的原因很简单:JavaScript 是单线程的,大量 ArrayBuffer 操作会阻塞 UI 渲染。解决办法不是不用 MD5,而是拆分任务。我推荐把 MD5 计算放到 Web Worker 里,这样计算过程完全在后台线程执行,页面保持流畅。
// 主线程中启动Workder计算MD5 var worker = new Worker('/js/md5-worker.js'); worker.postMessage({ file: file }); worker.onmessage = function(e) { globalFileMd5 = e.data; uploader.upload(); };如果项目不方便引入 Worker,退而求其次的做法是降低读块大小,比如从 4MB 降到 1MB,并且在计算过程中给用户明显的进度提示。这样虽然 UI 还是会有卡顿,但至少用户知道程序在干活,不会关闭页面。
5.3 问题三:断点续传时进度条突然“跳满”
用户上传 80% 后中断,重新上传时进度条立刻跳到 100%,点开文件却还是不能用的旧版本。
这个问题通常有两个原因。一是服务端没有对已上传的分片做“存在性校验”,返回了错误的已上传分片列表,前端跳过了一些实际不存在或损坏的分片。二是前端没有正确过滤未上传的分片,直接全部跳过了。
我的排查套路是:先看服务端返回的uploadedChunkIndices是否符合实际 disk 上.part文件的情况,再看前端uploadBeforeSend事件里跳过的判断逻辑是否拿对了file.uploadedChunk字段。WebUploader 的uploadedChunk是当前分片编号,uploadedChunkTotal是总分片数,用错字段时往往会出现诡异的进度跳跃。
5.4 问题四:老浏览器兼容性异常
虽然现在内网环境普遍是 Win10+Chrome,但总有一些特殊的办公终端还在用低版本浏览器。WebUploader 的 Flash 降级模式依赖swf文件路径,且需要在页面上引入 Flash 插件。
我在项目里直接限制了最低支持版本,对于连 H5 File API 都不支持的古董浏览器,页面会提示“当前浏览器不支持大文件上传,请使用 Chrome 或 Edge”。这样做虽然有点粗暴,但对于一个内部系统来说,引导用户换浏览器比适配十几个老版本内核的成本低得多。
5.5 常见问题速查表
| 症状 | 可能原因 | 处理建议 |
|---|---|---|
| 合并后文件损坏 | 分片并发写入覆盖 | 分片写入加锁 + 落盘后校验文件大小 |
| 浏览器卡死 | MD5 计算阻塞线程 | 使用 Web Worker 或降低读块大小 |
| 进度跳满但文件不可用 | 已上传列表不准 | 校验分片实际大小后再返回列表 |
| 分片重复上传 | 客户端网络重试 | 服务端做幂等处理 |
| 磁盘空间爆满 | 临时分片未清理 | 添加定时清理任务 |
| 上传到一半连接断开 | 代理/网关超时 | 调整分片大小并增加重试次数 |
6. 写在最后:一些真实的心得
这套方案上线后,某工程集团的设计人员每天都要上传和下载几十个模型文件,总数据量过 TB 级别。实际运行下来,稳定性确实比之前用 FTP 和网盘方案好太多,用户最大的感知就是“终于不用整文件重传了”。
我在实际项目中体会最深的两点,一个是大文件上传的正确性比速度更重要。优化带宽利用、缩短传输时间是让人高兴的事情,但如果一个 2GB 的模型因为某个分片坏掉而无法打开,之前省下来的几分钟在网络损耗面前完全不值一提。所以我把大部分精力都放在了校验机制上,宁可多花一点点计算时间,也要保证文件到达服务端时和本地文件完全一致。
另一个心得是能用标准方案解决的事别自己造轮子。分片上传本身不是一个新问题,WebUploader 这种老组件虽然看起来不够“新潮”,但它的设计足够把事情做对。很多人上来就研究自研分片协议、自研加密传输,结果测试环境一切正常,一上生产就崩,反而拖慢了项目进度。
最后再分享一个小技巧。如果你要在自己的项目里复现这套方案,建议先把“检查接口”和“合并接口”的日志打印做到足够详细。当文件上传出问题时,前后端联调最缺的就是有效信息。我在日志里记录了每个分片的文件名、大小、MD5、耗时,配合一条完整的链路追踪,排查问题时基本不需要让用户重新复现,看一眼日志就能定位是网络问题、前端问题还是服务端合并逻辑的问题。
提示:上传功能做到最后,你会发现 70% 的代码都在处理异常和边界情况,真正上传的那点逻辑只占 30%。这不是坏事,工程类的诉求就是这样,稳定压倒一切。