likes
comments
collection
share

中文乱码不再是难题,轻松搞定文件上传!

作者站长头像
站长
· 阅读数 54

背景

相信大家都碰到过上传文件或者下载文件,中文乱码的事情,希望看完本篇对你会有帮助。

故事还得从这次使用postman及apifox,上传文件到nodejs的服务时,nodejs服务这边收到的文件名中,中文部分是乱码的说起,如下图所示 中文乱码不再是难题,轻松搞定文件上传!

之前从浏览器上传中文名称文件到后端,文件名展示是正常的,然后我怕浏览器也有问题了,毕竟项目依赖可能会升级,导致结果发生变化,于是用浏览器表单上传同样的文件看看,发现通过浏览器上传中文名是没有乱码的,如下图所示 中文乱码不再是难题,轻松搞定文件上传!

这是怎么回事?不过想到这样的问题,肯定有很多人碰到过,所以先谷歌看看 中文乱码不再是难题,轻松搞定文件上传!

确实有很多人碰到这个问题,逐个点进去看,发现解决方案如下

方案1: 编码问题导致的乱码,所以在重新编码即可,如下所示 中文乱码不再是难题,轻松搞定文件上传!

方案2: 给上传文件的插件传入编码参数,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

方案3: 在header头中指定编码 中文乱码不再是难题,轻松搞定文件上传!

等等还有一些方式,有的场景,可能按照上面的方案试下来问题解决了,但是我试完了上面提到的方法,都没有解决,于是看了下nodejs服务这边上传问题,大概是怎么处理的,并记录下来,方便后面排查问题

分析

依赖关系分析

粗略看了下不论是用的koa、express这样的库,还是nestjs等这样框架,看了一圈之后,发现nodejs里面处理文件上传的,差不多都会依赖multer这个包

中文乱码不再是难题,轻松搞定文件上传!

multer这个包内上传文件的部分其实依赖的又是busboy这个包,来处理问题

中文乱码不再是难题,轻松搞定文件上传!

而busboy是有两个不同的使用版本,1.0版本相对于0.x版本是有breakchange的

中文乱码不再是难题,轻松搞定文件上传!

到这里我们已经知道,上传文件是busboy这个包处理的,而busboy这个包目前有两个不同的大版本1.x与0.x,出现中文乱码,肯定是这个包的问题

于是我分别看了下这两个版本,针对上传文件会有什么不同

busboy版本分析

首先看下我项目中正常使用的版本0.2.14

中文乱码不再是难题,轻松搞定文件上传!

0.2.14版本浏览器上传

通过断点查看,文件名是从header头中的content-disposition中取出来的,如下图所示,此时的文件名中文部分看起来是乱码

中文乱码不再是难题,轻松搞定文件上传!

解析content-disposition里面的参数之后,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

在讲文件名filename解码一次

中文乱码不再是难题,轻松搞定文件上传!

中文乱码不再是难题,轻松搞定文件上传!

也就是将filename从binary解码成utf8,最后可以看到中文名正常展示,没有乱码

中文乱码不再是难题,轻松搞定文件上传!

0.2.14版本postman等工具上传

与浏览器不同的是,header头中的content-disposition中,filename有两个,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

解析content-disposition里面的参数之后,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

断点会发现解析下标2的之后,filename是正常的中文名,但是解析下标3之后,中文名就乱码了,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

原因是因为filename*对应的文件名本身就已经是中文了,然后又被解码了,即

filename = decodeText('这是一个测试abc.xlsx', 'binary', 'utf8'); // 成乱码了

最终控制台输出如下图所示 中文乱码不再是难题,轻松搞定文件上传!

这时候,使用谷歌中的解决方案,Buffer.from(filename, 'latin1').toString('utf8')会发现转不成中文了,同时busboy也不支持defParamCharset参数,最终没有找到简单的解决方法

接着看busboy1.x的版本,这里以1.6.0为例

1.6.0版本浏览器上传

从header内的content-disposition获取文件名

中文乱码不再是难题,轻松搞定文件上传!

可以看到获取到filename中文是乱码的,与0.2.14中是一致的

中文乱码不再是难题,轻松搞定文件上传!

这时候,我们自己在代码中,通过转码处理Buffer.from(filename, 'latin1').toString('utf8'),中文就会正常输出了,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

还有一种传入defParamCharset: 'utf-8' 这样就会默认使用utf8来解码乱码的中文字符,这样获取到的filename也是正常的中文字符了

中文乱码不再是难题,轻松搞定文件上传!

中文乱码不再是难题,轻松搞定文件上传!

中文乱码不再是难题,轻松搞定文件上传!

1.6.0版本postman等工具上传

与浏览器不同的是,header头中的content-disposition中,filename有两个,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

这里解析出来的内容与0.2.4版本一致

中文乱码不再是难题,轻松搞定文件上传!

但是这里的取filename的方式,发生了变化,不再是通过遍历的方式获取,而是直接通过filename获取,且filename*的优先级高于filename

中文乱码不再是难题,轻松搞定文件上传!

最终控制台展示正常,如下图所示

中文乱码不再是难题,轻松搞定文件上传!

可以看到使用postman上传文件,文件名中的中文不是乱码的,而是正确的中文名,从上面可以看到header['content-disposition']中有两个filename,一个是filename="这是一个测试abc.xlsx"; 另一个是filename*=UTF-8''%E8%BF%99%E6%98%AF%E4%B8%80%E4%B8%AA%E6%B5%8B%E8%AF%95abc.xlsx

补充

上面验证完之后,可能会有一些问题:

  1. 为什么content-disposition收到的filename字段中中文字符是乱码?
  2. 为什么使用Buffer.from(filename, 'latin1').toString('utf8'),在1.x版本可以将乱码还原成中文字符,而在0.x版本中却不行
  3. 为什么header['content-disposition']会有两个filename字段?

为什么content-disposition收到的filename字段中,中文字符是乱码?

因为http协议规定,http头中携带的字符只能是ISO-8859-1字符集中的字符,所以中文被转码了,而ISO-8859-1是单字节编码,自身不能显示中文,若要显示中文,必须和其他能显示中文的编码配合,如“GBK”,“UTF-8"。

简单来说,要将中文转成http协议允许的编码,也就是ISO-8859-1,用nodejs代码来编码就是,先将中文转成能够编码中文的utf8,然后再将编码后的uft8字符在做ISO-8859-1编码

// 注意nodejs中latin1 or binary代表的就是ISO-8859-1编码
Buffer.from('这是一个测试abc.xlsx', 'utf8').toString('binary') // 返回编码结果

Buffer.from('这是一个测试abc.xlsx', 'utf8').toString('latin1')

中文乱码不再是难题,轻松搞定文件上传!

中文编码之后的结果如下所示

中文乱码不再是难题,轻松搞定文件上传!

然后在通过抓包工具,对比浏览器专递的最终编码字符是不是一样的,从当前给的案例看,是一样的 中文乱码不再是难题,轻松搞定文件上传!

为什么使用Buffer.from(filename, 'latin1').toString('utf8'),在1.x版本可以将乱码还原成中文字符,而在0.x版本中却不行

原因是因为在1.x中filename本身就是latin1编码,而lantin1就是ISO-8859-1的别名,所以能够将中文又解码回来,过程如下所示

const filename = Buffer.from('这是一个测试abc.xlsx', 'utf8').toString('binary') // http传递的编码

// 将http传递的编码,在解码成utf8
Buffer.from(filename, 'latin1').toString('utf-8') // 返回正确的中文名

而在0.x是因为,直接将中文先通过binary编码,然后在通过utf8编码,这样是解码不回来的

const filename = Buffer.from('这是一个测试abc.xlsx', 'binary').toString('utf8')

Buffer.from(filename, 'utf8').toString('binary') // 解码不回来的

为什么header['content-disposition']会有两个filename字段?

为了国际化考虑,允许通过filename来指定ISO-8859-1之外的字符,当不支持filename的则会忽略,具体可以查看how-to-encode-the-filename-parameter-of-content-disposition-header-in-http,部分截图如下所示

中文乱码不再是难题,轻松搞定文件上传!

所以这里可以总结,浏览器上传文件时,只带了一个filename字段在content-disposition字段中,而postman这类工具,带了filename还带了filename*

解决

从上面了解busboy不同的版本上传情况不同之后,那么解决该问题的时候,我们就需要根据实际项目中的版本进行解决,下面是具体的解决方案

针对busboy1.x版本

busboy1.x版本下中文乱码如下

  • 浏览器上传文件,中文文件名默认是会乱码的
  • postman等工具上传文件,中文文件名默认是会正常展示

所以最简单的解决方法,就是传入defParamCharset: 'utf-8',或者自己在代码里面接着转码一次,需要做一个判断如下所示,这样就可以解决所有上传场景

if (!/[^\u0000-\u00ff]/.test(value.originalname)) {
  value.originalname = Buffer.from(value.originalname, 'latin1').toString(
    'utf8',
  );
}

这也就是网上看到的大部分的解决方案,这种方案适合busboy1.x版本的

针对busboy0.x版本

busboy0.x版本下

  • 浏览器上传文件,中文文件名默认显示正常
  • postman等工具上传文件,中文文件名默认是会乱码的

因为0.x与1.x解析文件名filename的方式不一样,0.x是遍历的方式,所以filename*会覆盖filename

中文乱码不再是难题,轻松搞定文件上传!

而1.x不是遍历,而是优先获取filename*

中文乱码不再是难题,轻松搞定文件上传!

那么针对0.x怎么解决中文乱码呢?

1、可以升级对应的multer依赖、这里需要注意的是升级multer需要升级到可能会遇到esm模块与cjs模块混用的错误,确保multer依赖的busboy是1.x版本

2、当然也可以不用中间件,直接升级busboy,然后在项目内自己直接使用busboy,如下所示

@Post('/file')
async file(@Ctx() ctx: any): Promise<any> {

  const bb = busboy({ headers: ctx.req.headers, defParamCharset: 'utf-8' });

  await new Promise((resolve, reject) => {
    bb.on('file', (name, file, info) => {
      const { filename, encoding, mimeType } = info;
      console.log(
        `File [${name}]: filename: %j, encoding: %j, mimeType: %j`,
        filename,
        encoding,
        mimeType
      );

      

      const saveTo = path.join(process.cwd(), filename);
      file.pipe(fs.createWriteStream(saveTo));

    });

    bb.on('field', (name, val, info) => {
      console.log(`Field [${name}]: value: %j`, val);
    });

    bb.on('close', () => {
      console.log('Done parsing form!');
      resolve(1)
    });

    ctx.req.pipe(bb);
  })

  return 'hello world file'
}

3、可以patch busboy包,在获取filename那里加一行代码

diff --git a/node_modules/busboy/lib/types/multipart.js b/node_modules/busboy/lib/types/multipart.js
index b6d8e8b..d415020 100644
--- a/node_modules/busboy/lib/types/multipart.js
+++ b/node_modules/busboy/lib/types/multipart.js
@@ -156,6 +156,7 @@ function Multipart(boy, cfg) {
           if (RE_NAME.test(parsed[i][0])) {
             fieldname = decodeText(parsed[i][1], 'binary', 'utf8');
           } else if (RE_FILENAME.test(parsed[i][0])) {
+            if (filename !== undefined) continue;
             filename = decodeText(parsed[i][1], 'binary', 'utf8');
             if (!preservePath)
               filename = basename(filename);

总结

上传文件,中文名称乱码,主要还是中文编码的问题以及依赖的依赖问题,当某个底层依赖出现问题的时候,在项目内修改是成本比较高的,所以应该尽可能的降低这种依赖的依赖场景

其次浏览器上传文件与postman等工具上传文件在header字段中的content-disposition属性有差异,会出现filename*的情况

最后用一个表格总结上传文件中文乱码情况

版本默认是否正常展示中文名解决方法
0.x 浏览器上传
0.x postman等上传升级到1.x or patch busboy代码
1.x 浏览器上传设置defParamCharset: 'utf-8'
1.x postman等上传

最终解决方案,推荐使用busboy 1.x版本,并且设置defParamCharset: 'utf-8'

转载自:https://juejin.cn/post/7366177423775449088
评论
请登录