2019 年 2 月

第 34 卷,第 2 期

[领先技术]

在 Blazor 中处理窗体

作者 Dino Esposito | 2019 年 2 月

Dino EspositoASP.NET Core Blazor 是基于 C# 的客户端框架,用于生成单页应用程序 (SPA)。就这一点而言,它与 Angular 并无太大分别,都可以与公开可访问 HTTP 的终结点的任意后端交互。不过,Blazor 暂时正在开发,还远不能上线。

也就是说,Blazor 的一些部件是作为 ASP.NET Core 3.0 的一部分交付。Razor 组件是 Blazor 应用程序的一种特殊配置,它完全在服务器上与可能构成其后端的经典 ASP.NET Core 应用程序并行运行。主要用于执行后端任务的经典 Web 应用程序与 ASP.NET 服务器上基于 Blazor 的独特表示层的共存,不仅简化了一些编程方案,还在纯 SPA 方法和纯服务器方法之间找到了中间点。本文将介绍如何处理输入窗体和客户端到服务器通信。首先,快速了解一下 Razor 组件。

服务器端 Razor

Blazor 是在接收和处理事件的核心框架,无论周围环境如何。目前,唯一完全支持的方案是,在托管浏览器的 UI 线程和 ASP.NET Core 运行时内运行 Blazor。其他尚未支持的实际方案是,在 Web Worker 中或在甚至一些桌面授权平台(如 Electron)中运行 Blazor。托管在 Web Worker 中会让 Blazor 成为适合渐进式脱机 Web 应用程序的平台,而托管在 Electron 等平台上也是合理的。例如,简要比较一下非常基本的 Electron 启动指令(请访问 https://github.com/electron/electron-quick-start)与新服务器端 Blazor 项目运行时发生的情况。

Electron 的快速启动源代码包含以下方法:

function createWindow () { 
  mainWindow = new BrowserWindow({width: 
    800, height: 600}) 
  mainWindow.loadFile(‘index.html’)
}

此方法负责创建浏览器窗口,并将单独的 HTML 文件加载到其中。接下来发生的一切是,在外部环境(浏览器)和最里面 shell(HTML 网页)之间持续交换消息。中间的框架只负责妥善管理代理消息造成的负担。接下来看看在服务器端 Blazor 应用程序运行时会发生什么,如图 1 所示。

服务器端 Blazor 应用程序的加载阶段
图 1:服务器端 Blazor 应用程序的加载阶段

首先,浏览器加载用于定义主页面布局的应用程序级 index.html 文件(主要是 HEAD 标记),并且还会在下载过程中加载名为 blazor.server.js 的小型 JavaScript 文件。图 1 中的 _blazor/negotiate 步骤指明了,在浏览器的主机环境与 Blazor 应用程序之间建立 ASP.NET Core SignalR 连接的时刻。最后,连接升级到 Web 套接字。这是在图 1**** 中 Fiddler 屏幕捕获的最后一步中发生。

在这两种情况下,主机环境都加载初始值设定项文件。在连接建立后,应用程序逻辑运行,并生成所需的任何 HTML。在服务器端 Blazor 中,生成 HTML 的服务器端相当于当前呈现的 DOM,实际更新的内容只有 DOM 的必要片段。所有更新都是通过 SignalR 连接运行。

浏览器中捕获的用户单击则是反过来的。相关事件被打包并发送到服务器端,在这里事件由 Blazor JavaScript 互操作层在 C# 代码中进行处理。

Visual Studio 随附用于服务器端 Blazor 应用程序的临时项目模板。典型项目包括客户端项目和 ASP.NET Core 项目。最有可能需要添加 .NET Standard 库项目,用于保留前端和后端必须共用的类。

设置客户端应用程序

客户端应用程序的启动类几乎是空的:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
  }

  public void Configure(IBlazorApplicationBuilder app)
  {
    app.AddComponent<App>(“app”);
  }
}

唯一需要更改的是,注入对 HttpClient 的引用,以便任何 Blazor 页面都能执行 HTTP 远程调用:

public void ConfigureServices(IServiceCollection services)
{
  services.AddScoped<HttpClient>();
}

向客户端应用程序附加的最相关内容是,可便于用户填写和发布窗体的组件。在图 2**** 中,可以看到“用户注册”按钮生成的应用程序 UI。这是非常简单的普通 HTML 窗体,用于收集用户提供的电子邮件地址和密码,并将此类信息传递回远程后端。

示例窗体
图 2:示例窗体

其中有意思的是,需要有为图 2 中的输入域生成容器的标记。尽管容器的外观和行为都像 HTML 窗体一样,但它不是真正的 HTML 窗体,如图 3**** 所示。

图 3:生成(非 HTML)窗体的标记

<div class=”col-sm-6 col-md-4 offset-md-4”>
  <h3 class=”text-center login-title”>I Want To Be a New User</h3>
  <div class=”account-wall”>
    <div class=”form-signin”>
      <input type=”text” 
                   class=”form-control” 
                   name=”email” bind=”@Email” />
      <input type=”password” 
                   class=”form-control” 
                   name=”password” bind=”@Password” />
      <button class=”btn btn-primary 
                     onclick=”@TryRegister”>
        Sign me in
      </button>
    </div>
    <div class=”text-center”>@Message</div>
  </div>
</div>

可以看到,此标记是纯 HTML,但不需要 FORM 元素。最后,在客户端上运行的 C# 代码负责收集输入值,并将它们转发到可能是(或可能不是)ASP.NET 后端的远程终结点。

与 INPUT 元素关联的绑定属性可保证实现伪窗体与 Blazor 客户端页面属性之间的双向绑定。TryRegister 函数负责将内容发布到后端。图 4 展示了客户端页面的 @functions 代码块。

图 4:@functions 代码块

@inject HttpClient Http...
@functions
{
  public string Email { get; set; }
  public string Password { get; set; }
  public string Message = “?”;

  public async Task TryRegister()
  {
    var input = new RegisterUserViewModel(Email, Password);
    try
    {
      var response = await Http.PostJsonAsync<CommandResponse>(
        “.../account/register”, input);
      if (response.Success)
        Message = response.Message;
    }
    catch (Exception e)
    {
      Console.WriteLine(e);
      throw;
    }
  }
}

输入域的内容在数据传输对象 RegisterUserViewModel 的新实例中进行收集,并通过 HttpClient 的 PostJsonAsync 方法序列化为 JSON。后端接收 HTTP POST 请求,其中正文的设置如下:

{“email”:”...”,”password”:”...”}

在服务器端 Blazor 应用程序中,后端通常是 ASP.NET Core 应用程序。在这种情况下,请求转发到应用了模型绑定的控制器终结点。因此,已发布的数据将由同一 RegisterUserViewModel 类的服务器端实例捕获。但请注意,后端不一定是 ASP.NET Core 应用程序。通过 HttpClient 在调用中指定的 URL 必须是,可以将数据发布到几乎任何位置(包括一些基于旧版 Visual Basic 的后端)的绝对 URL。

设置服务器应用程序

在示例应用程序中,服务器应用程序是普通的 ASP.NET Core Web API,可以像保护任何此类 Web API 一样保护它。例如,可以通过 JWT 令牌抵御未经授权的访问,此令牌也很适用于非 Web 客户端,包括桌面客户端和移动客户端。现在,假设 Web API 允许匿名访问。若要成功接收和处理 Blazor 发布的任何数据,需要以下代码:

public class AccountController : Controller
{
  [HttpPost]
  public IActionResult Register(
    [FromBody] RegisterUserViewModel input)
  {
    // Some work here      ...
    return Json(CommandResponse.Ok.AddMessage(“Done”));
  }
}

FromBody 属性起着关键作用。如果没有此属性,Blazor 客户端那样对终结点的任何调用都会抛出模型绑定异常。

FromBody 属性指示 ASP.NET Core 运行时将参数绑定到传入 HTTP 请求正文中的数据。请注意,每操作使用 FromBody 属性修饰的参数最多只能有一个。

ASP.NET Core 将处理请求流的责任委托给专用的格式化程序类。默认类是 JsonInputFormatter。另请注意,一旦为一个参数读取了请求流,就无法再为其他参数读取了。流指针其实已提升到正文末尾,无法向后移动。

Web API 返回的响应是 Blazor 客户端接收和反序列化的另一个 JSON 响应,如下所示:

var response = await 
  Http.PostJsonAsync<CommandResponse>(absoluteUrl, input);

在上面的代码片段中,响应被反序列化为 CommandResponse 类型的实例;如果结果证明是不可能的,便会抛出异常。

浅谈安全性和身份验证

Blazor 的客户端部件在沙盒中运行,运行方式与 JavaScript 代码相同。服务器后端与客户端完全断开连接,并且只能在后端定义的条件下才可供访问。Blazor 客户端应用程序必须遵循包覆后端的任何安全层。就这一点而言,Blazor 是基于 Web 且调用 Web API 的普通客户端。它可以通过 Cookie 进行身份验证,但更有可能需要通过 JWT 持有者令牌进行身份验证。

服务器端 Blazor 方案可能就不是这样,因为它可以启用备用身份验证和授权方案。尚未就这一点做出任何决定,但可以访问 bit.ly/2CdS74c 了解最新讨论情况。很可能会提供某内置 API 来简化服务器端 Blazor 应用程序的登录和注销窗体的创建流程,以及用于执行其他常见身份验证操作,如恢复密码和与内部成员身份系统交互。Blazor 可能会以某种方式纳入 ASP.NET Core 标识的内置 UI。但同样,截至本文撰写之时,尚未做出任何决定。

Razor 组件的优缺点

服务器端 Blazor 或 Razor 组件将成为第一个达到上线状态(作为 ASP.NET Core 3.0 的一部分交付)的 Blazor 部分。一些开发人员可能会反对它的缺点。Blazor 用于设置纯 SPA 解决方案,可能与经典 ASP.NET Web 开发人员的编程体验相去甚远。另外,必须有 WebAssembly,才能在浏览器中运行 C#,这会产生下载和初始启动成本,同时调试也可能会出现问题。借助服务器端解决方案,开发体验更加顺畅,不仅调试和 JIT 编译过程得以改进,还能使用更多 API。更重要的是,所有浏览器都受支持,无论它们对 WebAssembly 的本机支持如何。对于经典 ASP.NET 开发人员,仅刷新 UI 部分变得几乎与桌面应用程序一样简单。

问题在于,这神奇的一切都依赖 SignalR 连接。这带来了一些潜在问题。首先,将有相当多的请求通过网络。事实上,任何更新都需要往返。其次,服务器被迫跟踪每个客户端及其状态。ASP.NET Core SignalR 同时作为 Azure 服务提供,这提供了良好的可伸缩性基础,但还没有人实际尝试过。最后,还没有可用于在 SignalR 连接断开时恢复应用程序状态的内置机制。任意永久性解决方案都很不错,但目前所有这些都要交由开发人员处理。

总而言之,Blazor 推广 SPA 模式,但 SPA 模式会对许多开发人员造成点阻碍。服务器端 Blazor(亦称为“Razor 组件”)是一种逐渐而零碎地向 SPA 体系结构推进的无阻碍、智能方式。本文的源代码基于 Blazor 0.7.0,可以从 bit.ly/2EpGF8d 获取。


Dino Esposito**** 在他 25 年的职业生涯中撰写了超过 20 本书籍和超过 1,000 篇文章。Esposito 不仅是舞台剧《事业中断》的作者,还是 BaxEnergy 的数字策略分析师,正忙于编写有助于建设环保世界的软件。可以在 Twitter 上关注他 (@despos)。

衷心感谢以下技术专家对本文的审阅:Jonathan Miller


在 MSDN 杂志论坛讨论这篇文章