基于NodeJS/Express实现Form Data上传的全控制与进度追踪
手动处理Form Data上传(不依赖第三方模块)
核心逻辑
Form Data基于multipart/form-data编码传输,需要手动解析该编码格式以拆分文本字段和文件内容;同时监听数据流实时计算上传进度,通过WebSocket推送至客户端,最后对文件做尺寸、画质调整后多位置保存。
1. 从请求头提取边界符
multipart/form-data的请求头中会携带boundary参数,这是分隔各个字段的标记,先将其提取出来:
const contentType = req.headers['content-type']; const boundary = contentType.split('boundary=')[1]; if (!boundary) { res.writeHead(400); res.end('无效的Form Data格式'); return; }
2. 实时计算并推送上传进度
从Content-Length请求头获取总数据大小,每次接收数据时累加已接收量,计算进度百分比后通过WebSocket发送给客户端:
let receivedSize = 0; const totalSize = parseInt(req.headers['content-length'], 10); // 假设已通过ws模块建立WebSocket连接(client为连接实例) req.on('data', (chunk) => { receivedSize += chunk.length; const progress = Math.round((receivedSize / totalSize) * 100); ws.send(JSON.stringify({ type: 'uploadProgress', progress })); });
3. 手动解析multipart数据流
将接收的所有数据块拼接后,用边界符拆分各个字段,分别提取文本内容和文件Buffer:
let chunks = []; req.on('data', (chunk) => chunks.push(chunk)) .on('end', () => { const fullBuffer = Buffer.concat(chunks); // 按边界符拆分数据流,过滤空的分隔块 const parts = fullBuffer.toString().split(`--${boundary}`).filter(part => part.trim()); const formData = { fields: {}, files: {} }; parts.forEach(part => { // 拆分字段头部和内容(以两个换行符为分隔) const [headerSection, content] = part.split('\r\n\r\n'); if (!headerSection || !content) return; // 从头部提取字段名和文件名 const nameMatch = headerSection.match(/name="([^"]+)"/); const filenameMatch = headerSection.match(/filename="([^"]+)"/); if (nameMatch) { const fieldName = nameMatch[1]; if (filenameMatch) { // 处理文件字段:移除内容末尾换行符并转为Buffer const filename = filenameMatch[1]; const fileBuffer = Buffer.from(content.trimEnd(), 'binary'); formData.files[fieldName] = { filename, buffer: fileBuffer }; } else { // 处理文本字段 formData.fields[fieldName] = content.trimEnd(); } } }); // 拿到解析后的Form Data,开始处理文件 handleProcessedFile(formData); });
4. 处理文件并多位置保存
使用sharp库调整图片尺寸和画质,并行保存至多个路径:
const sharp = require('sharp'); const fs = require('fs/promises'); async function handleProcessedFile(formData) { // 假设客户端上传的文件字段名为avatar,文本字段名为username const { avatar } = formData.files; const { username } = formData.fields; // 调整图片参数:裁剪为300x300,JPG画质80% const processedBuffer = await sharp(avatar.buffer) .resize(300, 300, { fit: 'cover' }) .jpeg({ quality: 80 }) .toBuffer(); // 并行保存到多个位置 await Promise.all([ fs.writeFile(`./uploads/origin/${avatar.filename}`, avatar.buffer), // 原图 fs.writeFile(`./uploads/thumb/${username}_avatar.jpg`, processedBuffer), // 缩略图 fs.writeFile(`./uploads/backup/${avatar.filename}`, processedBuffer) // 备份 ]); // 通知客户端上传完成 ws.send(JSON.stringify({ type: 'uploadComplete', msg: '文件处理并保存完成' })); res.writeHead(200); res.end('上传成功'); }
注意事项
- 大文件上传时不要一次性将所有数据块存入内存,需改为流式解析,避免内存溢出
- 文本字段包含非ASCII字符时,确保编码为UTF-8
- 增加错误捕获:解析失败、文件处理出错时,及时向客户端返回错误响应并通过WebSocket同步错误信息
内容的提问来源于stack exchange,提问作者Theme Farmer
相关产品推荐
相关产品推荐

