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

Ben*_*ual 25 c# asp.net-core blazor

HttpGet在我的 Server-API 中创建了一个 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",
    };
}
Run Code Online (Sandbox Code Playgroud)

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

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

    // ... how do I download the file in the browser?
}
Run Code Online (Sandbox Code Playgroud)

调用了控制器中的 Get,但我不知道该怎么做才能在 api 调用后将文件下载到浏览器中。

Pan*_*vos 45

浏览器不允许脚本写入文件系统,无论是用 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>
Run Code Online (Sandbox Code Playgroud)

或者

@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}";
}
Run Code Online (Sandbox Code Playgroud)

使用动态生成的数据链接

如果这不可能,您必须在 JavaScript 中使用数据 URL 或 Blob 创建一个链接元素,然后单击它。速度太慢有以下三个原因:

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

在 Blazor WebAssembly 应用程序中生成并高效导出文件一文展示了如何使用某些 Blazor 运行时技巧传递字节而不进行封送。

如果您使用 Blazor WASM,则可以使用 useInvokeUnmarshalled传递byte[]数组并使其Uint8Array在 JavaScript 中显示为 a。

    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);
    }
Run Code Online (Sandbox Code Playgroud)

JavaScriptBlazorDownloadFileFast方法检索数组,将其转换为文件,然后转换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);
    a.remove();
}
Run Code Online (Sandbox Code Playgroud)

对于 Blazor Server,封送是不可避免的。BlazorDownloadFile在这种情况下,将调用较慢的方法。该byte[]数组被编组为 BASE64 字符串,必须对其进行解码。不幸的是,JavaScriptatobbtoa函数无法处理每个值,因此我们需要另一种方法将 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);
    a.remove();
}
Run Code Online (Sandbox Code Playgroud)

以及解码器函数,借用自Mozilla的Base64文档

// 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;
}
Run Code Online (Sandbox Code Playgroud)

开拓者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);
    a.remove();
}
Run Code Online (Sandbox Code Playgroud)

  • .NET 5 中的完整编码示例:https://github.com/tysongibby/BlazorExcelFileDownload (2认同)

小智 12

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

在服务器端

js.InvokeVoidAsync("jsSaveAsFile",
                        filename,
                        Convert.ToBase64String(GetFileByteArrayFunction())
                        );
Run Code Online (Sandbox Code Playgroud)

在 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);}
Run Code Online (Sandbox Code Playgroud)