当前位置: 首页 > news >正文

Node.js 文件上传中文文件名乱码问题,为什么只有Node会有乱码问题,其他后端框架少见?

问题现象

当用户上传包含中文字符的文件时,在服务器端获取到的文件名可能变成类似 æµ‹è¯•文件.txt 这样的乱码,而不是预期的中文文件名。

为什么只有Node会乱码?

  • 很多后端框架(如 Java Spring Boot、Python Django、PHP Laravel)为了简化开发,在底层已经处理了 “编码不匹配” 问题,开发者感知不到。
  • Node.js 的核心特点是 “轻量、原生模块仅提供基础能力,不做过度封装”,这导致它没有默认解决编码问题,需要开发者手动处理

问题根源

首先了解 HTTP 协议和 Node.js 处理请求的方式:

  1. HTTP 协议的历史遗留问题:早期的 HTTP 协议主要设计用于传输英文内容,默认采用 latin1(ISO-8859-1)编码。

  2. 表单提交的编码方式:当通过 multipart/form-data 格式上传文件时,浏览器会使用 latin1 编码来传输文件名等元数据,即使其中包含非拉丁字符。

  3. Node.js 的默认处理:Node.js 在解析请求时,默认会将这些 latin1 编码的数据直接转换为字符串,而 latin1 无法正确表示中文字符,从而导致乱码

简单来说,中文文件名被浏览器以 latin1 编码传输,但 Node.js 没有正确解码,导致了乱码现象。

latin1 是一种适合西欧语言的单字节编码,因历史原因成为早期互联网的默认编码,也因此导致了中文等多字节字符在传输中的乱码问题


解决方案

一、接收前端传递:解决这个问题的关键在于正确地解码文件名。我们可以使用 Node.js 的 Buffer 类来实现这一转换:

// 将乱码的文件名转换为正确的中文
const correctFilename = Buffer.from(originalname, "latin1").toString("utf8");
第一步:Buffer.from (originalname, "latin1")
  • originalname 是从请求中获取的原始文件名(已被错误解码为乱码)
  • 第二个参数 "latin1" 表示:把乱码的字符串按照 latin1 编码重新转换为字节序列
  • 这一步的作用是还原浏览器发送时的原始字节数据
第二步:.toString ("utf8")
  • 将上一步得到的原始字节序列,用正确的编码(utf8)重新解码为字符串
  • 这一步会把之前被拆分为单字节的中文字符重新组合为正确的多字节表示

为什么这样有效?

  • latin1 编码的特性是:每个字符都直接对应一个字节(0-255),不会丢失信息
  • 即使原始字符是 UTF-8 编码,用 latin1 解码成乱码后,依然可以通过反向操作还原
  • 这是一种 "-lossless"(无损失)的转换方式,专门用于修复此类编码不匹配问题

二、返回前端响应:同时,为了确保服务器返回的响应中中文能正确显示,我们需要设置响应头的字符集

告诉浏览器:“我(服务器)成功处理了你的请求,接下来会返回一段 HTML 格式的内容,并且这段内容是用 UTF-8 编码的,请你用 HTML 规则渲染、用 UTF-8 解码,确保界面正常显示且中文不乱码”。

// 设置响应头,确保中文正常显示
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});

建议放置到全局中间件

// 全局编码处理中间件
app.use((req, res, next) => {// 设置响应头确保UTF-8编码res.setHeader('Content-Type', 'application/json; charset=utf-8');// 处理请求中的文件名编码问题if (req.headers['content-type'] && req.headers['content-type'].includes('multipart/form-data')) {// 对于multipart/form-data请求,确保正确处理文件名编码req.setEncoding = 'utf8';}next();
});

完整示例(原生Nodejs示列)

const http = require('http');
const fs = require('fs');
const path = require('path');// 创建服务器
const server = http.createServer((req, res) => {// 处理 GET 请求 - 显示上传表单if (req.method === 'GET') {res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });res.end(`<form method="POST" enctype="multipart/form-data"><input type="file" name="file" /><button type="submit">上传文件</button></form>`);return;}// 处理 POST 请求 - 处理文件上传if (req.method === 'POST' && req.headers['content-type'].startsWith('multipart/form-data')) {// 获取分隔符const boundary = req.headers['content-type'].split('; ')[1].split('=')[1];let fileName = '';let fileData = [];let isFilePart = false;// 接收数据req.on('data', (chunk) => {// 转换为字符串用于解析文件名(临时用 latin1)const chunkStr = chunk.toString('latin1');// 提取并处理文件名(核心解决方案)if (!fileName && chunkStr.includes('filename="')) {const match = chunkStr.match(/filename="(.*?)"/);if (match && match[1]) {// 关键步骤:修复中文文件名乱码// 将 latin1 编码的文件名重新解码为 utf8fileName = Buffer.from(match[1], 'latin1').toString('utf8');}}// 收集文件内容if (fileName && !isFilePart && chunkStr.includes('\r\n\r\n')) {isFilePart = true;const start = chunkStr.indexOf('\r\n\r\n') + 4;fileData.push(chunk.slice(start - chunk.length));} else if (isFilePart && !chunkStr.includes(`--${boundary}--`)) {fileData.push(chunk);}});// 数据接收完成,保存文件req.on('end', () => {if (!fileName) {res.writeHead(400, { 'Content-Type': 'text/html;charset=utf-8' });return res.end('未找到文件');}// 合并并清理文件内容const fileBuffer = Buffer.concat(fileData);const endIndex = fileBuffer.lastIndexOf(Buffer.from(`--${boundary}--`));const cleanData = endIndex > 0 ? fileBuffer.slice(0, endIndex - 2) : fileBuffer;// 保存文件const savePath = path.join(__dirname, 'uploads', fileName);fs.writeFile(savePath, cleanData, (err) => {// 关键:设置响应编码为 utf8,确保返回中文正常显示res.writeHead(err ? 500 : 200, { 'Content-Type': 'text/html;charset=utf-8' });res.end(err ? '上传失败' : `文件上传成功: ${fileName}`);});});}
});// 启动服务器
const PORT = 3000;
server.listen(PORT, () => {console.log(`服务器运行在 http://localhost:${PORT}`);// 创建上传目录if (!fs.existsSync('./uploads')) fs.mkdirSync('./uploads');
});


文章转载自:

http://MVr8zLRF.yxwnn.cn
http://AS2UqPMO.yxwnn.cn
http://AiybYL1q.yxwnn.cn
http://6xqvso06.yxwnn.cn
http://brQma0Xy.yxwnn.cn
http://ZJJsP6nW.yxwnn.cn
http://KuYiZ6dF.yxwnn.cn
http://dmb9D1je.yxwnn.cn
http://nwv58Z6d.yxwnn.cn
http://kgcmkjHT.yxwnn.cn
http://R534JVyD.yxwnn.cn
http://PG8C88zE.yxwnn.cn
http://gSGqOvoP.yxwnn.cn
http://BiP55fAS.yxwnn.cn
http://fgdQFLse.yxwnn.cn
http://svCVMg0B.yxwnn.cn
http://QrGtnc4z.yxwnn.cn
http://9p8zsxC9.yxwnn.cn
http://DXSVGE4Q.yxwnn.cn
http://hsZ08zOd.yxwnn.cn
http://x6JxJ4w5.yxwnn.cn
http://lJu22GgO.yxwnn.cn
http://76nHdDqm.yxwnn.cn
http://MBjU2Vpr.yxwnn.cn
http://5uiV6xji.yxwnn.cn
http://nlv9KbWl.yxwnn.cn
http://iEUGJzHZ.yxwnn.cn
http://gOrmC61d.yxwnn.cn
http://t0z65SQy.yxwnn.cn
http://QiAJovEm.yxwnn.cn
http://www.dtcms.com/a/385989.html

相关文章:

  • Redis 线上遍历 Key 的正确姿势:SCAN 命令详解
  • 【软考】笔记总结二
  • gemini cli 一个可以参考的prompt
  • 第9章 Prompt提示词设计
  • 嘉银科技基于阿里云 Kafka Serverless 提升业务弹性能力,节省成本超过 20%
  • 信任链验证流程
  • 从技术视角解析加密货币/虚拟货币/稳定币的设计与演进
  • Redis(高性能数据处理、NOSQL、分库分表)
  • CI/CD开发工作流实践技术日志
  • 小程序调用地图api
  • 数字人分身系统源码/网页端+移动小程序端技术开发方案
  • 对等实体认证:筑牢网络安全防线
  • 工作量证明(PoW)
  • uniapp微信小程序自定义头部导航栏后怎么设置时间、电量等样式
  • App 上架流程全解析 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核经验分享
  • 66_基于深度学习的花卉检测识别系统(yolo11、yolov8、yolov5+UI界面+Python项目源码+模型+标注好的数据集)
  • Chromium 138 编译指南 macOS 篇:环境配置与准备(一)
  • 系统清理优化工具Ashampoo WinOptimizer v28.00.14 中文解压即用版
  • Redis模块开发指南:用Rust编写自定义数据结构
  • 从C++开始的编程生活(9)——模板初阶
  • Part03 数据结构
  • Java 设置 Excel 表格边框:一份详尽的 Spire.XLS 教程
  • Electron + Vue2 IPC 通讯实例
  • 【工具代码】使用Python截取视频片段,截取视频中的音频,截取音频片段
  • 《百日战纪:最终防卫学园》体验版在Steam平台推出!
  • 服务器 IPMI 实战:从 BMC 认知到 ipmitool 命令行运维
  • Cancer Cell最新空间组学研究|香港科技大学王吉光/天坛医院江涛院士团队合作提出IDH突变型星形细胞瘤的新分类标准
  • MissionPlanner架构梳理之(十四)日志浏览
  • 搭建论坛用什么服务器好?论坛服务器配置要求
  • 两台电脑如何共享“共享文件夹”