使用 Blazor 应用程序从服务器下载文件

时间:2021-07-15 11:47:49

标签: c# asp.net-core blazor

我在我的 Server-API 中创建了一个 HttpGet,它创建了一个 CSV 文件并用 FileStreamResult 返回它:

[HttpGet]
public IActionResult Get() {
    // do logic to create csv in memoryStream

    return new FileStreamResult(memoryStream, "text/csv;charset=utf-8") {
        FileDownloadName = "products.csv",
    };
}

在我的 Blazor 客户端应用程序中,我创建了一个带有处理程序的按钮:

private async Task DownloadCatalog() {
    var file = HttpClient.GetAsync("api/csvProduct");

    // ... how do I download the file in the browser?
}

调用了Controller中的Get,但是不知道怎么做才让api调用后在浏览器中下载文件。

3 个答案:

答案 0 :(得分:1)

浏览器不允许脚本写入文件系统,无论是用 JavaScript 还是 WebAssembly 编写的。仅当用户点击链接时,浏览器才会显示下载对话框。

使用链接按钮

如果最终文件直接从服务器返回,最简单的解决方案是使用带有指向 API 端点的 URL 的链接按钮,可能在运行时计算。您可以使用 download 属性来指定文件名。当用户点击链接时,文件将被检索并使用 download 名称

保存

例如:

<a id="exportCsv" class="btn" href="api/csvProduct" download="MyFile.csv" 
   role="button" target="=_top">Export to CSV</a>

@if (_exportUrl != null)
{
    <a id="exportCsv" class="btn" href="@_exportUrl" download="MyFile.csv" 
       role="button" target="=_top">Export to Csv</a>
}

...
int _productId=0;
string? _exportUrl=null;

async Task Search()
{
   //Get and display a product summary
   _model=await GetProductSummary(_productId);
   //Activate the download URL 
   _exportUrl = $"api/csvProduct/{_productId}";
}

使用动态生成的数据链接

如果这不可能,您必须在 JavaScript 中创建一个带有数据 URL 或 Blob 的链接元素,然后单击它。这是 SLOOOOW 的三个原因:

  1. 您正在为下载的文件制作至少比原始文件大 33% 的内存副本。
  2. JS 互操作数据编组,这意味着将字节从 Blazor 传递到 Javascript 也很慢。
  3. 字节数组作为 Base64 字符串传递。这些需要被解码回一个字节数组以用作 blob。

文章 Generating and efficiently exporting a file in a Blazor WebAssembly application 展示了如何使用一些 Blazor 运行时技巧在不封送处理的情况下传递字节。

如果您使用 Blazor WASM,您可以使用 InvokeUnmarshalled 传递一个 byte[] 数组并让它在 JavaScript 中显示为 Uint8Array

    byte[] file = Enumerable.Range(0, 100).Cast<byte>().ToArray();
    string fileName = "file.bin";
    string contentType = "application/octet-stream";

    // Check if the IJSRuntime is the WebAssembly implementation of the JSRuntime
    if (JSRuntime is IJSUnmarshalledRuntime webAssemblyJSRuntime)
    {
        webAssemblyJSRuntime.InvokeUnmarshalled<string, string, byte[], bool>("BlazorDownloadFileFast", fileName, contentType, file);
    }
    else
    {
        // Fall back to the slow method if not in WebAssembly
        await JSRuntime.InvokeVoidAsync("BlazorDownloadFile", fileName, contentType, file);
    }

BlazorDownloadFileFast JavaScript 方法检索数组,将其转换为文件,然后通过 URL.createObjectURL 转换为可单击的安全数据 URL :

function BlazorDownloadFileFast(name, contentType, content) {
    // Convert the parameters to actual JS types
    const nameStr = BINDING.conv_string(name);
    const contentTypeStr = BINDING.conv_string(contentType);
    const contentArray = Blazor.platform.toUint8Array(content);

    // Create the URL
    const file = new File([contentArray], nameStr, { type: contentTypeStr });
    const exportUrl = URL.createObjectURL(file);

    // Create the <a> element and click on it
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.href = exportUrl;
    a.download = nameStr;
    a.target = "_self";
    a.click();

    // We don't need to keep the url, let's release the memory
    // On Safari it seems you need to comment this line... (please let me know if you know why)
    URL.revokeObjectURL(exportUrl);
}

对于 Blazor Server,封送处理是不可避免的。在这种情况下,调用较慢的 BlazorDownloadFile 方法。 byte[] 数组被编组为必须被解码的 BASE64 字符串。不幸的是,JavaScript 的 atobbtoa 函数 can't handle every value 所以我们需要另一种方法将 Base64 解码为 Uint8Array:

function BlazorDownloadFile(filename, contentType, content) {
    // Blazor marshall byte[] to a base64 string, so we first need to convert the string (content) to a Uint8Array to create the File
    const data = base64DecToArr(content);

    // Create the URL
    const file = new File([data], filename, { type: contentType });
    const exportUrl = URL.createObjectURL(file);

    // Create the <a> element and click on it
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.href = exportUrl;
    a.download = filename;
    a.target = "_self";
    a.click();

    // We don't need to keep the url, let's release the memory
    // On Safari it seems you need to comment this line... (please let me know if you know why)
    URL.revokeObjectURL(exportUrl);
}

以及从 Mozilla 的 Base64 documentation 借用的解码器功能

// Convert a base64 string to a Uint8Array. This is needed to create a blob object from the base64 string.
// The code comes from: https://developer.mozilla.org/fr/docs/Web/API/WindowBase64/D%C3%A9coder_encoder_en_base64
function b64ToUint6(nChr) {
  return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0;
}

function base64DecToArr(sBase64, nBlocksSize) {
  var
    sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""),
    nInLen = sB64Enc.length,
    nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2,
    taBytes = new Uint8Array(nOutLen);

  for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
    nMod4 = nInIdx & 3;
    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
    if (nMod4 === 3 || nInLen - nInIdx === 1) {
      for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
        taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
      }
      nUint24 = 0;
    }
  }
  return taBytes;
}

Blazor 6

最近发布的 ASP.NET Core 6 Preview 6 不再将 byte[] 编组为 Base64 字符串。应该可以使用下面的函数

function BlazorDownloadFile(filename, contentType, data) {

    // Create the URL
    const file = new File([data], filename, { type: contentType });
    const exportUrl = URL.createObjectURL(file);

    // Create the <a> element and click on it
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.href = exportUrl;
    a.download = filename;
    a.target = "_self";
    a.click();

    // We don't need to keep the url, let's release the memory
    // On Safari it seems you need to comment this line... (please let me know if you know why)
    URL.revokeObjectURL(exportUrl);
}

答案 1 :(得分:0)

当您执行 HttpClient.GetAsync 时,Blazor 运行时会获取该文件。但它不能直接将文件保存到磁盘,因为浏览器环境(Blazor 在其中运行)无法访问磁盘。

因此您将不得不使用一些 Javascript Interop 来触发浏览器的文件下载功能。您可以生成一个链接 data:text/plain;charset=utf-8,<<content of the file>> 并调用它。

答案 2 :(得分:0)

为了下载文件,您必须使用 Microsoft JSInterop。有很多方法可以实现您的请求。我使用的一种方法是将文件作为字节数组获取,然后将其转换为 base64string。最后调用您在 javascript 中创建的函数。

在服务器端

js.InvokeVoidAsync("jsSaveAsFile",
                        filename,
                        Convert.ToBase64String(GetFileByteArrayFunction())
                        );

然后在 wwwroot 的 javascript 文件中创建一个函数

function jsSaveAsFile(filename, byteBase64) {
var link = document.createElement('a');
link.download = filename;
link.href = "data:application/octet-stream;base64," + byteBase64;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);}
相关问题