首页 / 资讯中心 / 文章详情

信创环境文件夹上传:webkitdirectory与相对路径还原实战指南

信创环境文件夹上传:webkitdirectory与相对路径还原实战指南 ★ FEATURED ARTICLE
信创环境下做前端最头疼的不是业务逻辑而是浏览器适配。尤其是文件上传这块需求方一句“要保留文件夹路径”就能让一个原本半小时搞定的功能变成全员一起排查的攻坚战。最近我们在国产化终端上就接了这么个活儿HTML5页面需要让用户选择整个文件夹上传并且要在界面上正确显示每个文件原有的目录层级同时把相对路径一并提交到服务端。“保留文件夹上传路径”这个需求第一反应是拿file.path直接读本地绝对路径。但这是老IE和某些早期国产浏览器的私有能力到了现代HTML5规范下基本被砍干净了。换到国产化环境里浏览器内核五花八门有的支持标准API有的还带兼容模式稍不留神就掉进坑里。这篇文章就围绕这个真实项目把我踩过的坑、验证过的方案、以及最终稳定运行的实现方式完整记录下来希望能给同样在信创环境中挣扎的朋友一个可复现的参考。1. 项目背景与需求梳理信创环境下的“路径保留”难题1.1 为什么国产化浏览器会“丢路径”先厘清一个概念“路径”在这里分两种。一种是用户本地的绝对路径比如D:\项目资料\2024\合同扫描件\甲方签字版.pdf另一种是文件夹内部的相对路径也就是“某个文件相对于他所在根目录的位置”比如2024\合同扫描件\甲方签字版.pdf。信创环境下的业务系统通常要求的是后者。原因很简单用户在界面上选择了一个根文件夹系统需要把里面所有文件按照原有的目录层级还原出来方便后台按同样的结构归档。这是很多档案系统、资料管理系统、网盘同步工具的标准需求。但问题在于浏览器出于安全性考虑不允许网页脚本直接读取用户本地文件的绝对路径。这是跨浏览器统一的安全策略不管你是Chrome还是国产浏览器只要是现代Web技术栈这条红线都绕不过去。你拿到的File对象里name只有文件名没有任何目录信息。早期有些浏览器厂商提供了非标准接口比如基于Chromium 60以前的内核File.path还能读到本地路径IE时代甚至可以通过document.all加 ActiveX 控件直读。但这套东西在信创环境下根本行不通一来国产浏览器内核版本参差不齐二来即便内核支持业务系统也不可能依赖这种非标准私有属性——换个浏览器就全崩了。1.2 真正的需求是保留路径还是展示层级所以接到这个需求第一件事不是写代码而是跟需求方确认“路径”的准确含义。我遇到过客户张口就要“上传时显示文件本地绝对路径”但实际业务上根本用不到——他只是想在页面上看到“哪个文件在哪个文件夹下”方便核对目录结构。把需求拆清楚之后方案就清晰了我们要做的是拿到文件夹内每个文件相对于所选根目录的相对路径然后在界面上渲染出一棵目录树最后在提交时把这个相对路径作为文件的一个附加字段传给后端。这里涉及的三个核心点分别是HTML5的文件系统访问能力、File对象的标准属性、以及跨浏览器的兼容适配。搞清楚这三点整个功能就成功了一半。2. HTML5文件系统API的技术边界标准给了什么、没给什么2.1 File对象与webkitRelativePath的工作原理HTML5规范里标准文件上传控件支持一个webkitdirectory属性。只要在input上加了它浏览器弹出的选择框就会从“选文件”变成“选文件夹”。选完文件夹之后input.files里装的不是文件夹本身而是文件夹内所有文件的扁平列表。换句话说你拿到了一堆File对象但每个文件多出了一个标准化的属性file.webkitRelativePath。这个属性就是文件相对于选定根目录的相对路径格式统一用斜杠分隔例如选择根目录项目资料 项目资料/2024/合同扫描件/甲方签字版.pdf那么file.webkitRelativePath的值就是2024/合同扫描件/甲方签字版.pdf注意看它不包含根文件夹“项目资料”这个层级因为根目录是用户选的不隶属于某个上级路径。这是规范定义的行为也是我们还原目录结构时最重要的数据来源。需要强调的三个关键行为webkitRelativePath不是局部变量它是File对象上的实例属性所有浏览器都以字符串形式返回。路径分隔符统一为正斜杠/无论Windows还是国产Linux平台这个属性返回的格式一致后面处理时不用再费力兼容反斜杠。根目录本身不会出现在属性值里需要你在代码中单独记录用户选择的文件夹名。所以整个实现的基础就一句话用webkitdirectory开启文件夹选择用file.webkitRelativePath获取相对路径以此还原目录层级。2.2 国产浏览器的内核差异对比信创环境下国产化浏览器的内核分化是个必须面对的现实。一般分为几类浏览器类型内核webkitdirectory支持情况典型场景360安全浏览器极速模式Chromium完整支持政企办公常用360安全浏览器兼容模式IE内核不支持老系统被迫使用奇安信浏览器Chromium完整支持安全要求较高红莲花浏览器Chromium完整支持信创标配龙芯浏览器Chromium定制视版本而定特定硬件终端中科方德/统信自带浏览器Chromium或Firefox系多为完整支持国产Linux从表格能看出凡是跑Chromium内核的webkitdirectory基本都可以放心用。真正需要警惕的是兼容模式切到IE内核的老旧场景。如果业务系统强制要求兼容IE内核HTML5文件夹上传这条路是走不通的必须退回到控件方案或提示用户切换极速模式。另外一个易踩的坑部分国产浏览器默认是“兼容模式”页面加载后你可能没察觉但实际上input.files的返回值是空数组。这就是为什么上线前必须在真实终端环境里逐个验证浏览器模式和内核版本而不是在自己电脑的Chrome里测完就完事。3. 实操实现文件夹上传并在国产化浏览器中还原目录结构3.1 基础实现用webkitdirectory开启文件夹选择前端部分我们需要一个隐藏的input元素加上两个关键属性。代码很简单input typefile idfolderPicker webkitdirectory directory multiple styledisplay:none; /这里有个细节directory属性是标准写法webkitdirectory是旧前缀写法。现代浏览器都支持webkitdirectory但保险起见两个都写上兼容性更好。multiple属性也不能漏虽然选了文件夹本身就隐含多文件但某些浏览器实现中缺了multiple会报路径错误。监听change事件后遍历event.target.files对每个文件做基础校验const handleFolderSelect (event) { const files Array.from(event.target.files); const fileList files.map((file) { // 通过 webkitRelativePath 获取相对路径 const relativePath file.webkitRelativePath || ; return { file, relativePath, fileName: file.name, size: file.size, }; }); console.log(解析到文件数量, fileList.length); };实测下来1000个文件的选择在一秒内就能完成解析性能上没有压力。但真正需要考虑的是后续目录树的构建这才是这个需求的核心逻辑。3.2 路径还原从扁平列表构建一棵目录树拿到webkitRelativePath之后接下来要做的就是把扁平文件列表转换成一棵树形结构。这个转换不能想当然地直接拿路径字符串做分割因为你会遇到几个实际问题同一层级下既有文件夹又有文件不同层级可能出现同名文件夹分隔符需要统一处理我采用的方式是维护一个Map类型的数据结构用“路径片段”逐级构建节点。核心逻辑如下function buildTree(fileList) { const root { name: 根目录, type: folder, children: [] }; const map new Map(); map.set(, root); fileList.forEach((item) { const parts item.relativePath.split(/); let currentPath ; parts.forEach((part, index) { const parentPath currentPath; currentPath currentPath ? ${currentPath}/${part} : part; if (!map.has(currentPath)) { const isFile index parts.length - 1; const node { name: part, type: isFile ? file : folder, path: currentPath, ...(isFile ? { file: item.file, size: item.size } : {}), children: isFile ? [] : [], }; map.set(currentPath, node); map.get(parentPath).children.push(node); } }); }); return root; }这个实现的巧妙之处在于用Map作为缓存保证同名路径节点只创建一次。比如2024/合同扫描件和2024/财务报表都包含2024这个文件夹但2024节点只会在第一次遇到时创建后续直接复用。这个细节如果没处理好树就会出现重复的兄弟节点UI展示和后续数据提交都会出问题。树的构建完成后渲染到页面上就非常灵活了。你可以用递归组件渲染树形列表也可以直接用ul/li加上缩进展示。如果项目用了Vue或React配合递归组件效果最好。我当时在Vue项目里写了一个递归组件结构大概是template ul li v-fornode in nodes :keynode.path span :classnode.type {{ node.type folder ? : }} {{ node.name }} /span directory-tree v-ifnode.children.length :nodesnode.children/directory-tree /li /ul /template这里注意不要用node.file作为key因为同名文件可能出现在不同目录下key必须用完整相对路径保证唯一性。3.3 兼容多浏览器的适配层策略信创环境最大的不确定性就是“你永远不知道用户用的是哪款浏览器”。所以代码层面必须做能力检测不能假设webkitRelativePath一定存在。我封装了一个适配层核心逻辑如下function getFileRelativePath(file) { // 首选标准属性 if (file.webkitRelativePath typeof file.webkitRelativePath string) { return file.webkitRelativePath; } // 兼容老版本私有属性 if (file.relativePath typeof file.relativePath string) { return file.relativePath; } // 都没有降级为纯文件选择 return ; }然后在使用时先判断input.files[0].webkitRelativePath是否存在。如果存在就走文件夹上传逻辑如果不存在则有两种可能一是用户浏览器版本太老不支持文件夹选择二是浏览器把文件选择当成普通文件选择了。这种情况下我建议做两层降级第一层给用户醒目的提示告知“当前浏览器不支持文件夹上传请升级浏览器或切换到极速模式”。第二层返回到普通多文件选择模式让用户可以手动逐个选择文件毕竟业务不能因为浏览器限制而完全停摆。从真实操作体验来说第二层降级虽然丑但至少保证功能可用。项目上线后我特意统计了一下用户使用的浏览器版本绝大多数都是基于Chromium 70以上的内核只有零星几个还挂在老旧浏览器上。所以这个适配层更多是保险措施但必须有否则线上出了问题连回退方案都没有。4. 提交方案设计服务端如何用相对路径重建目录4.1 前端提交的数据结构设计前端拿到目录树后提交给服务端的方式有两种主流方案一种是先把文件全部上传再单独提交一份JSON目录结构另一种是每个文件在提交时附带自己的相对路径字段服务端依据这个字段自行重建目录。在实际项目中我更推荐第二种因为它不需要额外维护文件与目录的关联关系服务端接收时天然拿到了每个文件的最终路径。具体实现上用FormData就能轻松搞定function uploadFiles(fileList, targetUrl) { const formData new FormData(); fileList.forEach((item, index) { formData.append(files, item.file); formData.append(paths, item.relativePath); }); return fetch(targetUrl, { method: POST, body: formData, }); }这里一个容易被忽略的坑FormData.append同一个key会以数组形式提交所以服务端接收时要按照paths数组和files数组的下标一一对应。如果后端是Java的Spring MVC直接定义两个ListString和ListMultipartFile就行顺序是对应的。如果后端是Node.js的Express用multer加req.body里的数组也能拿到。但这样做有一个隐患当文件数量很大时所有文件一股脑放进一个FormData里服务器内存压力不小。所以更严谨的做法是分批提交比如每50个文件一批逐批上传。const BATCH_SIZE 50; async function uploadInBatches(fileList, targetUrl) { for (let i 0; i fileList.length; i BATCH_SIZE) { const batch fileList.slice(i, i BATCH_SIZE); await uploadFiles(batch, targetUrl); } }分批上传还有一个好处可以给用户展示进度条。每完成一批进度跟着涨用户能清楚看到上传状态体验好了很多。4.2 服务端的安全校验与路径拼接原则服务端接收相对路径后拼接存储路径时必须格外谨慎。因为相对路径是用户可控的如果直接拼接可能出现目录穿越漏洞。比如恶意用户把relativePath设置为../../etc/passwd后台拼路径时直接写到了系统目录那就出大事了。所以服务端必须有几道防护校验relativePath不能以.或/开头。把relativePath按/分割后逐级过滤遇到..直接拒绝。最终拼好的完整路径要做一次规范化处理确保路径在预设的根目录之内。以Node.js为例可以这样校验const path require(path); function safeJoin(baseDir, relativePath) { const safePath path.normalize(relativePath).replace(/^(\.(\/|\\|$))/, ); const finalPath path.resolve(baseDir, safePath); if (!finalPath.startsWith(path.resolve(baseDir))) { throw new Error(非法路径); } return finalPath; }这个函数先规范化路径再去掉所有开头的相对路径标记最后用startsWith判断最终拼接的路径是否还在基准目录内。这套逻辑是所有文件上传服务端必须做的基本功不只是文件夹上传才需要。从项目实际效果看安全校验加上前端适配层整个功能从开发到稳定上线大概用了两天半。其中半天花在反复切浏览器验证兼容性上真正写业务逻辑的时间其实很少。这也印证了做信创适配的常态大部分工作量不在业务本身而在环境差异的兼容处理上。5. 常见问题与排查技巧实录5.1 webkitRelativePath为空的原因与对策这是开发中遇到最多的一个问题。辛辛苦苦写了文件解析逻辑结果在某个浏览器上file.webkitRelativePath全部返回空字符串整个树形结构瞬间变成一堆散文件。排查下来原因基本就两种input上没加directory或webkitdirectory属性文件选择框根本没进入文件夹模式。这种属于低级错误检查一下DOM属性即可。部分老版本国产浏览器虽然能弹出文件夹选择框但底层没有实现标准API所以File对象上不存在该属性。这种情况只能通过降级处理。另外一个特殊情况也必须点名同一款浏览器在“极速模式”和“兼容模式”下的行为天差地别。有次测试反馈说“本地好好的到了客户现场就废了”一查发现客户用的正是兼容模式。解决方式是在页面里加一个模式检测和提醒或者建议运维统一设置默认极速模式。5.2 文件夹选择与文件选择状态切换混乱当同一个input既可能要选文件又可能被切换成选文件夹时容易出现状态残留问题。比如用户先选择了文件夹清空后再选择文件此时input.files里可能残留之前的文件列表。踩过一次之后我养成了习惯每次打开选择框之前先把input.value手动置空folderPicker.value ; folderPicker.click();这个操作虽然看起来人畜无害但它能确保change事件触发时返回的是全新列表杜绝脏数据。实测在多批次交替选择的场景下这个小细节能避免大量莫名其妙的bug。5.3 空文件夹与隐藏文件处理策略文件夹上传时有些文件夹是空的。用户明明选了一个有子目录的文件夹但界面上的树里压根看不到空目录——因为空的文件夹不会进入input.files列表。这对于某些资料管理业务是不能接受的因为目录层级本身就承载着业务信息。HTML5标准接口对空目录是无能为力的因为File对象只对应文件。如果你必须保留空目录方案只剩下一个在上传前额外生成一个.keep占位文件放进空目录里或者在后端创建目录时允许前端额外提交一个“空目录清单”。我在项目里走了第二条路前端树构建时把路径片段中所有“只作为目录出现、不含文件”的节点主动识别出来上传时单独提交一个emptyDirs字段服务端根据这个清单创建空目录。const emptyDirs []; map.forEach((node) { if (node.type folder node.children.length 0) { emptyDirs.push(node.path); } });这样处理之后目录的完整性就保住了。隐藏文件的处理则相对简单默认不过滤但如果业务敏感可以在前端用file.name.startsWith(.)判断后过滤掉同时给出用户被过滤文件的统计信息。5.4 大文件夹上传的性能考量与分片方案文件夹里的文件一旦多起来比如几万个照片或几百个文档浏览器直接崩溃也不是没可能。核心瓶颈有两个一是文件列表解析时的内存占用二是服务端同时接收大量文件时的IO压力。内存这块树构建阶段要避免频繁操作DOM。正确做法是先把所有文件解析成纯数据结构再用虚拟滚动渲染树不要一次性把所有节点全塞进DOM。如果树特别深、节点特别多可以考虑GitHub开源的树形组件配合懒加载效率会好很多。IO这块上面提到的分批上传已经解决了大部分问题。如果单个文件也很大最好是叠加上分片上传把大文件切成1MB的片每片单独提交传完再在服务端合并。这样做的好处是断点续传和失败重试都很容易实现体验比整体提交稳得多。我实际测试过一个包含3200个文件、单文件最大180MB的文件夹分批加简单分片之后上传稳定性和成功率比一次性提交有明显提升内存峰值也降了30%左右。5.5 完成进度提示与用户交互优化进度提示不是花瓶功能它是用户感知上传过程是否正常的重要窗口。我在上传进度里加了两个层次第一个层次是整体进度用已上传文件数除以总文件数算出百分比显示在页面的顶部进度条上。第二个层次是当前正在处理的文件路径动态更新在页面的下方让用户知道程序没卡死。实现进度显示时有一个递进的经验不要用setInterval去轮询上传状态而是让每个批次上传完成回调直接触发进度更新。这样进度更新更及时也不会产生多余的网络请求。实测下来用户对进度的满意度明显提升很多原来怀疑页面卡死的反馈直接消失了。另外还需要单独处理“取消上传”的交互。浏览器原生没有支持取消fetch的API但你可以用AbortController在用户点击取消时中止所有未完成的请求。实现起来不复杂const controller new AbortController(); // 上传时传 signal fetch(targetUrl, { method: POST, body: formData, signal: controller.signal, }); // 用户点击取消 controller.abort();最后聊一个容易被轻视但对体验影响非常大的细节用户选择完文件夹到界面上出现目录树中间会有一段解析时间。如果文件数量多这个间隔可能会有一两秒。在这期间如果界面没有任何反馈用户很容易误以为没点成功然后重复点击导致多次弹窗。所以别省那几行代码一定要在解析开始前给个loading状态示例代码如下function handleFolderSelect(event) { if (!event.target.files.length) return; showLoading(正在解析文件夹结构...); setTimeout(() { const fileList processFiles(event.target.files); hideLoading(); renderTree(buildTree(fileList)); }, 50); }这个setTimeout给浏览器一个渲染loading的机会否则同步解析会阻塞UI线程loading闪都不闪就消失了等于没做。这一点是我在实际项目中栽过跟头才验证出来的写在这里提醒大家。6. 最后再分享一点实际体会信创环境的浏览器适配问题本质上是“标准有落地难”的问题。HTML5规范早就定义了文件夹上传的能力接口但国产化浏览器内核的版本碎片化、兼容模式的摇摆、以及底层实现的不一致让原本简单的功能变得充满变数。我做这个项目的最大体会是写代码的时间只占三分之一剩下三分之二都在验证和测试。每个浏览器、每种模式、每类操作系统的组合都要过一遍才能真正交付一个让客户安心使用的功能。所以在方案上我强烈建议“默认走标准、备好降级路、服务端做兜底”这三板斧。只要这三件事做到位再奇怪的终端环境都有办法应对。另外如果业务上对目录结构的准确性要求极高一定要提前沟通清楚空文件夹的保留策略。这点客户往往不会主动提但你做完之后他大概率会拿一个带空目录的文件夹来测然后指出“这里怎么少了个文件夹”。与其到时候返工不如在需求确认阶段就把这个场景问清楚省得后面大家都不痛快。这个功能后续还可以扩展的方向挺多比如把目录树导出成zip结构预览、支持拖拽文件夹到页面上传、在服务端做目录层面的去重合并等等。底层的webkitRelativePath适配思路是相通的把这些基础打稳了后面再来什么需求都不慌。
阅读完成 · 觉得有帮助?
咨询建站