简体   繁体   English

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

[英]Download File from Server with Blazor App

I have created an HttpGet in my Server-API which creates a CSV-File and returns it with FileStreamResult :我在我的 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",
    };
}

In my Blazor-Client App, I have created a Button with a handler:在我的 Blazor-Client 应用程序中,我创建了一个带有处理程序的 Button:

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

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

The Get in the Controller is called, but I don't know what to do so that the file is downloaded in the browser after the api call.调用了Controller中的Get,但是我不知道该怎么做才能在api调用后将文件下载到浏览器中。

Browsers don't allow scripts to write to the file system, whether written in JavaScript or WebAssembly.浏览器不允许脚本写入文件系统,无论是用 JavaScript 还是 WebAssembly 编写的。 The download dialog is displayed by the browser only when the user clicks on a link.仅当用户单击链接时,浏览器才会显示下载对话框。

Using a link button使用链接按钮

If the final file is returned directly from the server, the easiest solution is to use a link button with a URL to the API endpoint, possibly calculated at runtime.如果最终文件直接从服务器返回,最简单的解决方案是使用带有指向 API 端点的 URL 的链接按钮,可能在运行时计算。 You can use the download attribute to specify a file name.您可以使用download属性来指定文件名。 When the user clicks on the link, the file will be retrieved and saved using the download name当用户点击链接时,文件将被检索并使用download名称保存

For example :例如 :

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

or或者

@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}";
}

Using a dynamically generated data link使用动态生成的数据链接

If that's not possible, you have to create a link element in JavaScript with a data URL, or a Blob, and click it.如果这不可能,您必须在 JavaScript 中创建一个带有数据 URL 或 Blob 的链接元素,然后单击它。 That's SLOOOOW for three reasons :这是 SLOOOOW 的三个原因:

  1. You're making an in-memory copy of the downloaded file that's at least 33% larger than the original.您正在制作比原始文件大至少 33% 的下载文件的内存副本。
  2. JS interop data marshalling is slow , which means that passing the bytes from Blazor to Javascript is also slow. JS 互操作数据编组很,这意味着将字节从 Blazor 传递到 Javascript 也很慢。
  3. Byte arrays are passed as Base64 strings.字节数组作为 Base64 字符串传递。 These need to be decoded back into a byte array to be used as blobs.这些需要被解码回一个字节数组以用作 blob。

The article Generating and efficiently exporting a file in a Blazor WebAssembly application shows how to pass the bytes without marshaling using some Blazor runtime tricks. 在 Blazor WebAssembly 应用程序生成并有效导出文件一文展示了如何使用一些 Blazor 运行时技巧在封送的情况下传递字节。

If you use Blazor WASM, you can use use InvokeUnmarshalled to pass a byte[] array and have it appear as a Uint8Array in JavaScript.如果您使用 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);
    }

The BlazorDownloadFileFast JavaScript method retrieves the array, converts it to a File and then, through URL.createObjectURL to a safe data URL that can be clicked : 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);
}

With Blazor Server, marshaling is unavoidable.使用 Blazor Server,封送处理是不可避免的。 In this case the slower BlazorDownloadFile method is called.在这种情况下,将调用较慢的BlazorDownloadFile方法。 The byte[] array is marshaled as a BASE64 string which has to be decoded. byte[]数组被编组为必须被解码的 BASE64 字符串。 Unfortunately, JavaScript's atob and btoa functions can't handle every value so we need another method to decode Base64 into Uint8Array:不幸的是,JavaScript 的atobbtoa函数无法处理每个值,因此我们需要另一种方法将 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);
}

And the decoder function, borrowed from Mozilla's Base64 documentation和解码器功能,借用 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;
}

Blazor 6开拓者 6

The ASP.NET Core 6 Preview 6 that was released recently no longer marshals byte[] as a Base64 string.最近发布的ASP.NET Core 6 Preview 6不再将byte[]编组为 Base64 字符串。 It should be possible to use the following function应该可以使用以下功能

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);
}

When you do HttpClient.GetAsync the Blazor runtime gets the file.当您执行HttpClient.GetAsync ,Blazor 运行时会获取该文件。 But it cannot directly save the file to the disk as a Browser environment (in which Blazor runs) does not have access to the disk.但它不能直接将文件保存到磁盘,因为浏览器环境(Blazor 在其中运行)无法访问磁盘。

So you will have to use some Javascript Interop to trigger the file download feature of the browser.因此,您将不得不使用一些 Javascript Interop 来触发浏览器的文件下载功能。 You can generate a link data:text/plain;charset=utf-8,<<content of the file>> and invoke click on it.您可以生成一个链接data:text/plain;charset=utf-8,<<content of the file>>并调用单击它。

In order to download file you have to use Microsoft JSInterop.为了下载文件,您必须使用 Microsoft JSInterop。 There are many ways to implement your request.有很多方法可以实现您的请求。 One way that i use, is to get the file as byte array then convert it to base64string.我使用的一种方法是将文件作为字节数组获取,然后将其转换为 base64string。 Finally call the function that you created in javascript.最后调用您在 javascript 中创建的函数。

In server side在服务器端

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

And in javascript file in wwwroot you create a function在 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);}

use the NavigationManager使用导航管理器

@inject NavigationManager NavigationManager

private async Task DownloadCatalog() {
    NavigationManager.NavigateTo("api/csvProduct",true);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM