注册
web

你不常用的 FileReader 能干什么?

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件
  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取

总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:



如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

0 个评论

要回复文章请先登录注册