关于帮老婆在前端导数据这件小事
一个普通的晚上,普通的我听着普通disco回到普通的家,不普通的老婆让我做一件普通的事情:导数据
因为种种原因,只能在前端通过控制台脚本导数据,而且有这几种类型的数据:
1. 查询列表接口,并导出一个 Excel 表格;
2. 查询列表接口,分别将每一行数据导出一个文本文件;
3. 查询列表接口,基于每一行数据的 ID 查询详情接口,再将详情数据导出为文本文件。
为了晚上不用睡地板,我赶紧抄起键盘一顿操作
一、接口请求
由于是通过控制台脚本请求,所以只能用原生 JavaScript 编写,也没办法引入 npm 库
好在不用担心兼容性问题,于是 fetch 成为发起请求的首选方案
首先是 GET 请求,比较简单,简单配一下请求头就行:
function GET(url) {
return fetch(url, {
method: "GET",
headers: new Headers({
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/x-www-form-urlencoded',
}),
credentials: 'include',
})
.then(response => response.json())
.then(json => new Promise((res) => {
res(json.data)
})
)
}
POST 请求会稍微复杂一点,需要根据 Content-Type 调整请求体 body
如果 Content-Type 接受 application/json 就还好,直接通过 JSON.stringify 处理即可
而对于 application/x-www-form-urlencoded 类型,通常是通过 qs.stringify 处理
但由于不能使用 npm 仓库,只好手写一个 stringify 函数
function POST(url, data) { return fetch(url, { method: 'POST', headers: new Headers({ Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/x-www-form-urlencoded', }), credentials: 'include', body: stringify(data), }) .then((response) => response.json()) .then( (json) => new Promise((res) => { res(json.data); }), ); } function stringify(data) { return Object.keys(data) .map((k) => `${k}=${data[k]}`) .join('&'); }
二、文件导出
和服务端不同,前端应用本身不具备读写文件系统的能力,只能通过特定的页面元素调用浏览器 API 实现
比如读取文件只能通过主动触发 <input type="file"> 实现,写入文件只能通过 <a href="" download="" /> 触发下载功能
// 通过 <a/> 下载文件,content 可能是一段富文本
function writeFile(fileName, content) {
const a = document.createElement('a');
const div = document.createElement('div');
div.innerHTML = content;
// 通过 innerText 过滤掉富文本中的 html 标签,得到纯文本
const text = div.innerText || '';
const blob = new Blob([text], { type: 'text/plain' });
a.download = fileName;
a.href = URL.createObjectURL(blob);
a.click();
}
在这次的导出需求中,会同时创建多个下载任务。首次触发时,谷歌浏览器会提示“是否允许下载多个文件”,选择“允许”就好
然而即便允许多文件下载,浏览器最多同时创建 10 个下载任务,这个问题可以通过异步创建下载任务的方式解决
// 就是有点费回车键
三、导出表格
准备就绪,首先处理第一种需求:查询列表接口,并导出一个 Excel 表格
在正常的工作环境下,可以使用 js-xlsx 生成 .xlsx文件。现在一切从简,可以用一个替代方案:制表符 tab
如果我们复制下图这样的表格
然后粘贴到记事本或者任何文本编辑器,会得到这样的结果
反过来,如果我们生成这样的文本,再复制粘贴到 Excel 中,也能得到对应的表格
于是就有了这样的代码:
function exportTable() {
const HEADER = ['序号', '名称', '描述', '备注'];
fetchList().then(({ list }) => {
const temp = [renderRow(HEADER)];
list.forEach((x) => {
const row = renderRow([x.key1, x.key2, x.key3, x.key4]);
temp.push(row);
});
writeFile('表格', temp.join('\n'));
});
}
function fetchList() {
return get('/api/list');
}
function renderTxt(v) {
return v || '-';
}
function renderRow(arr) {
return arr.map((x) => renderTxt(x)).join(' ');
}
四、导出详情
除了导出表格以外,还有两种情况:
1. 查询列表接口,分别将每一行数据导出一个文本文件;
2. 查询列表接口,基于每一行数据的 ID 查询详情接口,再将详情数据导出为文本文件。
这两个情况的处理思路很类似,只是第二种情况需要再请求一次接口
但有个细节需要注意:列表的数据很多,会同时创建多个下载任务,需要通过延时的方式绕开浏览器的下载任务上限
function exportDetail() {
fetchList().then(({ list }) => {
list.forEach((item, index) => {
fetchDetail(item.id).then((detail) => {
setTimeout(() => {
// 通过 setTimeout 异步创建下载任务,绕过浏览器的下载上限
writeFile(item.title, renderContent(detail))
}, index * 500);
});
});
});
}
function fetchList() {
return get('/api/list');
}
function fetchDetail(id) {
return get(`/api/detail/${id}`);
}
// 排版
function renderContent(detail) {
const { title, desc, content } = detail || {};
return `<h1>${title}\n</h1><div>${desc}\n</div>${content}`;
}
终于避免睡地板的惨剧,收工~