我们可能会有这样一个需求:提供一个下载文件接口,该接口从数据库(可能查询慢并且数据大)拉取数据生成文件,返回文件内容提供下载。

 不考虑优化的情况,我们可能是这样的:

async function downloadAction(req, res) {
    const data = await getData(); // 获取数据
    const fileBinary = await createFile(data); // 使用数据创建文件
    res.writeHead(200, {
        'Content-Type': 'application/vnd.ms-excel',
        'Content-Disposition': `filename=aa.xls`,
    });
    res.body = fileBinary; // 返回文件内容
}

假如getData用了2s,createFile用了1s,下载需要使用2s。其它时间比较少忽略不计。

那么用户请求该接口的时候下载开始前会等待 2s+1s=3s,而下载完则需要 2s+1s+2s=5s 才能下载完,也就是3-5s期间才是用户需要的下载流程。

产生的问题就是下载前的等待比较久(3s),请求到下载完成也比较久(5s)。

在这种情况下,我们就可以用 node.js的stream来优化。

async function downloadAction(req, res) {
    const dataReadableStream = getDateStream(); // 创建数据读取流 
    const fileDuplexStream = createFileStream(); // 创建对应文件格式生成的流
    res.writeHead(200, {
        'Content-Type': 'application/vnd.ms-excel',
        'Content-Disposition': `filename=aa.xls`,
    });
    dataReadableStream.pipe(fileDuplexStream).pipe(res); // 数据流的数据流向文件生成流,文件生成流的数据给到response
}

我们再假设getData 耗时的2s中 1s为生成数据1s为传输数据。

那么,使用流后下载开始的等待时间 为 1s(getData的生成数据),

使用流后的下载完成耗时约 1s(getData的生成耗时)+2s(传输给用户)≈3s

  因为流式操作是并行的,相当于我们边获取数据,边生成文件,边返回给用户,所以时间缩短了(同时进行的操作以最长耗时的操作为准)。

也就是1-3s期间才是用户需要的下载流程。比原流程快。(我们这次不讲流的具体操作,因为不同数据,不同文件格式的实现都不一样,需要具体问题具体分析)

 

我们是否能让下载更快点呢?

我们可以让用户感觉快一点,就是让下载提前开始。

async function downloadAction(req, res) {
    res.writeHead(200, {
        'Content-Type': 'application/force-download',  // 强制下载的请求头,这样浏览器收到该标识,就可以开始下载了
        'Content-Disposition': `filename=aa.xls`,
    });
    res.flushHeaders(); // 设置完返回头后,先把返回头发给用户
    const dataReadableStream = getDateStream(); // 创建数据读取流
    const fileDuplexStream = createFileStream(); // 创建对应文件格式生成的流

    dataReadableStream.pipe(fileDuplexStream).pipe(res); // 数据流的数据流向文件生成流,文件生成流的数据给到response
}

以上,我们把content-type设置为强制开始下载,并且明确提前把请求头发给用户的浏览器,这样用户一收到该头部,就会吊起一个下载流程,达到让用户看到的下载提前开始的效果。

使用该优化后0-3s期间才是用户需要的下载流程,总下载时间没变化,但用户一触发下载按钮,就能给出开始下载的反应,体验比较好。

 

可能有些人会在自己的项目里面这样处理了,也没达到提前开始下载的体验,原因可能是:使用了反向代理。

譬如nginx反向代理,默认情况下都会对返回数据进行缓冲,提高数据传输效率。而这个缓冲也就导致我们提前要返回的返回头被nginx拦截了,延迟发给用户的浏览器。

遇到这种情况只需要在nginx中把你的下载的接口对应的buffer给关掉即可:

location /download/ {
        proxy_buffering off; # 关闭buffer
            proxy_pass http://127.0.0.1:8888/download;
}

如果从用户到接口有多层的反向代理,需要每层都关闭缓冲。

 

PS:这里的优化是不考虑错误提示的。这种提前返回请求头的方式,对错误的提醒不友好,因为返回头已经提前返回了,目前我没有了解到另外抛错的好方法。

 posted on 2020-07-07 17:48  南宫千寻  阅读(273)  评论(0编辑  收藏  举报