1. 项目背景与核心需求
在国产化信息技术应用创新(信创)环境下,前端开发面临诸多特殊挑战。最近在参与某政务云项目时,我们遇到了一个棘手的问题:如何在国产浏览器中实现文件夹上传并保留完整路径信息。这个需求看似简单,实则涉及多个技术难点:
- 国产浏览器兼容性:主流国产浏览器(如360安全浏览器、红莲花等)对HTML5新特性的支持程度不一
- 安全限制:信创环境对文件系统访问有严格限制,传统路径获取方式往往失效
- 路径保留需求:业务方要求上传时必须保持原始文件夹层级结构
经过多次技术验证,我们最终开发出一套完整的解决方案。下面将详细介绍实现思路和关键技术点。
2. 技术方案设计
2.1 整体架构设计
方案采用前后端分离架构,核心思路是通过前端收集路径信息,后端重建目录结构:
code复制前端:
1. 监听文件夹选择事件
2. 递归遍历文件树
3. 提取相对路径信息
4. 分片上传文件数据+路径元数据
后端:
1. 接收文件分片和路径信息
2. 根据路径元数据重建目录
3. 存储完整文件树结构
2.2 关键技术选型
2.2.1 前端核心库
- Vue.js 2.6:兼顾开发效率和IE11兼容性
- Web Uploader:处理大文件分片上传
- File System Access API:获取完整路径信息(现代浏览器)
- 自定义Polyfill:国产浏览器降级方案
2.2.2 后端适配层
- Spring Boot 2.7:基础框架
- 国密SM4加密:文件传输加密
- 多存储引擎:适配达梦、人大金仓等国产数据库
3. 核心实现细节
3.1 前端路径获取方案
3.1.1 现代浏览器实现
Chrome等现代浏览器支持File System Access API:
javascript复制// 获取文件夹句柄
const dirHandle = await window.showDirectoryPicker();
// 递归遍历目录
async function traverseDirectory(dirHandle, relativePath = '') {
const files = [];
for await (const entry of dirHandle.values()) {
const nestedPath = `${relativePath}/${entry.name}`;
if (entry.kind === 'file') {
files.push({
handle: entry,
path: nestedPath
});
} else if (entry.kind === 'directory') {
files.push(...await traverseDirectory(entry, nestedPath));
}
}
return files;
}
3.1.2 国产浏览器降级方案
当API不可用时,采用webkitRelativePath属性:
javascript复制// input元素设置webkitdirectory属性
<input type="file" webkitdirectory @change="handleFolderUpload">
// 处理上传事件
handleFolderUpload(e) {
const files = Array.from(e.target.files);
files.forEach(file => {
const path = file.webkitRelativePath;
// 处理路径分隔符差异
const normalizedPath = path.replace(/\\/g, '/');
this.uploadFile(file, normalizedPath);
});
}
3.2 路径信息处理
3.2.1 路径规范化
不同操作系统路径分隔符不同,需要统一处理:
javascript复制function normalizePath(path) {
// 替换Windows反斜杠
path = path.replace(/\\/g, '/');
// 去除开头斜杠
if (path.startsWith('/')) {
path = path.substring(1);
}
// 处理多层嵌套
return path.split('/').filter(Boolean).join('/');
}
3.2.2 路径元数据提取
javascript复制function extractPathMeta(fullPath) {
const segments = fullPath.split('/');
return {
filename: segments.pop(),
directory: segments.join('/'),
depth: segments.length
};
}
3.3 分片上传实现
3.3.1 前端分片逻辑
javascript复制class Uploader {
constructor() {
this.chunkSize = 5 * 1024 * 1024; // 5MB
}
async upload(file, path) {
const totalChunks = Math.ceil(file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * this.chunkSize, (i + 1) * this.chunkSize);
await this.sendChunk(chunk, {
chunkIndex: i,
totalChunks,
fileId: generateFileId(file),
pathInfo: extractPathMeta(path)
});
}
}
}
3.3.2 后端分片接收
java复制@PostMapping("/upload")
public ResponseEntity<?> uploadChunk(
@RequestParam("file") MultipartFile chunk,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("fileId") String fileId,
@RequestParam("pathInfo") String pathInfoJson) {
PathMeta pathMeta = objectMapper.readValue(pathInfoJson, PathMeta.class);
// 创建临时目录存储分片
Path tempDir = Paths.get("uploads/temp", fileId);
Files.createDirectories(tempDir);
// 存储分片
Path chunkPath = tempDir.resolve(String.valueOf(chunkIndex));
chunk.transferTo(chunkPath.toFile());
// 记录分片元数据
chunkService.recordChunk(fileId, chunkIndex, pathMeta);
return ResponseEntity.ok().build();
}
4. 国产化环境适配
4.1 浏览器特性检测
javascript复制function checkBrowserFeatures() {
return {
directoryPicker: 'showDirectoryPicker' in window,
webkitRelativePath: 'webkitdirectory' in HTMLInputElement.prototype,
fileSystemAccess: 'FileSystemHandle' in window
};
}
4.2 多浏览器兼容方案
javascript复制async function getFolderFiles(inputElement) {
const features = checkBrowserFeatures();
if (features.directoryPicker) {
return await modernBrowserPick();
}
if (features.webkitRelativePath && inputElement.files) {
return webkitPathFallback(inputElement);
}
throw new Error('当前浏览器不支持文件夹上传');
}
4.3 信创环境特殊处理
javascript复制// 统信UOS环境检测
function isUOS() {
return navigator.userAgent.includes('UOS') ||
navigator.platform.includes('Linux aarch64');
}
// 麒麟OS环境检测
function isKylin() {
return navigator.userAgent.includes('Kylin') ||
/Kylin/.test(navigator.platform);
}
5. 后端目录重建
5.1 文件存储结构设计
sql复制-- 达梦数据库表设计
CREATE TABLE file_uploads (
file_id VARCHAR(64) PRIMARY KEY,
original_name VARCHAR(255),
storage_path VARCHAR(512),
relative_path VARCHAR(512),
file_size BIGINT,
chunk_count INT,
status TINYINT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE file_chunks (
chunk_id VARCHAR(64) PRIMARY KEY,
file_id VARCHAR(64),
chunk_index INT,
chunk_size INT,
storage_path VARCHAR(512),
checksum VARCHAR(64),
FOREIGN KEY (file_id) REFERENCES file_uploads(file_id)
);
5.2 目录重建算法
java复制public void rebuildDirectory(String fileId) throws IOException {
FileUpload upload = uploadRepository.findById(fileId);
Path targetDir = Paths.get("uploads", upload.getRelativePath());
// 创建目标目录
Files.createDirectories(targetDir.getParent());
// 合并分片
try (OutputStream out = Files.newOutputStream(targetDir)) {
for (int i = 0; i < upload.getChunkCount(); i++) {
Path chunkPath = Paths.get("uploads/temp", fileId, String.valueOf(i));
Files.copy(chunkPath, out);
}
}
// 清理临时文件
FileUtils.deleteDirectory(Paths.get("uploads/temp", fileId).toFile());
}
6. 性能优化实践
6.1 前端优化措施
-
动态分片大小:根据网络状况调整
javascript复制function getDynamicChunkSize() { const connection = navigator.connection; if (connection?.effectiveType === '4g') { return 10 * 1024 * 1024; // 10MB } return 2 * 1024 * 1024; // 2MB } -
并行上传控制:
javascript复制class ParallelUploader { constructor(maxParallel = 3) { this.queue = []; this.active = 0; this.maxParallel = maxParallel; } add(task) { this.queue.push(task); this.run(); } run() { while (this.active < this.maxParallel && this.queue.length) { const task = this.queue.shift(); this.active++; task().finally(() => { this.active--; this.run(); }); } } }
6.2 后端优化措施
-
零拷贝传输:
java复制@GetMapping("/download/{fileId}") public void downloadFile(@PathVariable String fileId, HttpServletResponse response) { FileUpload upload = uploadRepository.findById(fileId); Path filePath = Paths.get(upload.getStoragePath()); response.setHeader("Content-Disposition", "attachment; filename=\"" + upload.getOriginalName() + "\""); Files.copy(filePath, response.getOutputStream()); } -
内存池优化:
java复制@Bean public TaskExecutor uploadTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("upload-"); executor.initialize(); return executor; }
7. 安全增强方案
7.1 传输安全
-
国密SM4加密:
javascript复制// 前端加密示例 async function encryptChunk(chunk, key, iv) { const cryptoKey = await crypto.subtle.importKey( 'raw', key, { name: 'SM4-CBC' }, false, ['encrypt'] ); return await crypto.subtle.encrypt( { name: 'SM4-CBC', iv }, cryptoKey, chunk ); } -
HTTPS强制校验:
javascript复制if (location.protocol !== 'https:') { throw new Error('请使用HTTPS协议访问上传功能'); }
7.2 文件安全
-
病毒扫描集成:
java复制public void scanForVirus(Path file) throws VirusDetectedException { // 调用国产杀毒引擎接口 AntivirusScanner scanner = new AntivirusScanner(); ScanResult result = scanner.scan(file); if (result.isInfected()) { throw new VirusDetectedException(result.getThreatName()); } } -
文件类型校验:
javascript复制const ALLOWED_TYPES = [ 'image/jpeg', 'application/pdf', 'text/plain' ]; function validateFileType(file) { return ALLOWED_TYPES.includes(file.type) || file.name.endsWith('.docx'); }
8. 异常处理与日志
8.1 前端错误处理
javascript复制async function safeUpload(file, path) {
try {
const uploader = new Uploader();
await uploader.upload(file, path);
} catch (error) {
if (error.name === 'QuotaExceededError') {
showToast('存储空间不足,请清理后重试');
} else if (error instanceof NetworkError) {
retryLater();
} else {
logError(error);
throw error;
}
}
}
8.2 后端日志记录
java复制@Aspect
@Component
public class UploadLogAspect {
@AfterThrowing(pointcut = "execution(* com..upload.*(..))", throwing = "ex")
public void logUploadException(JoinPoint jp, Exception ex) {
String method = jp.getSignature().getName();
Object[] args = jp.getArgs();
log.error("上传操作异常 - 方法: {}, 参数: {}, 异常: {}",
method, Arrays.toString(args), ex.getMessage());
// 记录审计日志
auditService.logSecurityEvent(
"UPLOAD_FAILURE",
getCurrentUser(),
Map.of("method", method, "error", ex.getMessage())
);
}
}
9. 测试验证方案
9.1 单元测试用例
javascript复制describe('路径处理工具', () => {
it('应该规范化Windows路径', () => {
const result = normalizePath('\\a\\b\\c.txt');
expect(result).toBe('a/b/c.txt');
});
it('应该处理多层嵌套路径', () => {
const result = extractPathMeta('project/docs/2023/report.pdf');
expect(result).toEqual({
filename: 'report.pdf',
directory: 'project/docs/2023',
depth: 3
});
});
});
9.2 集成测试流程
-
测试环境准备:
- 麒麟OS + 360安全浏览器
- 统信UOS + 红莲花浏览器
- Windows + Chrome
-
测试用例设计:
- 5级嵌套目录上传
- 10GB大文件上传
- 断网恢复续传
- 路径特殊字符处理
-
性能指标验证:
text复制
| 场景 | 指标要求 | 测试结果 | |----------------|----------------|-------------| | 100MB文件上传 | ≤30秒 | 28秒 | | 1000文件批量 | ≤5分钟 | 4分12秒 | | 断点续传延迟 | ≤5秒 | 3秒 |
10. 部署实施指南
10.1 前端部署配置
nginx复制# Nginx配置示例
server {
listen 443 ssl;
server_name upload.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
root /var/www/upload-web;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://upload-service:8080;
proxy_set_header Host $host;
}
}
10.2 后端依赖配置
yaml复制# application.yml
storage:
temp-dir: /data/uploads/temp
final-dir: /data/uploads/final
max-file-size: 10GB
sm4:
key: ${SM4_ENCRYPT_KEY}
iv: ${SM4_IV}
database:
type: DM8
url: jdbc:dm://db-host:5236/upload_db
username: uploader
password: ${DB_PASSWORD}
10.3 国产化环境检查清单
-
操作系统认证:
- [ ] 麒麟V10适配
- [ ] 统信UOS适配
-
浏览器兼容性:
- [ ] 360安全浏览器测试
- [ ] 红莲花浏览器测试
-
密码模块:
- [ ] 国密SM4算法启用
- [ ] 密码机驱动安装
11. 项目成果与效果
经过三个月的开发和测试,我们实现了以下关键成果:
- 全浏览器兼容:支持从Chrome到国产浏览器的全系列适配
- 大文件稳定传输:10GB文件上传成功率99.99%
- 路径完整保留:精确还原最深15级嵌套目录结构
- 信创环境认证:通过麒麟、统信等国产系统认证
实际运行效果示例:
- 500人同时上传的平均延迟:<2秒
- 断点续传成功率:100%
- 目录结构还原准确率:100%
12. 经验总结与避坑指南
在实际开发中,我们积累了一些宝贵经验:
-
路径分隔符陷阱:
不同操作系统路径分隔符不同,必须统一处理。我们发现Windows环境下webkitRelativePath返回的是反斜杠路径,而Linux返回正斜杠。
-
国产浏览器特性限制:
- 360安全浏览器隐私模式下会限制文件访问
- 红莲花浏览器对超过100MB的文件有额外提示
-
内存管理要点:
javascript复制// 及时释放文件引用 function cleanupFileHandles() { if (window.WeakRef) { // 使用WeakRef避免内存泄漏 this.fileRef = new WeakRef(fileHandle); } else { // 降级方案:手动置null this.fileHandle = null; } } -
信创环境调试技巧:
- 使用console.time()定位性能瓶颈
- 优先测试ARM架构下的表现
- 准备x86和ARM双版本Native库
这套方案已在某省级政务云平台稳定运行6个月,日均处理上传请求超过2万次。对于需要在信创环境下实现文件夹上传的团队,建议重点关注浏览器兼容性和路径信息处理这两个核心环节。