需要背景:我的项目中有多处下载数据的中央,有时候遇到几百万条数据,一口气返回的话,可能会导致内存不够用。
需要:是不是有一种办法,能让我循环每次取一点数据返回?
解决方案:目前想到两种——
一种是 node 端应用 stream 形式返回,前端用 window.location.href
的形式关上后端接口。
另一种是后端提供分页接口,前端应用 StreamSaver.js(文件大小无限度)或 FileSaver.js(文件大小受限于前前端可用内存和 Blob 容许的最大值即 2G)保留文件。
两种办法各有劣势,按需选取。
计划 | 长处 | 毛病 |
---|---|---|
服务端 stream | 只发动一次 http 申请 | 如果接口有可能会返回 json 让前端判断是否下载,则前端会很难。如果运维不违心加长网关超时,也是一个毛病 |
前端 stream | 前端能够做更细的判断 | 发动屡次 http 申请 |
本文先介绍第一种,另一种另起一篇文章。
服务端 stream
查阅 koa 的文档,只须要 ctx.body=
左边的值类型是 ReadableStream
即可。那么能够用 stream.Readable
,因为我不习惯 stream.Readable
自身的用法,所以我封装了一个繁难的函数:
/**
* 创立一个可读 stream,循环调用 getData 函数获取数据,当 该函数 返回 null 时完结,如果返回 undefined,会认为是返回空字符串 * @param getData size 参数是用于参考单次返回多少数据,不是说要严格依照这个。size 单位应该是字节。必须返回的是 utf8 编码的 * */
function createReadableStream(getData: (size: number) => Promise<string | null>
) {
return new stream.Readable({async read(size) {while (true) {
let data = null
try {data = await getData(size)
} catch (e) {console.error('[h-node-utils-error createReadable]:', e)
}
const goContinue = this.push(data, 'utf8')
if (!goContinue || data === null) {break}
}
},
})
}
应用办法:
ctx.set('Content-Type', 'text/csv; charset=utf-8')
// 中文必须用 encodeURIComponent 包裹,否则会报 Invalid character in header content ["Content-Disposition"]
ctx.set(
'Content-Disposition',
`attachment; filename=${encodeURIComponent('具体数据')}.csv`
)
let page = 0
ctx.body = createReadableStream(async () => {
page += 1
// 这里从数据库读一页数据,// 如果有数据,把数据转为字符串,如果是 csv 则够用了,如果要用 Excel,须要查查有没有办法能够用
// 如果没有更多数据了,返回 null
})
前端浏览器间接关上该接口地址即可下载