使用 fetch() 从经过身份验证的 REST 下载和保存数据

Downloading and saving data with fetch() from authenticated REST

我有一个 React 应用程序,它使用 Python 和 Flask 中内置的 REST 后端。我正在从数据库下载数据并通过浏览器将其保存为 CSV 文件。 我有这个工作。然而,我不明白的是为什么我必须超越我一直在阅读的资源并将东西混合在一起才能让它工作。为什么我没有找到更好的概述?

有人说我所要做的就是用 mimetype 和 Content-Disposition: attachment; filename=something.csv:

设置响应 header

然而,仅此一项,仅适用于普通链接,而不适用于 fetch() 和身份验证 ,因此我不得不寻找拯救客户端的方法数据到磁盘,例如:

所以,我的问题是:

第一个问题的答案似乎是我无法修改请求 headers(以添加身份验证令牌),除非通过 XHR 类型的工作。这似乎在这里得到了回答(non-answered,真的):

而且,出于某种原因,使用 Content-Disposition: attachment 对 XHR 的响应毫无意义。真的吗?在现代浏览器中没有更内在的方式来管理这样的请求吗?

我觉得我对这个理解不够,这让我很烦恼。

无论如何,这里是 有效的 代码,如果可能的话,我希望对其进行简化:

JS(反应):

// 
download(filename, text) {
    var pom = document.createElement('a');
    pom.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(text));
    pom.setAttribute('download', filename);

    if (document.createEvent) {
        var event = document.createEvent('MouseEvents');
        event.initEvent('click', true, true);
        pom.dispatchEvent(event);
    }
    else {
        pom.click();
    }
}
downloadReport(studyID) {
    fetch(`/api/admin/studies/${studyID}/csv`
    ,   {
            headers: {
                "Authorization": "Bearer " + this.props.authAPI.getToken()
            ,   "Accept": "text/csv"
            }
        }
    )
    .then(this.checkStatus.bind(this))
    .then((response) => response.text())
    .then((responseText) => this.download(`study${studyID}.csv`, responseText))
    .catch((error) => {
        console.error(this.props.location.pathname, error)
    })
}

Python(烧瓶):

@app.route("/api/admin/studies/<int:study_id>/csv", methods=["GET"])
@admin.login_required
def admin_get_csv(study_id):

    test = [("1","2","3"),("4","5","6")]
    def generate():
        for row in test:
            yield ",".join(row) + "\n"

    return Response(
                generate()
            ,   mimetype="text/csv"
            ,   headers={"Content-Disposition": "attachment; filename=study{0}.csv".format(study_id)}
            )

使用 DOM 告诉浏览器如何处理您的下载。用 Node.appendChild 注入锚标签,让用户点击 link.

<a href="/api/admin/studies/1/csv" download="study1.csv">Download CSV</a>

您正在做的是下载文件,将 已经完成的 请求插入锚标记,然后在您的代码中使用 pom.click() 创建第二个请求。

编辑:我错过了授权 header。如果您喜欢这个建议,您可以将令牌放在查询字符串中。

参考 this answer, you can use FileSaver or download.js 个库。

示例:

var saveAs = require('file-saver');

fetch('/download/urf/file', {
  headers: {
    'Content-Type': 'text/csv'
  },
  responseType: 'blob'
}).then(response => response.blob())
  .then(blob => saveAs(blob, 'test.csv'));

我被迫回到这里,因为我们 运行 进入 Chrome 的 2MB 限制。谁知道? (这个人,举个例子:https://craignicol.wordpress.com/2016/07/19/excellent-export-and-the-chrome-url-limit/ 在我的问题发布几个月后,我在下面实施了他的解决方案。)

无论如何,我现在将尝试 回答我自己的问题,因为我还没有看到关于请求的身份验证要求的可接受答案。 (尽管如此,Conrado 确实为该要求提供了一些有用的链接。)

关于以下问题:为什么我必须这样做,没有更内在的方法吗?答案似乎分别是“仅仅因为”和“不”。不是很好的答案,我知道。我希望我能解释得更多……但如果不把脚伸到嘴里,我真的做不到。我不是网络专家,当然也不是解释它的合适人选。这就是我读到的内容。您只是不能在验证时将流伪造为文件下载。

至于:我缺少什么以及更简单的方法是什么?嗯,没有更简单的方法。当然,我可以在代码库中添加更多像 FileSaver.js 这样的库,让它们为我隐藏一些代码。但是,我不喜欢比我真正需要的更大的工具集(我正在看着你,你这些可笑的 50MB 网站)。我可以通过在别处隐藏我的 download() 函数并导入它来完成与那些库相同的事情。不,从我的角度来看,这并不容易。获取预建功能的工作可能更少,但就执行下载所需的代码量而言并不容易。对不起。

但是...我 遗漏了一些东西:导致 Chrome 中 2MB 限制的那个东西。当时,我并不真正理解我使用的这个 URI 数据黑客是如何工作的。我找到了一些有效的代码,并使用了它。我现在明白了——现在我有更多时间更深入地阅读问题的那一部分。简而言之,我错过了 blob 选项与 URI 选项。当然,blob 在各种浏览器中都有其自身的局限性,但是考虑到我的用例在 2016 年不会受到任何浏览器的影响,blob 选项从一开始就是一个更好的选择......而且感觉不那么骇人听闻(也许仅此一点就“更容易”了)。

这是我当前的解决方案,它在回退到 URI 数据 hack 之前尝试 blob 保存:

JS(反应):

saveStreamCSV(filename, text) {
    this.setState({downloadingCSV: false})
    if(window.navigator.msSaveBlob) {
        // IE 10 and later, and Edge.
        var blobObject = new Blob([text], {type: 'text/csv'});
        window.navigator.msSaveBlob(blobObject, filename);
    } else {
        // Everthing else (except old IE).
        // Create a dummy anchor (with a download attribute) to click.
        var anchor = document.createElement('a');
        anchor.download = filename;
        if(window.URL.createObjectURL) {
            // Everything else new.
            var blobObject = new Blob([text], {type: 'text/csv'});
            anchor.href = window.URL.createObjectURL(blobObject);
        } else {
            // Fallback for older browsers (limited to 2MB on post-2010 Chrome).
            // Load up the data into the URI for "download."
            anchor.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(text);
        }
        // Now, click it.
        if (document.createEvent) {
            var event = document.createEvent('MouseEvents');
            event.initEvent('click', true, true);
            anchor.dispatchEvent(event);
        }
        else {
            anchor.click();
        }
    }
}
handleDownloadClick(e) {
    this.setState({downloadingCSV: true})
    fetch(`/api/admin/studies/${this.props.study.id}/csv`
    ,   {
            headers: {
                "Authorization": "Bearer " + this.props.authAPI.getToken()
            ,   "Accept": "text/csv"
            }
        }
    )
    .then((response) => response.text())
    .then((responseText) => this.saveStreamCSV(`study${this.props.study.id}.csv`, responseText))
    .catch((error) => {
        this.setState({downloadingCSV: false})
        console.error("CSV handleDownloadClick:", error)
    })
}

注意:我走这条路只是因为我不需要担心 FileSaver.js 构建的所有用例(这仅适用于管理应用程序的功能,而不是 public-面向)。