在 Blazor Server 中读取富文本编辑器的 HTML

示意图:Save 按钮通过 JS 互操作请求 IJSStreamReference,浏览器把 getHTMLCode() 编码为字节,.NET 用 OpenReadStreamAsync 读取;超过 32 KB 的普通字符串会关闭线路

Blazor Server 页面迟早需要把编辑器的 HTML 取到 C# 中保存。最直观的调用 JS.InvokeAsync<string> 在测试时没问题,但遇到第一篇长文档就会失败,因为返回结果要经过线路(circuit)的 SignalR 连接传输,而该连接有 32 KB 的消息大小限制。本页介绍我们碰到这个限制时发生了什么,以及示例如何改为以流的形式读取 HTML。

32 KB 限制

在 Blazor Server 中,从浏览器发往服务器的每条消息都经过线路,JS 互操作调用的返回结果也不例外。线路的 SignalR MaximumReceiveMessageSize 默认为 32 KB。我们通过 InvokeAsync<string> 返回 editor.getHTMLCode(),测得结果如下:

  • 20,000 个字符的 HTML:成功到达。
  • 40,000 个字符:浏览器记录了 "Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'"。页面上的消息没有变化,而线路已经断开。

在默认日志级别下,服务器端不会记录这次失败,因此在测试中很容易被忽略。

从 JavaScript 返回字节

当 JS 函数返回 Uint8Array 时,Blazor 可以把它作为 IJSStreamReference 交给 .NET,它会分块发送,不受消息大小限制。示例的互操作函数位于 App.razor 中:

Razor · Components/App.razor
<script>
    // Called from Home.razor. The editor options stay in JavaScript, where they can also hold
    // functions (custom buttons, event handlers) that cannot be sent from .NET as JSON.
    window.rteBlazor = {
        create: function (host) {
            host.rte = new RichTextEditor(host, {
                toolbar: "full",
                galleryEndpoint: "/rte/gallery",
                maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
            });
        },
        // Returned as bytes: .NET reads them as a stream, so documents over Blazor's 32 KB
        // SignalR message limit still arrive.
        getHtml: function (host) {
            return new TextEncoder().encode(host.rte.getHTMLCode());
        },
        destroy: function (host) {
            if (!host || !host.rte) return;
            // editor.destroy() exists only in newer editor builds
            if (typeof host.rte.destroy === "function") host.rte.destroy();
            host.rte = null;
            host.innerHTML = "";
        }
    };
</script>

getHtml 用 TextEncoder 把 HTML 编码为 UTF-8。编辑器本身由 create 创建;在 Blazor 中为富文本编辑器添加图片库介绍了 create 和 destroy。

在 C# 中读取流

Razor · Components/Pages/Home.razor
async Task Save()
{
    var bytes = await JS.InvokeAsync<IJSStreamReference>("rteBlazor.getHtml", host);
    await using var stream = await bytes.OpenReadStreamAsync(maxAllowedSize: 10_000_000);
    using var reader = new StreamReader(stream);
    var html = await reader.ReadToEndAsync();
    message = $"Saved {html.Length} characters of HTML."; // save html to your database here
}
  • InvokeAsync<IJSStreamReference> 以流而不是值的形式请求字节。
  • OpenReadStreamAsync(maxAllowedSize: 10_000_000) 设置服务器最多读取的大小。它的默认值是 512,000 字节,请把它设置为数据库列能够容纳的大小。
  • StreamReader 把 UTF-8 字节还原为字符串。在这里保存它;在展示给其他用户之前,请先净化。

使用这段代码后,一篇包含 100,000 个 x 字符和一张插入图片的文档完整到达:"Saved 100116 characters of HTML."

为什么不提高限制

另一种办法是在中心(hub)选项中调大 MaximumReceiveMessageSize,例如通过 AddInteractiveServerComponents().AddHubOptions(...)。但这会提高每条线路上每条消息的限制。本页没有使用也没有测试这种做法。流式传输保留了默认限制,只让 HTML 在您选择的大小以内通过。

图片则是另一回事:它们根本不经过组件。在 Blazor 中从富文本编辑器上传图片解释了编辑器为什么把图片发送到 HTTP 端点,这样保存的 HTML 中只包含图片的 URL。

测试

用 dotnet run 运行本指南的示例项目,然后打开 http://localhost:5304。把一篇长文档粘贴到编辑器中,或者输入几段文字后反复粘贴多次,然后点击 Save。编辑器下方的消息会显示到达 C# 的字符数,远超 32 KB。要观察这个限制,可以把 Save 改为通过 InvokeAsync<string> 从一个以字符串形式返回 getHTMLCode() 的 JS 函数读取 HTML,再保存同一篇文档:线路会关闭。

延伸阅读