Skip to content

Blazor 学习笔记

🏷️ Blazor

Blazor 是一个新的实验性的 .NET Web framework。该框架使用 C#/Razor 和 HTML 构建可以运行在浏览器(使用 WebAssembly )中的客户端 APP。

1. 获取 Blazor 0.3.0

  1. 安装 .NET Core 2.1 SDK (2.1.300-preview2-008533 或更新).

  2. 安装 Visual Studio 2017 (15.7 Preview 5 或更新),安装时选择 ASP.NET and web development workload 组件。

  3. 安装最新的 Blazor Language Services extension.

2. First Blazor

根据官方 Guide Get started with Blazor 写的示例 FirstBlazor

3. 知识点

组件(Components)

组件类(Component classes)

典型的是通过 .cshtml 文件定义组件。大体上类似于 ASP.NET MVC 中的 View,使用 Razor 引擎。区别是 Blazor 中的视图包含 C# 代码,编译时会把 HTML 和 C# 代码转化成组件类(component class),类名同文件名

类代码通过 @functions 块定义,一个 .cshtml 文件中可以包含多个 @function 块。块中可以定义属性、字段、方法。

html
<h1 style="font-style:@_headingFontStyle">@_headingText</h1>

@functions {
    private string _headingFontStyle = "italic";
    private string _headingText = "Put on your new Blazor!";
}

使用组件

使用 HeadingComponent.cshtml 组件:

html
<HeadingComponent />

组件参数

通过 [Parameter] 特性将私有属性定义为组件参数。使用组件时,通过同名的标签属性来对该属性赋值。

ParentComponent.cshtml:

html
@page "/ParentComponent"

<h1>Parent-child example</h1>

<ChildComponent Title="Panel Title from Parent">
    Child content of the child component is supplied by the parent component.
</ChildComponent>

ChildComponent.cshtml:

html
<div class="panel panel-success">
  <div class="panel-heading">@Title</div>
  <div class="panel-body">@ChildContent</div>
</div>

@functions {
    [Parameter]
    private string Title { get; set; }

    [Parameter]
    private RenderFragment ChildContent { get; set; }
}

子内容

上例中通过 RenderFragment 类型的属性获取父组件标签包含的内容。

数据绑定 (data binding)

通过 bind 属性绑定,值通过 @ 开头(如:bind="@_italicsCheck")。italicsCheck 控件的值会绑定到 _italicsCheck 字段或属性。

html
<input type="checkbox" class="form-check-input" id="italicsCheck" bind="@_italicsCheck" />

上例相当于如下代码:

html
<input value="@CurrentValue" 
    onchange="@((UIValueEventArgs __e) => CurrentValue = __e.Value)" />

数据格式化

通过 format-value 标签属性定义日期显示格式。

html
<input bind="@StartDate" format-value="yyyy-MM-dd" />

@functions {
    [Parameter]
    private DateTime StartDate { get; set; } = new DateTime(2020, 1, 1);
}
组件属性(Component attributes)

可以通过 bind-{property} 格式跨组件绑定属性。子组件中需要定义对应属性的 Changed 事件。

Parent component:

html
<ChildComponent bind-Year="@ParentYear" />

@functions {
    [Parameter]
    private int ParentYear { get; set; } = 1979;
}

Child component:

html
<div> ... </div>

@functions {
    [Parameter]
    private int Year { get; set; }

    [Parameter]
    private Action<int> YearChanged { get; set; }
}

事件操作(Event handling)

Blazor 可以通过控件的 on<event> 属性(如 onclick, onsubmit)绑定事件。

onclick

html
<button class="btn btn-primary" onclick="@UpdateHeading">
    Update heading
</button>

@functions {
    void UpdateHeading(UIMouseEventArgs e)
    {
        ...
    }
}

onchange

html
<input type="checkbox" class="form-check-input" onchange="@CheckboxChanged" />

@functions {
    void CheckboxChanged()
    {
        ...
    }
}

支持的事件参数:

  • UIEventArgs
  • UIChangeEventArgs
  • UIKeyboardEventArgs
  • UIMouseEventArgs

Lambda 表达式写法

html
<button onclick="@(e => Console.WriteLine("Hello, world!"))">Say hello</button>

生命周期方法(Lifecycle methods)

  • OnInitAsync & OnInit

    组件被初始化后执行

    cs
    protected override async Task OnInitAsync()
    {
        await ...
    }
    cs
    protected override void OnInit()
    {
        ...
    }
  • OnParametersSetAsync & OnParametersSet

    当从父组件接受参数且值已被赋到属性后执行;
    该方法在 OnInit 方法后执行;

    cs
    protected override async Task OnParametersSetAsync()
    {
        await ...
    }
    cs
    protected override void OnParametersSet()
    {
        ...
    }
  • OnAfterRenderAsync & OnAfterRender

    每当一个组件被渲染之后执行;
    此时组件元素及组件的引用已经可以被访问;
    可以在该阶段执行附加的初始化操作;

    cs
    protected override async Task OnAfterRenderAsync()
    {
        await ...
    }
    cs
    protected override void OnAfterRender()
    {
        ...
    }
  • SetParameters

    SetParameters 方法可以被重写,在参数被赋值前执行;

    cs
    public override void SetParameters(ParameterCollection parameters)
    {
        ...
    
        base.SetParameters(parameters);
    }

    If base.SetParameters isn't invoked, the custom code can interpret the incoming parameters value in any way required. For example, the incoming parameters aren't required to be assigned to the properties on the class.

    如果 base.SetParameters 没有被执行,这个自定义方法可以理解为入参始终需要。比如,入参不需要被赋值到属性上。
    不知道是不是我理解反了,怎么感觉怪怪的

  • ShouldRender

    ShouldRender 可以被重写以阻止 UI 的刷新。如果该实现返回 true 则 UI 已经刷新了。
    即使 ShouldRender 被重写,组件初始时始终会被渲染。

    cs
    protected override bool ShouldRender()
    {
        var renderUI = true;
    
        return renderUI;
    }
  • 通过 IDisposable 清理组件 (Component disposal with IDisposable)

    组件可以通过 @implements 继承接口,然后在 @functions 块中实现该接口。
    当组件从 UI 中移除时,Dispose 方法就会被调用。

    cs
    @using System
    @implements IDisposable
    
    ...
    
    @functions {
        public void Dispose()
        {
            ...
        }
    }

路由(Routing)

*.cshtml 文件通过 @page 指令定义路由。生成的 组件类 中会将其转换成 RouteAttribute 特性。
同一个组件可以定义多个路由。

html
@page "/BlazorRoute"
@page "/DifferentBlazorRoute"

<h1>Blazor routing</h1>

路由参数(Route parameters)

可以通过 @page 指令定义路由参数。

html
@page "/RouteParameter"
@page "/RouteParameter/{text}"

<h1>Blazor is @Text!</h1>

@functions {
    [Parameter]
    private string Text { get; set; } = "fantastic";
}

JavaScript/TypeScript 互操作

注册 JavaScript 脚本;

cs
Blazor.registerFunction('doPrompt', function(message) {
    return prompt(message);
});

从 .net 调用 JavaScript。

cs
public static bool DoPrompt(string message)
{
    return RegisteredFunction.Invoke<bool>("doPrompt", message);
}

代码后置

可以通过 @inherits 指令实现代码后置(Code-Behind)效果。后置的 class 必须继承自 BlazorComponent

BlazorRocks.cshtml:

html
@page "/BlazorRocks"
@*
    The inherit directive provides the properties and methods
    of the BlazorRocksBase class to this component.
*@
@inherits BlazorRocksBase

<h1>@BlazorRocksText</h1>

BlazorRocksBase.cs:

cs
using Microsoft.AspNetCore.Blazor.Components;

public class BlazorRocksBase : BlazorComponent
{
    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
}

Razor 支持

Razor 指令
DirectiveDescription
@functions添加一个代码块到组件。
@implements生成的组件类继承一个接口。
@inherits提供对组件继承的类的完全控制。(机翻的:)该指令指定的类替代自动生成的同名类控制当前组件)
@inject启用服务注入。
@layout指定一个 Layout 组件。Layout 组件用来避免代码重复和不一致。
@page指定这个组件可以直接处理请求。该指令可以指定路由及可选参数。该指令不是必须放在文件顶部。
@using添加 using 指令到生成的组件类中。
@addTagHelper通过该指令可使用别的 assembly 中的组件。
可选属性

如果值为 falsenull,Blazor 不会渲染该属性。
如果值为 true,则会最小化的渲染该属性。

html
<input type="checkbox" checked="@IsCompleted" />

@functions {
    [Parameter]
    private bool IsCompleted { get; set; }
}

IsCompletedtrue 时:

html
<input type="checkbox" checked />

IsCompletedfalse 时:

html
<input type="checkbox" />

布局(Layouts)

What are layouts?

Layout 组件必须继承自 BlazorLayoutComponentBlazorLayoutComponent 定义了一个 Body 属性,该属性包含了要被渲染到该 Layout 中的内容。

视图中通过 @Body 指令来指定 Body 属性的内容应该被渲染到页面的什么位置。

html
@inherits BlazorLayoutComponent

<header>
    <h1>ERP Master 3000</h1>
</header>

<nav>
    <a href="/master-data">Master Data Management</a>
    <a href="/invoicing">Invoicing</a>
    <a href="/accounting">Accounting</a>
</nav>

@Body

<footer>
    &copy; by @CopyrightMessage
</footer>

@functions {
    public string CopyrightMessage { get; set; }
    ...
}

在组件中使用 Layout

使用 @layout 指令来指定当前组件使用的 Layout。

html
@layout MasterLayout

@page "/master-data"

<h2>Master Data Management</h2>

Centralized layout selection

每个目录都包含一个名为 _ViewImports.cshtml 的模板文件。当前目录及子目录中的所有组件都会导入该模板。

嵌套布局(Nested layouts)

CustomersComponent.cshtml:

html
@layout MasterDataLayout

@page "/master-data/customers"

<h1>Customer Maintenance</h1>

MasterDataLayout.cshtml:

html
@layout MainLayout
@inherits BlazorLayoutComponent

<nav>
    <!-- Menu structure of master data module -->
    ...
</nav>

@Body

MainLayout.cshtml:

html
@inherits BlazorLayoutComponent

<header>...</header>
<nav>...</nav>

@Body

依赖注入(Dependency injection)

Add services to DI

Program.cs

cs
using Microsoft.Extensions.DependencyInjection

static void Main(string[] args)
{
    var serviceProvider = new BrowserServiceProvider(services =>
    {
        services.AddSingleton<IDataAccess, DataAccess>();
    });

    new BrowserRenderer(serviceProvider).AddComponent<App>("app");
}

服务可以定义一下三种生命周期:

  • Singleton
    单例服务。所有组件都使用该实例。
  • Transient
    每当一个组件使用时创建一个新的实例。
  • Scoped
    Blazor 暂没有该 DI 范围。Blazor 中该范围的行为类似于 Singleton。因为推荐使用 Singleton,避免使用 Scoped。

默认服务(Default services)

  • IUriHelper
    该 Helper 同 URIs 和 导航状态一起工作(单例)。
  • HttpClient
    提供发送 HTTP 请求和接受 HTTP 响应的方法(单例)。 HttpClient.BaseAddress 值为当前 APP 的 URI 前缀。

在组件中请求服务

使用 @inject 指令注入服务到组件。@inject 有两个参数:

  • Type name: 注入服务的类型。
  • Property name: 接受注入服务的属性名。该属性不需要手动创建。编译器会自动创建。

一个组件中可以使用多个 @inject 指令。

cs
@page "/customer-list"
@using Services
@inject IDataAccess DataRepository

<ul>
    @if (Customers != null)
    {
        @foreach (var customer in Customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    }
</ul>

@functions {
    private IReadOnlyList<Customer> Customers;

    protected override async Task OnInitAsync()
    {
        // The property DataRepository received an implementation
        // of IDataAccess through dependency injection. Use
        // DataRepository to obtain data from the server.
        Customers = await DataRepository.GetAllCustomersAsync();
    }
}

本质上,自动生成的属性(DataRepository)是用 InjectAttribute 特性装饰的。
通过使用代码后置方式的代码来实现相同的效果,看起来就很清晰了。

ComponentBase.cs

cs
public class ComponentBase : BlazorComponent
{
    // Blazor's dependency injection works even if using the
    // InjectAttribute in a component's base class.
    [Inject]
    protected IDataAccess DataRepository { get; set; }
}

demo.cshtml

html
@page "/demo"
@inherits ComponentBase

<h1>...</h1>

Service 中的依赖注入

DI 容器中定义的服务仍然可以使用依赖注入。仅支持构造函数依赖注入。

cs
public class DataAccess : IDataAccess
{
    // The constructor receives an HttpClient via dependency
    // injection. HttpClient is a default service offered by Blazor.
    public DataAccess(HttpClient client)
    {
        ...
    }
    ...
}

Service 中使用依赖注入需要满足以下条件:

  • 构造函数中的参数均可以被 DI 实现。附加参数如果定义了初值可以不被 DI 实现。
  • 构造函数必须是 public 的。
  • 有且只有一个合适的构造函数。有歧义时 DI 会抛出异常。

附加资料

路由(Routing)

路由模板

App.cshtml 中启用路由。

html
<!--
    Configuring this here is temporary. Later we'll move the app config
    into Program.cs, and it won't be necessary to specify AppAssembly.
-->
<Router AppAssembly=typeof(Program).Assembly />

在组件中通过 @page 定义路由,生成的组件类会具有 RouteAttribute 特性。
运行时,路由器会在所有带有 RouteAttribute 特性的组件中查询匹配当前请求地址的组件。

一个组件可以定义多个路由。

html
@page "/BlazorRoute"
@page "/DifferentBlazorRoute"

<h1>Blazor routing</h1>

路由参数

生成的是 <a> 标签控件,但是会根据当前的 URL 设置标签的 active 样式。

html
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>
            <NavLink href="/" Match=NavLinkMatch.All>
                <span class="glyphicon glyphicon-home"></span> Home
            </NavLink>
        </li>
        <li>
            <NavLink href="/BlazorRoute">
                <span class="glyphicon glyphicon-th-list"></span> Blazor Route
            </NavLink>
        </li>
    </ul>
</div>

Match 属性有两个可选值:

  • NavLinkMatch.All
    匹配当前整个 URL 时,链接显示为激活状态;
  • NavLinkMatch.Prefix
    配置当前 URL 的任何前缀时,链接显示为激活状态;

JavaScript 交互

在 .NET 方法中调用 JavaScript 方法

  1. 注册 JavaScript 方法

    在 JavaScript 中通过 Blazor.registerFunction('functionName', functionImplementation) 注册 JavaScript 方法。
    下例中注册的 echo 方法是同步方法;echoAsync 方法是异步方法。

    js
    Blazor.registerFunction(
        'echo',
        function(message){
            return message;
        });
    
    Blazor.registerFunction(
        'echoAsync',
        function(message){
            return Promise.Resolve(message);
        });
  2. 在 .NET 方法中调用 JavaScript 方法

    使用 T Invoke<T>(functionName, args) 调用同步方法;
    使用 Task<T> InvokeAsync<T>(functionName, args) 调用异步方法;

    cs
    var helloWorld = RegisteredFunction.Invoke<string>("echo", "Hello world!");
    var helloWorldAsync =
        await RegisteredFunction
            .InvokeAsync<string>("echoAsync", "Hello world async!");

在 JavaScript 中调用 .NET 方法

Blazor 中可以通过 Blazor.invokeDotNetMethodBlazor.invokeDotNetMethodAsync 方法调用 .NET 方法。被调用的 .NET 方法必须满足以下特性:

  • 静态方法
  • 非泛型
  • 没有被重载
  • 具体类型的方法参数
  • 参数能够使用 JSON 序列化
  1. 创建 .NET 方法

    cs
    namespace Alerts
    {
        public class Timeout
        {
            public static void TimeoutCallback()
            {
                Console.WriteLine('Timeout triggered!');
            }
        }
    }
  2. 在 JavaScript 中调用 .NET 方法

    在 JavaScript 中使用 Blazor.invokeDotNetMethodBlazor.invokeDotNetMethodAsync 调用 .NET 方法。需要指定 .NET Assembly、带全命名空间的类型名和需要调用的方法名。

    js
    Blazor.invokeDotNetMethod({
        type: {
            assembly: 'MyTimeoutAssembly',
            name: 'Alerts.Timeout'
        },
        method: {
            name: 'TimeoutCallback'
        }
    });

存储和部署

Publish the app

在 Visual Studio 中直接使用发布向导就可以发布了。
命令行使用 dotnet publish 发布。

bat
dotnet publish -c Release

会发布到 /bin/Release/<target-framework>/publish 目录。

Rewrite URLs for correct routing

在客户端 APP(一般是 index.html)加载后,所有的页面请求都不会触发浏览器的请求,而是由路由在内部处理这些请求。

App base path

如果 App 不是放在网站根目录,而是放在某个虚拟目录(如 /CoolBlazorApp/)中时,需要在 index.html 中的 <head> 标签中的 <base> 标签的 href 属性中设置基本路径。
href 属性从默认值 / 改为 /<virtual-path>/(斜杠不可以省略)。

部署模式

有以下两种部署模式:

  • 通过 ASP.NET Core 部署 - 通过一个 ASP.NET Core app 服务器部署 Balzor app。
  • 独立部署 - 部署在一个静态托管 Web 服务器上。
通过 ASP.NET Core 部署

ASP.NET Core 必须满足满足以下要求:

  • 引用 Blazor app 工程

  • 在 ASP.NET Core 工程的 Startup.Configure 中通过 UseBlazor 启用上面引用工程的 Blazor。

    cs
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseBlazor<Client.Program>();
    }

UseBlazor 扩展方法执行了如下任务:

  • 配置静态文件中间件,提供 dist 目录中 Balzor 静态资源的访问。在开发环境,这些文件在 wwwroot 目录中。
  • 配置单页面应用程序的路由。当服务器上不存在对应的静态文件、对应的 API 或者 Action 时,默认总是返回 index.html 页面。

当 ASP.NET Core 工程被发布时,引用的 Blazor 工程也会被包含在发布的输出中。

独立部署

仅 Blazor 的客户端会被发布。ASP.NET Core 的服务器端 App 不能作为该 Blazor app 的宿主。但 Blazor app 的静态文件可以通过 静态文件 Web Server 或者 Service 被浏览器直接请求。

Blazor 也可以部署到 IIS 上。参考Build a Static Website on IIS

web.config

Blazor 项目默认发布到 \bin\Release&lt;target-framework>\publish 目录。该目录下会自动生成一个 Web.config 文件。

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <staticContent>
      <remove fileExtension=".dll" />
      <remove fileExtension=".json" />
      <remove fileExtension=".wasm" />
      <remove fileExtension=".woff" />
      <remove fileExtension=".woff2" />
      <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json" mimeType="application/json" />
      <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
      <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
      <mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
    </staticContent>
    <httpCompression>
      <dynamicTypes>
        <add mimeType="application/octet-stream" enabled="true" />
        <add mimeType="application/wasm" enabled="true" />
      </dynamicTypes>
    </httpCompression>
    <rewrite>
      <rules>
        <rule name="Serve subdir">
          <match url=".*" />
          <action type="Rewrite" url="FirstBlazor\dist\{R:0}" />
        </rule>
        <rule name="SPA fallback routing" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
          </conditions>
          <action type="Rewrite" url="FirstBlazor\dist\" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
  • 设置下列 MIME 类型
    • *.dll: application/octet-stream
    • *.json: application/json
    • *.wasm: application/wasm
    • *.woff: application/font-woff
    • *.woff2: application/font-woff
  • 启用了下列 MIME 类型的 HTTP 压缩
    • application/octet-stream
    • application/wasm
  • 设定了 URL 重写规则
    • 以应用程序的静态资源所在的子目录为根目录(<assembly_name>\dist&lt;path_requested>
    • 创建 SPA fallback routing ,使那些没有文件的资源重定向到 App 的默认文档(<assembly_name>\dist\index.html)。
安装 URL 重写模块

重写 URLs 需要URL Rewrite Module。该模块默认是不安装的,而且也不能作为一个 Web 角色服务特征安装。必须从 IIS 网站下载并执行安装程序。

  1. 访问 URL Rewrite Module downloads page。英文版下载 WebPI 安装程序,其它语言选择对应合适的安装程序。
  2. 复制安装程序到服务器并执行。安装后服务器不需要重启。
配置 Web 站点

设置站点的物理路径为 Blazor app 的目录,该目录需要包含以下内容:

  • web.config(包含重定向规则和文件内容类型);
  • app 的静态资源文件夹;
故障修复

如果试图访问站点配置文件时发生异常并接收到 500 服务错误,确认 URL 重写模块(URL Rewrite Module)是否已安装。当该模块未安装时,web.config 不能被 IIS 正确的识别。

性能

决定是否使用 Blazor 的最终还是性能,下面是在 Stack Overflow 上关于 Blazor performance 的一个回答。

Is this system faster to work than, for example, React / Vue, compiled in JavaScript?

Blazor uses web assembly, On paper web assembly should be faster than any js library however not all browsers have a mature web assembly parsers yet. So you might find that browsers will not run web assembly in an optimal speed as of now.

You can create a small blazor app and run it in Firefox, chrome and edge. In most cases Firefox runs blazor apps much faster than chrome or edge, which implies that browser makes still need to improve, even Firefox can improve.

If your app needs to access DOM frequently, then definitely web assembly / Blazor will be slower compared to any JS libraries since web assembly can’t directly access DOM without using Invokes (Which is slow at the moment, please refer my blazer benchmark below).

On Firefox 10,000 RegisteredFunction.InvokeUnmarshalle calls to empty methods takes 250ms while chrome and edge need more than 2400ms in my PC.’ In pure JS it takes below 10 millisonds for the same scenario.

https://webassemblycode.com/webassembly-cant-access-dom/

Additionally, current implementation Blazor has its own MSIL engine on top of the browsers web assembly Engine, which means there are two interpreters working to run a Blazor project, Like two translators interpreting a conversation instead on one. Currently Microsoft is working on an AOT compiler, which is not yet release. Once its release Blazor will be much faster than the current implementation.

http://www.mono-project.com/news/2018/01/16/mono-static-webassembly-compilation/

We can safely assume that the web assembly is the future of web development, but at the moment we can’t say anything about Blazor’s future. On paper Blazor can be faster than any framework out there, however we need commitment from web assembly maintainers, Browser developers, Microsoft and the communities to make the theories practical.

简单来说,Blazor 毕竟还是一个实验性质的项目,暂时不具备放到生产环境的条件。

附 1. 参考文档