十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FineUploader 5精简改造:从全量包到core版+自绘UI

FineUploader 5精简改造:从全量包到core版+自绘UI 简介这是一份面向前端开发者的 FineUploader 5.0.2 精简版上传组件资源完全基于 JavaScript 编写移除了内置模板让开发者能够自由设计上传界面。它保留了多文件选择、拖放上传、断点续传、实时进度条、错误处理、AJAX 异步提交、跨域请求以及 RESTful 接口兼容等关键能力适用于需要在网站后台或管理系统中灵活嵌入上传功能的中级前端工程师。资源包共 5 个文件包括 1 个 CSS 样式文件、1 个 JS 主逻辑文件以及 3 个 GIF 状态提示图标总体积仅 74KB加载成本低适合在带宽敏感或轻量级项目中直接使用。包内不含现成示例页面使用时需自己创建 DOM 容器并初始化 FineUploader 实例设置服务器接收地址即可工作便于按项目需求定制样式和交互也可作为二次开发的基础。目前已有 441 人学习下载若想了解上传组件的轻量化实现或进行快速集成这份资源能提供简洁实用的参考。 FineUploader 5 这款上传组件我到现在还在不少老项目里见到它自己也维护过一个用了它多年的后台系统。它的特点是功能太全了拖拽、粘贴、分块、并发、缩略图、S3、Azure、图片校验一个分发包里全塞进去了。但实际业务里绝大多数项目只用了其中两三个能力剩下的就是白付的加载成本。所以我最近做了一次 FineUploader 5 的“精简”改造这篇文章就是完整的实操记录从需求梳理、方案选型、具体替换到踩坑排查一次讲清楚。如果你也在被这个老牌上传库的体积和复杂度困扰可以参考这套做法。1. 为什么我要折腾 FineUploader 5 的精简1.1 它的“全”不是免费午餐FineUploader 5 的核心优势是集成简单、文档稳定、跨浏览器处理得比较到位。但代价也很直接官方构建产物是全功能集合dist/fine-uploader.new.min.js这个文件里包含了 UI 渲染、模板解析、拖拽、粘贴、缩略图、S3/Azure 适配器等一大堆代码。我接手后台系统时页面里只是上传一个 Excel 文件却加载了整套上传引擎实在说不过去。这个库还有一个问题它本身是一个长期维护但功能冻结的老项目不会有人像现代 npm 生态那样帮你做按需加载和 tree shaking。社区里也少有专门讲它精简的文章大多数人只会在初始化参数里关掉几个功能但代码量并不会因此减少。只要引入的是全量文件浏览器就必须下载、解析、执行所有模块。对一个后台管理系统来说每多出来的几十 KB都可能在低配办公机上变成可感知的白屏时间。我统计了一下当时页面的情况左侧菜单栏加载了 jQuery、jQuery UI、多个业务脚本中间还有大大小小十几个插件FineUploader 全量文件在其中的体积占比不小而且它是上传统一入口几乎每个用到上传的页面都会带上它。与其到处打补丁不如从源头把这个包裁掉一圈。1.2 精简之前先列需求清单精简的第一步不是打开压缩工具而是坐下来列清楚当前业务真正依赖 FineUploader 的哪些能力。我当时的场景很典型就是一个传统后端接口接收multipart/form-data上传 Excel 和图片需要支持一次选多个文件、有进度条、能取消某个未完成的任务、上传完成后显示成功或失败。我把功能需求逐项过了一遍列成下面这个表功能项当前业务是否真的需要备注文件选择与多选需要基础能力必须保留分块上传需要大 Excel 分块后体验更好上传进度显示需要用户反馈必须要有进度取消/删除文件需要错误重选场景拖拽上传不需要后台用户习惯点按钮选择粘贴上传不需要从未被使用缩略图生成不需要只是 Excel 和普通图片无需预览S3 / Azure 适配不需要我们只用自建接口官方内置 UI 模板不需要页面本来就有自己的交互样式做完清单之后结论非常明确我需要的核心能力一个.core版本就能完全覆盖UI 部分完全可以自己画。FineUploader 官方发布包里有一个fine-uploader.core.min.js这相当于一个去掉了全部 UI 绑定和模板渲染的上传内核保留了最关键的请求、分块、并发、事件系统。把它接进来再写一个轻量的上传列表 DOM就是一次标准的精简改造。2. 三条精简路线按成本排序2.1 路线 A直接上 coreUI 自己画这条路最直接也是我这次选用的方案。fine-uploader.core.min.js的体量比 UI 全量包小一截而且它不再依赖任何模板元素页面想怎么展示上传队列都行。代价也很明确进度条怎么渲染、错误信息在哪显示、删除按钮绑定什么事件这些原本官方 UI 帮你做的事全部要自己实现。我建议小团队或者特殊定制需求优先考虑这条路线。原因很简单官方 UI 模板本身是为通用场景设计的你要精简它就得改模板和 CSS改来改去可能比直接自己渲染列表还麻烦。而 core 版本用完好的事件系统自由度极高适合做定制上传交互。需要提前适应的一点是core 版实例挂载的 API 名是qq.FineUploaderBasic和 UI 版的qq.FineUploader不同。事件名称大多保留但所有跟 DOM 相关的 UI 方法比如getItemByFileId、getFileEl在 core 版本里都不存在。后面我会专门说这个坑。2.2 路线 B保留官方 UI从源码定制构建如果你们的业务确实需要官方 UI 的整套交互比如拖拽区域、缩略图、自动弹出的错误提示但又不想要 S3/Azure 那些用不到的部分可以从 GitHub 拉取 FineUploader 5 的源码用 Gulp 任务重新构建一版精简产物。官方仓库的构建逻辑是按模块打包的理论上你可以把client/js/azure、client/js/s3这类目录从打包入口里去除只保留core和ui相关模块。这条路最大的成本在环境维护上。FineUploader 5 的构建链路是很久以前的 Gulp Node 生态放到现在的环境下跑经常会出现依赖兼容问题。我试过一次它依赖的老版本 Gulp 和 Node 接口变动比较大需要在虚拟机或 nvm 切到旧 Node 版本才能顺利构建折腾成本不低。如果团队里没有人熟悉老构建工具链我不太建议为了一个上传库去维护这套东西。2.3 路线 C打包器层面的引用裁剪还有一种更“现代”的思路不直接改库文件而是利用 webpack 或 Vite 的 externals、alias、IgnorePlugin 等机制把 FineUploader 代码里的 S3、Azure、粘贴等功能模块排除在产物之外。思路没问题但现实有点骨感。FineUploader 的发行包是预构建的 UMD 文件模块之间的关系并不是标准的 ES Module 依赖打包器能做的主要是把不用的独立文件排除掉真正的核心代码仍然被捆绑在一起裁剪效果有限。不过这条路线也不是完全没有价值。如果项目里通过 npm 引入了fine-uploader包又用了官方提供的多个入口文件利用打包工具把实际没用到的那几个 add-on 文件 ignore 掉至少能避免它们进到最终 bundle 里。对于使用了不同入口的项目这个方式可以作为一个辅助手段不要指望它能替代前面的方案。3. 一次完整的 core 版精简改造实操3.1 文件与环境准备我拿到的项目用原生 JavaScript jQuery 写页面后端是 Spring MVC 接口接收上传文件。改造前页面在顶部直接引用了fine-uploader.new.min.js和官方模板相关的 CSS我把这两行去掉换成了fine-uploader.core.min.js。这个文件在官方下载包的dist目录下就能找到也可以直接引用 CDN 上对应版本的链接我用的是本地静态资源方便离线部署。官方 5.16.x 版本里core 版文件暴露的全局变量结构是qq.FineUploaderBasic它和 UI 版本命名空间不同但共享同一套配置项和回调体系。我遇到的一个小问题是替换文件后需要保证页面里没有遗留任何依赖qq.FineUploaderUI 方法的代码否则就会在调用时直接报错。这一步建议提前搜索一下getItemByFileId、getFileEl、getUuid这类 UI 专属方法在项目里的使用情况。3.2 初始化代码与事件绑定改造后的上传核心代码如下。我禁用了自动上传用户选择文件后先展示在列表里点击“开始上传”按钮再统一提交分块开启每块 2MB并且支持并发分块。const rows new Map(); const uploader new qq.FineUploaderBasic({ debug: false, autoUpload: false, request: { endpoint: /api/upload, params: { token: window.currentUserToken } }, chunking: { enabled: true, partSize: 2 * 1024 * 1024, concurrent: { enabled: true } }, callbacks: { onSubmitted: function (id, name) { const row renderUploadRow(id, name); rows.set(id, row); }, onProgress: function (id, name, uploadedBytes, totalBytes) { const row rows.get(id); if (!row) return; const percent totalBytes 0 ? Math.round((uploadedBytes / totalBytes) * 100) : 0; row.progressBar.style.width percent %; row.progressText.textContent percent %; }, onComplete: function (id, name, response) { const data typeof response string ? JSON.parse(response) : response; const row rows.get(id); if (!row) return; if (data.success) { row.statusText.textContent 上传成功; row.statusText.classList.add(success); } else { row.statusText.textContent data.error || 上传失败; row.statusText.classList.add(error); } }, onError: function (id, name, errorReason) { const row rows.get(id); if (!row) return; row.statusText.textContent errorReason; row.statusText.classList.add(error); } } });初始化的配置和 UI 版本几乎一样重点变化在于所有原来由模板自动更新的 DOM 内容现在都要自己在回调里更新。我在onSubmitted里创建一行上传记录并把 DOM 节点的引用存到 Map 中后续的onProgress、onComplete、onError都通过id找到对应行。这个id是 FineUploader 内部维护的文件索引整个生命周期内都保持不变用它做关联最稳妥。3.3 自定义上传列表的渲染实现既然用了 core 版上传列表的 HTML 结构就是自己控制的了。我写了一个简单的渲染函数没引入任何前端框架直接操作 DOMfunction renderUploadRow(id, name) { const wrapper document.createElement(div); wrapper.className upload-row; const nameDiv document.createElement(span); nameDiv.className file-name; nameDiv.textContent name; const progressWrap document.createElement(div); progressWrap.className progress-wrap; const progressBar document.createElement(div); progressBar.className progress-bar; progressWrap.appendChild(progressBar); const progressText document.createElement(span); progressText.className progress-text; progressText.textContent 0%; const statusText document.createElement(span); statusText.className upload-status; const cancelBtn document.createElement(button); cancelBtn.type button; cancelBtn.textContent 取消; cancelBtn.addEventListener(click, function () { uploader.cancel(id); }); wrapper.appendChild(nameDiv); wrapper.appendChild(progressWrap); wrapper.appendChild(progressText); wrapper.appendChild(statusText); wrapper.appendChild(cancelBtn); document.getElementById(upload-list).appendChild(wrapper); return { progressBar: progressBar, progressText: progressText, statusText: statusText }; }文件选择部分也要自己实现。我用一个隐藏的input[typefile]点击按钮时触发选择选择完成后把文件传给 FineUploaderconst fileInput document.getElementById(file-input); document.getElementById(select-files-btn).addEventListener(click, function () { fileInput.click(); }); fileInput.addEventListener(change, function () { const files Array.from(fileInput.files); if (files.length 0) { uploader.addFiles(files); fileInput.value ; } }); document.getElementById(start-upload-btn).addEventListener(click, function () { uploader.uploadStoredFiles(); });这里用uploader.uploadStoredFiles()是因为我在初始化阶段设置了autoUpload: false文件添加后不会立即上传会暂存在内部队列里手动调用这个方法才统一发送。这个方法是 core 版本早就有提供的UI 版本同样可用。3.4 页面接入与体积对比把原来的 UI 版标签移除后页面里需要引入的核心资源就只剩一行脚本外加我自己的上传列表样式。改造完成后我专门看了一下网络面板FineUploader 相关资源的体积明显下降。在我这个项目里UI 全量文件的体积大约是 core 版本的两倍左右考虑到上传页面并不需要官方模板和拖拽逻辑换成 core 后不仅脚本体积减了连初始化时的 DOM 解析和事件绑定成本也跟着降低。这里我要强调一个容易被忽略的点体积差异在普通后台页面上看起来只有几十 KB但一旦涉及页面性能评审、移动端弱网环境或者多页面复用这个差距会被放大。我管理的系统里多个页面都用了上传组件全量包被浏览器缓存后问题不明显但首次访问和强刷时少加载的这部分资源对加载速度是有真实贡献的。4. 精简过程中遇到的坑和排查技巧4.1 问题速查表我在改造过程中踩了不少坑这里整理成一张速查表方便后面直接对照排查异常现象常见原因处理方式uploader.getItemByFileId is not a function用了 core 版UI 专属方法不存在维护一个id - DOM 节点的 Map自己处理关联后端拿不到文件参数名对不上FineUploader 默认字段名是qqfile后端请从qqfile字段取文件而不是file分块上传后文件损坏后端没有处理分块合并根据qqpartindex、qqtotalparts、qquuid字段合并或重写上传逻辑response.success报错后端返回了文本类型response 是字符串先JSON.parse(response)再读取属性加载模板相关错误页面残留了 UI 模板引用的元素清理qq-template相关 HTML 块其中字段名qqfile是最容易被后端同事忽略的。FineUploader 在做普通 post 上传时文件数据挂在名为qqfile的字段下面Java 端用RequestParam(qqfile) MultipartFile file才能接住。如果后端写的还是RequestParam(file)那就会莫名其妙的“文件不存在”。4.2 让我印象最深的两个坑第一个坑是onComplete回调里的 response 类型问题。我的后端接口明明返回了 JSON但浏览器端拿到的 response 却是字符串读response.success时直接报错。查了一圈发现是后端的接口没有设置application/json响应头XHR 自动把返回内容当成纯文本了。FineUploader 内部会根据 response 类型做解析但这里如果 Content-Type 不对它传给回调的就会是字符串。解决办法有两个后端加上正确的响应头或者在前端回调里先判断类型做一次JSON.parse。我两个都做了双保险。第二个坑是removeFile和cancel的区别。我在 UI 版本的旧代码里取消某个任务时调用的是cancel(id)但业务方反馈说取消之后文件还留在列表里点开始上传又会重新上传。后来看了文档才确认cancel(id)只是终止当前请求并没有把文件从上场队列中移除。如果你希望用户点击“删除”后这个文件就不再参与上传必须调用removeFile(id)这个才是从内部文件列表里彻底移除的方法。当时我把这两个方法混用了好几轮踩完才发现命名上确实容易让人混淆。4.3 带上缓存和懒加载效果更好精简完还不够我顺手把 FineUploader 的加载时机往后退了一层只有点击了“上传文件”按钮并弹出文件选择框时才动态加载fine-uploader.core.min.js。原先它是全局 JS 首屏加载现在变成一个按需加载的异步脚本。老代码里没有现成的 loader我直接写了个简单的动态插入脚本逻辑几行代码就搞定了。如果你们的项目也用 webpack可以在入口文件里动态引入核心代码类似function ensureUploaderLoaded() { return new Promise(function (resolve, reject) { if (window.qq window.qq.FineUploaderBasic) { resolve(); return; } const script document.createElement(script); script.src /static/vendor/fine-uploader.core.min.js; script.onload resolve; script.onerror reject; document.head.appendChild(script); }); }另一个建议是给这个文件设置一个比较长的浏览器缓存时间因为它属于固定版本的第三方库不会频繁变动用长期缓存可以减少非首次访问的下载量。加上这层缓存后后续页面访问时 FineUploader 部分基本就是从本地缓存读取几乎没有额外加载成本。这几次折腾下来我的体会很明确FineUploader 5 的精简优先级最高的不是炫技式的代码拆分而是先理清楚当前业务到底需要哪些能力。core 版加自绘 UI 是绝大多数后台上传场景的最优解省下来的体积是实打实的。最后再分享一个小建议在老项目里做这种替换之前先把旧版本的初始化和所有事件回调截图或保存下来切换完之后逐项对照避免漏掉某个隐藏依赖那样排查起来会很浪费时间。本文还有配套的精品资源点击获取
返回列表