前言
下面所涉及到的完整代码 github 地址:
1.业务背景
最近在维护代码的时候,发现一个奇怪的问题,关于CSV下载的问题,我们有很多平台,然后有很多CSV下载的需求,然后有的是下载模板,下载完然后填好内容再次上传到平台上的,然后发现这很多的CSV文件下载分为三种情况:
-
1.一种是下载下来中文不乱码,文件格式也是对的,但是通过网页请求看数据,发现回来的数据是乱码的,这种属于好的,起码不影响使用。
-
2.第二种是回来的数据网页请求看没问题,不乱码,但是打开csv文件发现里面的中文乱码了,这种就影响使用了。
-
3.第三种是回来的数据网页请求看也没问题,不乱码,但是填完存的时候发现默认存的时候不是csv,是txt,也就是说虽然你下载的文件是以csv做后缀的,但是本质上是txt文件,这种也是有问题的,这种如果你只是下载下来看,不修改,也勉强能接受,但终归还是有问题的。
2.如何解决
面对上面的问题,我该如何解决呢!
1.从请求入手
首先,因为前端代码都是一样的,然后就需要对比他们请求的接口了,对比之后发现两个东西,一个返回的数据,上面的第一种中文是乱码的,然后后面两种都是正常的,再一个就是返回数据里的Response Headers中的Content-Type字段是不一样的,第一个的Content-Type: application/octet-stream,第二个和第三个都是Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,类似这种的,然后去查发现第二个和第三个都不是标准的Content-Type,后来才知道是自定义的一个告诉excel客户端,这是一个特殊文件。当然上面都是我的怀疑,怎么能知道真的就是接口返回的Content-Type的问题呢,你说直接找后端,后端不认说第一种不就可以用吗,你都是同一个代码下载的,怼的你都没话说,你还得自己回来找原因,那么不如直接先把原因找到。
2.自己写接口测试
既然我们怀疑是接口的问题,那我们就可以写一个接口进行测试了,这就是会一门后端语言的好处啊,直接拿起我们的Node,写一个接口还是非常简单的。然后刚开始写了如下的接口代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const express = require('express');
const cors = require('cors');
const app = express();
// 配置CORS,允许跨域请求
app.use(cors());
app.get('/api/csv-data', (req, res) => {
res.setHeader('Content-Type','text/csv')
const data = '数据1,数据2,数据3'
res.end(data)
});
const port = 3001;
// 启动服务
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
|
启动起来我们的Node接口就写好了,我们就能前端调用下载测试了,前端页面代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import axios from 'axios';
function App() {
const downloadFile = (content, filename) => {
// 创建一个 Blob 对象
const blob = new Blob([content], { type: 'application/octet-stream' });
// 创建一个 a 标签
const link = document.createElement('a');
// 设置 href 为 Blob 对象
link.href = URL.createObjectURL(blob);
// 设置下载的文件名
link.download = filename;
// 将 a 标签添加到 DOM
document.body.appendChild(link);
// 触发下载
link.click();
// 从 DOM 移除 a 标签
document.body.removeChild(link);
// 释放 Blob URL
URL.revokeObjectURL(link.href);
}
const csvDownLoad = () => {
axios.get(' http://localhost:3001/api/csv-data')
.then(response => {
downloadFile(response.data,'测试CSV下载.csv')
}).catch(error => {
console.log(error);
});
}
return <button onClick = {csvDownLoad}>下载CSV文件</button>;
}
export default App;
|
结果下载下来发现文件格式是对的,真的是csv文件,但是内容乱码了,咱也不知道这是啥字:
但是乱码了,大概能猜到是编码的问题,例如我们熟知的UTF-8,然后就开始沿着这个方向找问题。
3.解决编码问题
首先,我们要明白我们是用Excel打开的csv文件,那么Excel打开csv文件默认使用的编码是什么呢?根据搜索我们发现Excel打开csv文件使用的默认编码是跟操作系统的地区设置和Excel的版本有关系的,当然你在国内,Excel打开csv文件默认使用的编码一般是GBK编码。
既然知道了是GBK编码,那么我们就改一下接口,将编码格式变成GBK编码格式,下面是Node代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const express = require('express');
const cors = require('cors');
const app = express();
const iconv = require('iconv-lite');
// 配置CORS,允许跨域请求
app.use(cors());
// 读取CSV文件的接口
app.get('/api/csv-data', (req, res) => {
res.setHeader('Content-Type', 'text/csv; charset=GBK');
const data = '数据1,数据2,数据3';
const gbkBuffer = iconv.encode(data, 'GBK');
res.end(gbkBuffer);
});
const port = 3001;
// 启动服务
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
|
然后我们继续使用前面的前端代码进行下载,你会发现下载下来的文件打开依然是乱码,这是为什么呢,我们的数据已经采用GBK编码了,为什么打开还是乱码呢。此时我就在想是不是前端代码下载有问题,看了一下前端下载的代码,发现axios获取到的响应数据是作为一个UTF-8字符串处理的,而不是原始的字节流。因此,当你将其传递给Blob
对象并下载时,它是以UTF-8编码的。也就是说前端下载的时候还是以UTF-8编码的,这是不行的,所以还是得以后端接口给的原来得编码格式进行编码,这样才不会乱码。为了解决这个问题,我们就需要告诉axios将响应数据作为原始的字节流(ArrayBuffer)处理,而不是将其作为UTF-8字符串处理。可以通过设置responseType
为'arraybuffer'
来实现这一点。这样,当数据从服务器返回时,它将保持其原始的GBK编码。
前端修改后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import axios from 'axios';
function App() {
const downloadFile = (content, filename) => {
// 创建一个 Blob 对象
const blob = new Blob([content], { type: 'application/octet-stream' });
// 创建一个 a 标签
const link = document.createElement('a');
// 设置 href 为 Blob 对象
link.href = URL.createObjectURL(blob);
// 设置下载的文件名
link.download = filename;
// 将 a 标签添加到 DOM
document.body.appendChild(link);
// 触发下载
link.click();
// 从 DOM 移除 a 标签
document.body.removeChild(link);
// 释放 Blob URL
URL.revokeObjectURL(link.href);
}
const csvDownLoad = () => {
axios.get(' http://localhost:3001/api/csv-data',{
responseType: 'arraybuffer'
}).then(response => {
downloadFile(response.data,'测试CSV下载.csv')
}).catch(error => {
console.log(error);
});
}
return <button onClick = {csvDownLoad}>下载CSV文件</button>;
}
export default App;
|
用上面的代码再进行下载的时候,CSV文件不管格式和编码都是正确的了,也是解决了问题。当然我这是专门写了一个例子进行测试,探究这个问题背后的原因,既然问题已经找到了,原理也明白了,此时就可以找后端了,告诉他数据编码相关的问题需要改一下。上面刚开始说的业务背景中的第1项的原因应该是后端设置的响应头中的Content-Type设置有问题,第2项应该是编码问题,第3种就是接口返回数据的时候返回的本质上还是txt的数据。
3.总结思考
解决了上面的问题,我们再来反思一下解决问题的过程。首先是定位是哪里的问题,然后就是做一个简单的例子进行测试,把怀疑的点进行测试,然后发现最终的问题在哪。这里面最主要的点就是分析为什么会出现这个问题,例如Excel为什么打开csv会乱码,这是为什么,你知道是编码问题,那又是什么编码问题呢,一探究发现原来是Excel打开csv文件默认编码的问题。所以说代码这东西是比较诚实的,对就是对,错就是错,错了我们就去找寻背后的原因,然后解决它就行了,不要怕出现问题,出现问题咱们就解决问题。