需求背景: 项目中有多处下载数据的地方,有时候遇到几百万条数据,一口气返回的话,可能会导致内存不够用。
需求:是不是有一种方法,能让我循环每次取一点数据返回?
解决方案:目前想到两种——
一种是node端使用 stream 方式返回,前端用window.location.href
的方式打开后端接口。
另一种是后端提供分页接口,前端使用 StreamSaver.js(文件大小无限制) 或 FileSaver.js(文件大小受限于前前端可用内存和Blob允许的最大值即2G) 保存文件。
两种方法各有优势,按需选取。
方案 | 优点 | 缺点 |
---|---|---|
服务端stream | 只发起一次http请求 | 如果接口有可能会返回json让前端判断是否下载,则前端会很难。假如运维不愿意加长网关超时,也是一个缺点 |
前端stream | 前端可以做更细的判断 | 发起多次http请求 |
本文先介绍第一种,另一种另起一篇文章。
查阅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 })
前端浏览器直接打开该接口地址即可下载