0%

一个Download的实现

fetch download CORS issue

1
2
3
4
5
6
7
8
const fetchDCM = async (url:string) => {
const response = await fetch(url, { method: "GET" })
if (response.ok) {
const buffer = await response.arrayBuffer()
const data = parseLoadData(buffer);
......
}
};

浏览器中点击直接下载文件的链接,放在fetch方法中会有跨域问题

浏览器中点击下载文件的链接时,这被视为用户的直接操作,浏览器允许这类导航请求执行,因为它符合用户的意图且风险较低。

fetch属于AJAX请求范畴,会受到同源策略的严格限制。即使请求的目标是下载一个文件,浏览器也会将其视为脚本试图访问跨域资源,从而可能触发跨域资源共享(CORS)检查。如果服务器没有正确配置CORS响应头,允许你的源域名发起请求,浏览器就会阻止这次请求或者请求成功但无法访问响应体中的数据。

CORS(跨域资源共享)主要是为了保护客户端(即用户的浏览器)的安全和隐私,同时也为服务器端提供了一定程度上的控制权。其工作原理是通过在浏览器层面实施安全策略,确保来自不同源的Web内容不能随意访问或操作其他源的资源,除非得到服务器的明确许可。

具体来说,当一个网页尝试通过JavaScript等客户端脚本从不同的源加载数据时,CORS机制会要求浏览器在实际发送请求之前,先向服务器发起一个预检(preflight)请求,询问服务器是否允许这样的跨域操作。服务器通过在响应头中添加特定的CORS相关字段,如Access-Control-Allow-Origin,来指示浏览器哪些来源的请求是可以接受的。如果服务器不允许该请求,浏览器则会阻止客户端脚本获取响应数据,从而防止了潜在的安全威胁,如跨站脚本攻击(XSS)和数据泄露等。

因此,虽然CORS规则是由服务器设置并返回给客户端的,其主要目的还是在于保护客户端免受恶意第三方网站的侵害,同时给予服务器对资源访问权限的精细控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Manual CORS Configuration
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept');
next();
});

app.get('/download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'public', req.params.filename);
res.setHeader('Content-Disposition', 'attachment;');
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
});

需求:导出列表

一个服务端分页的可检索列表,页面只缓存当前页的数据,导出功能无法完全在前端,点击export将现有检索条件传到后端,由后端查询数据库并生成excel文件,传到前端。

后端实现(Express.js)

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
app.post('/list/export', (req,res)=>{
const data = getDataFromDB(req.body)
generateExcel(results, ()=>{
res.download(path.join(__dirname,'list.xlsx'));
})
}, err => {
res.json(err)
})
})

const columns = [
{ header: 'ID', key: 'id', width: 32 },
{ header: 'Name', key: 'name', width: 32 }];
const generateExcel = function(data, callback){
const excel = require('exceljs');
// create excel workbook
var options = {
filename:path.join(__dirname, 'list.xlsx'),
useStyles: true,
useSharedStrings: true
};
const workbook = new excel.stream.xlsx.WorkbookWriter(options);
workbook.creator = 'QQs';
workbook.created = new Date();
workbook.modified = new Date();
// views
workbook.views = [
{
x: 0, y: 0, width: 10000, height: 20000,
firstSheet: 0, activeTab: 1, visibility: 'visible'
}
]
// add worksheet
const sheet = workbook.addWorksheet('List');
// define columns
sheet.columns = columns;
// add rows
/*
数据量大的情况下考虑到nodejs内存分配瓶颈
*应限制每次select的条数分批addRow并且Row.commit
*/
data.forEach(record => {
const row = record;
// TODO data convertor
sheet.addRow(row).commit();
});
sheet.commit();
workbook.commit().then(callback);
}

生成Excel用到了第三方库exceljs,该库实现了流式写excel的方法,可以在数据量较大的情况下缓解IO压力(待考证)

response.download(filename)方法以Blob方式返回数据

前端实现(Angular8)

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
export() {
const postParams = new Object();
// TODO collect query parameters
this.httpClient
.post('list/export', postParams, {
responseType: 'blob',
headers: new HttpHeaders().append('Content-Type', 'application/json'),
})
.subscribe(res => {
this.downloadFile(res);
});
}
/**
* 创建blob对象,并利用浏览器打开url进行下载
* @param data 文件流数据
*/
downloadFile(data) {
// 下载类型 xls
const contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const blob = new Blob([data], { type: contentType });
const url = window.URL.createObjectURL(blob);
// 打开新窗口方式进行下载
// window.open(url);

// 以动态创建a标签进行下载
const a = document.createElement('a');
a.href = url;
// a.download = fileName;
a.download = 'list.xlsx';
a.click();
window.URL.revokeObjectURL(url);
}

接受请求必须设置response headers,否则默认设置无法取得返回值并进入next回调。

单次下载限制

浏览器下载线程有限制,通常同一时间下载不超过10个文件,超过数量的请求直接被无视

解决方案一是设置时间间隔 避免同时下载
二是zip一下打包下载 jszip