导出文件实现

最近在做后台管理的时候有个导出记录的功能,我这边踩了个坑。后台接口是以流的形式把数据给我们,我这边是用 Blob 对象进行下载。如果不了解 Blob 的小伙伴可以查阅一下相关资料。我们来看一下用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
axios
.post('/demo', data, {
responseType: 'arraybuffer',
})
.then((res) => {
const blob = new Blob([res], {
type:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8',
});
const downloadElement = document.createElement('a');
const href = window.URL.createObjectURL(blob); // 创建下载的链接
downloadElement.href = href;
downloadElement.download = ''; // 下载后文件名
document.body.appendChild(downloadElement);
downloadElement.click(); // 点击下载
document.body.removeChild(downloadElement); // 下载完成移除元素
window.URL.revokeObjectURL(href); // 释放掉blob对象

我这边用 axios 去发起请求,这是传参的情况,如果没有可以不写,这样导出的功能其实就实现了。不过在测试测兼容性的过程中发现了 IE 下报错,提示拒绝访问。搜了一下,大多数都是说要判断 IE 用 window.navigator.msSaveOrOpenBlob 去处理。我也试了一下,下载是可以下载,但是文件一直在加载中,下载不下来。不知道什么原因导致的,排查了也没有什么头绪,所以换了个思路,改用 form 表单的方式去下载,以下是基于 react 封装的插件:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import React from "react";
import ReactDOM from "react-dom";
import { message } from "antd";

class ExportForm extends React.Component {
constructor(props) {
super(props);

this.renderList = this.initRenderList(props.query);
this.form = { current: null };
this.iframe = { current: null };

this.handler = this.handler.bind(this);
}

UNSAFE_componentWillReceiveProps(nextP) {
const { props } = this;
if (props.query !== nextP.query) {
this.renderList = this.initRenderList(nextP.query);
this.forceUpdate();
}
}

componentDidMount() {
this.iframe.current.addEventListener("load", this.handler);
this.export();
}

componentWillUnmount() {
this.iframe.current.removeEventListener("load", this.handler);
}

componentDidUpdate() {
this.export();
}

handler() {
this.props.reject();
try {
const obj = JSON.parse(
this.iframe.current.contentWindow.document.body.innerText
);
if (String(obj.code) !== "200") {
message.warning(obj.message);
}
} catch (e) {
message.warning(
this.iframe.current.contentWindow.document.body.innerText
);
}
// message.warning('导出失败,请联系管理员!');
}

initRenderList(query) {
const list = [];
for (const key in query) {
if (!query[key]) continue;
list.push({ name: key, value: query[key] });
}
return list;
}

export() {
if (!this.props.url || !this.renderList.length) return;
this.form.current.submit();
setTimeout(() => {
this.props.resolve();
}, 1000);
}

render() {
return (
<>
<iframe name="export-target" ref={this.iframe} />
<form
method={this.props.methods}
action={this.props.url}
ref={this.form}
target="export-target"
>
{this.renderList.map((item) => (
<input
type="hidden"
name={item.name}
value={item.value}
key={item.name}
/>
))}
</form>
</>
);
}
}

// ie9
const exportFileForm = function (url, query, methods, resolve, reject) {
if (!exportFileForm.container) {
exportFileForm.container = document.createElement("div");
exportFileForm.container.id = "export-form";
document.body.appendChild(exportFileForm.container);
}
ReactDOM.render(
<ExportForm
resolve={resolve}
reject={reject}
url={url}
query={query}
methods={methods}
/>,
exportFileForm.container
);
};

/*
*
* @function 用于导出文件
* @param url 请求路径
* @param query 请求参数
* @param methods 请求方法 默认post
* */
export default function (url, query, methods = "post") {
return new Promise((resolve, reject) => {
exportFileForm(url, query, methods, resolve, reject);
});
}

如果没有太多操作的话直接引入文件直接调用五个参数 url, query, methods, resolve, reject 做不同场景的处理。这样的 IE 也就兼容了,如果不用考虑 IE 的小伙伴,那用 Blob 对象直接导出就好了。