使用JavaScript打开“另存为”/“下载”对话框以下载即时创建的文件

4
我有一个NGINX+Flask+KnockoutJS单页应用程序,我想创建一个下载按钮,允许用户下载他/她正在客户端可视化和操作的数据而不重新加载完整页面。有许多纯JavaScript解决方案(例如download.js),但它们中没有一个与所有主要浏览器(例如Safari)完全兼容。
基本上,我想要的是:
  1. 应用程序向用户显示表格
  2. 用户按下下载按钮
  3. JavaScript将数据发送到服务器端点
  4. 服务器根据从客户端发送的数据即时生成文件
  5. 浏览器打开下载/另存为对话框
这是否可能?

1
download.js的作者在此:如果有人知道Safari的解决方法,我将非常感激。话虽如此,如果您使用服务器生成内容,则可以简单地使用content-disposition头在隐藏的iframe中触发下载。 - dandavis
1
顶部有一个很好的代码片段,可以向您展示如何下载表单响应或更改iframe位置:http://php.net/manual/en/function.header.php#refsect1-function.header-examples - dandavis
4个回答

4
当然可以。让我们来看一个例子:
应用程序向用户显示一个表格
你的意思是使用 <table> 标签这样的东西吗?
<table>
    <tr>
        <td>Foo bar</td>
        <td>123</td>
        <td>
            <form action="/download/foo/bar/123" method="post">
                <button type="submit" value="Download foo bar 123" />
            </form>
        </td>
    </tr>

    ... here come some other rows of the table ...
</table>
  1. 用户点击下载按钮

好的,这个很明显。用户通过点击表格中所需行对应的提交按钮来提交表单。

  1. JavaScript 将数据发送到服务器端点

既然有标准的 HTML 表单可以以纯浏览器无关的方式将数据提交到服务器(如第 1 点所示),为什么要关心 JavaScript 呢?如果你真的关心 JavaScript,你可以订阅我之前展示的 <form> 元素的onsubmit操作,并使用 JavaScript 将必要的数据注入到 DOM 中作为隐藏字段。

  1. 服务器根据从客户端发送的数据即时生成文件

是的,这就像标准的 HTTP 协议一样。服务器将简单处理 /download/foo/bar/123 端点并将文件作为附件发送:

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 29
Content-Disposition: attachment; filename=foobar123.bin

HERE COMES THE BINARY CONTENT
  1. 浏览器打开下载/另存为对话框

当浏览器处理之前从服务器返回的HTTP响应时,这就是任何浏览器将要做的事情。

结论:HTTP协议和标准HTML表单已经为您提供了实现您要求所需的必要工具。如果您想要一些额外的花哨效果,只需在提交HTML表单时使用JavaScript增强它,以便附加任何所需字段作为隐藏输入元素发送到服务器。然后让浏览器处理下载。


谢谢,我确实关心JavaScript,因为我想在不刷新页面的情况下下载文件(我编辑了我的问题以澄清)。我尝试向我的服务器应用程序端点发送POST HTTP请求,但即使HTTP响应成功,文件也无法下载。 - raben
1
如果用户将HTML表单POST到流文件的服务器端点,并使用Content-Disposition: attachment头,就不会刷新页面,如我的答案所示。 用户只需选择文件目标并显示“另存为”对话框,不会发生任何页面重新加载。 - Darin Dimitrov
哦,我不知道!我会试一下。 - raben

0
在实践中,我意识到我的问题可能没有百分之百的清晰。无论如何,我想分享我想出来的解决方案。我在Flask应用程序中创建了两个端点:
第一个通过AJAX POST从客户端获取数据,并将它们暂时存储在Redis中(我已经有一个用于缓存的Redis实例),并为文件生成UUID。
@mod.route("/create-csv", methods=['POST'])
def create_csv():
    csv_string = request.form.get('csv')
    file_id = str(uuid())
    rstore.setex(file_id, 60, csv_string)
    return jsonify({}), 202, {'Location': url_for('api.download',
                                              file_id=file_id,
                                              _external=True, 
                                              _scheme='https')}

第二个端点只是使用适当的标头将文件发送到客户端。
@mod.route("/download/<file_id>", methods=['GET'])
def download(file_id):
    file_content = rstore.get(file_id)
    response = make_response(file_content)
    response.headers["Content-Disposition"] = "attachment; filename=keywords.csv"
    response.headers['Content-Type'] = "application/octet-stream"
    return response

在客户端网站上,我有以下JavaScript代码:
  self.save = function(csvdata) {
    $.post( "/api/create-csv", csvdata, function(data, status, response){

      var file_url = response.getResponseHeader('Location');
      window.location.assign(file_url);

    });
  }

当POST请求成功发送时,我只需将当前URL分配给文件下载的URL。

0

只有在https中,才能通过(window as any).showSaveFilePicker(pickerOptions)打开“另存为”对话框:

async saveAs(data: string) {
    const filename = 'conf1.conf';
    if ('showSaveFilePicker' in window) {
        const pickerOptions = {
            suggestedName: filename,
            types: [
                {
                    description: 'Text File',
                    accept: {
                        'text/plain': ['.conf'],
                    },
                },
            ],
        };

        const fileHandle = await (window as any).showSaveFilePicker(pickerOptions);
        if (fileHandle) {
            const writableFileStream = await fileHandle.createWritable();
            if (writableFileStream) {
                var taBlob = new Blob([data], { type: 'text/plain' });
                await writableFileStream.write(taBlob);
                await writableFileStream.close();
                // this.messageService.add({ severity: 'info', summary: 'Saved ok', detail: '' });
            }
        }
    } // else { // no https
} 

0
浏览器将根据其自己的下载设置显示“另存为”对话框。文件是否被下载取决于操作系统是否有处理打开文件的程序。您可以通过创建带有下载属性的锚点来强制下载(覆盖打开文件)。

Safari不支持a[download],根据OP的说法... http://caniuse.com/#feat=download - dandavis
并不会改变我的答案,即浏览器决定如何处理下载的文件,而不是任何JS代码。 - Scott Marcus
@ScottMarcus 你说得没错,但重点不在于对话框本身。我只是用它来解释我想要下载文件而不是在浏览器中打开它的事实。 - raben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接