且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2023-02-16 14:02:33

浏览器不允许脚本写入文件系统,无论是用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}";
}

使用动态生成的数据链接

如果这是不可能的,您必须使用数据URL或Blob在JavaScript中创建一个link元素,然后单击它。这很慢,有三个原因:
  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);
    }

BlazorDownloadFileFastJavaScript方法检索数组,将其转换为文件,然后通过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();
}

使用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);
    a.remove();
}

和解码器函数,借用自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);
    a.remove();
}