在 Razor Pages 中从富文本编辑器上传图片

Razor Pages 示例页面上的 RichTextEditor,文本中插入了一张已上传的城市夜景照片

在 Razor Pages 应用中,围绕编辑器有两种不同的 POST 请求。每张图片在插入的那一刻就会上传到一个返回其 URL 的端点。之后,页面自己的表单再把包含这些 URL 的最终 HTML POST 到 OnPost。本页介绍这两种请求,以及为什么要把它们分开:表单保留自己的防伪造令牌,而 HTML 也能保持足够小,不超出 Razor Pages 的表单限制。

表单

Razor · Pages/Index.cshtml
<form method="post" id="post">
    <input type="hidden" asp-for="Html" />
    <div id="editor"></div>
    <p><button type="submit">Save</button> @Model.Message</p>
</form>
<script>
    var editor = new RichTextEditor("#editor", {
        toolbar: "full",
        galleryEndpoint: "/rte/gallery",
        maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
    });
    var html = document.getElementById("Html");
    editor.setHTMLCode(html.value);
    document.getElementById("post").addEventListener("submit", function () {
        html.value = editor.getHTMLCode();
    });
</script>
  • <input type="hidden" asp-for="Html" /> 承载 HTML。<form method="post"> 标记帮助程序会自动添加隐藏的 __RequestVerificationToken 字段。
  • 编辑器以隐藏字段的值作为初始内容,因此 POST 之后,页面会再次显示已保存的内容。
  • 提交处理函数在表单发送之前,把 editor.getHTMLCode() 复制到该字段中。

页面模型

C# · Pages/Index.cshtml.cs
// Razor Pages validates the antiforgery token on this POST by default; the <form> tag helper
// in Index.cshtml adds the hidden __RequestVerificationToken field. Save Html to your database here.
public void OnPost() => Message = $"Saved {Html?.Length ?? 0} characters of HTML.";

Html 上的 [BindProperty] 会在 POST 时绑定该字段。Razor Pages 在 OnPost 运行之前验证防伪造令牌;不带令牌的 POST 会得到 400。在存储 HTML 或把它展示给其他用户之前,请在服务器上对它进行净化:编辑器发送的内容仍然是来自浏览器的输入。

为什么先上传图片

没有上传处理程序时,编辑器会把粘贴的图片以 base64 data: URL 的形式嵌入 HTML,这是一段很长的文本。Razor Pages 的表单值受 FormOptions.ValueLengthLimit 限制,默认为 4,194,304 个字符。在令牌有效的情况下,100,000 个字符的表单 POST 能够保存,而 4,300,000 个字符的 POST 在 OnPost 运行之前就得到了 400。有了上传路由,HTML 中每张图片只占一个短 URL。在空编辑器中插入一张已上传的图片并点击 Save 后,页面显示 "Saved 108 characters of HTML."。

上传路由

页面上加载的 rte-files.js 会把每张图片发送到 /rte/upload:包括拖放、粘贴、Insert Image 对话框和图片库的 Upload 按钮。这个路由是在 Program.cs 中用 app.MapRteFiles() 映射的 Minimal API 端点:

C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

它不能是页面处理程序,因为处理程序的地址形如 /?handler=Upload,而且每个 POST 处理程序都要求防伪造令牌,编辑器的 XHR 无法发送这个令牌。在 Razor Pages 中为富文本编辑器添加图片库解释了这一选择。这两个限制让请求保持在 4,065,536 字节以内;否则 Kestrel 会接受最多 30,000,000 字节。随后 RteFiles.Save 检查来源、文件夹、大小、扩展名和文件开头的字节,并以新名称保存文件。这些检查与 ASP.NET Core 上传指南中的相同。

Razor Pages 示例的 Insert Gallery 对话框,列出 blog、products 和 team 文件夹以及四张已上传的照片,其中一张处于选中状态
上传四张图片之后,Razor Page 上的图片库。

测试

用 dotnet run 运行本指南的示例项目,然后打开 http://localhost:5303。从图片库插入一张图片(也可以先用 Upload 按钮上传一张),然后点击 Save:消息会显示 HTML 的长度,页面重新加载后图片仍然在。在 shell 中执行:

Shell
curl -F "file=@photo.jpg" http://localhost:5303/rte/upload
{"url":"/uploads/photo-4cdaeb3e.jpg"}

curl -s -o /dev/null -w "%{http_code}\n" -d "Html=x" http://localhost:5303/
400

上传不需要令牌也能成功。而单纯的表单 POST 会被拒绝,因为它没有令牌。

其他平台的同一指南

JavaScript · Next.js · PHP · ASP.NET Core · Blazor · MVC 5 · Web Forms

延伸阅读