Blazor和现代前端框架如React、Angular等都是类似的,它是组件化的。在Blazor项目的Components
目录中包含了许多以.razor
结尾的文件,它们都是Blazor组件。这篇笔记我们对Blazor组件的使用进行介绍。
Blazor组件定义在一个.razor
文件内,它遵循Razor模板语法,下面代码是最简单的Blazor组件,它显示一条文本信息。
Components/Shared/Hello.razor
@rendermode InteractiveServer
@inherits ComponentBase
<h1>@message</h1>
@code {
private string message = "Hello, Blazor!";
}
对于Blazor组件,它的文件名必须以大写开头,Hello.razor
的组件名就是Hello
。
Blazor组件的内容由3部分构成,分别是Razor指令、Razor模板(包含HTML标记)和C#代码。对于指令部分,@rendermode InteractiveServer
设置了该组件使用BlazorServer交互式渲染模式,如果不加该指令默认是静态渲染的,@inherits ComponentBase
指定该组件继承ComponentBase
类,这行代码不是必须的,所有Blazor组件都默认继承ComponentBase
类,除非你要继承其它的类(如LayoutComponentBase
等)否则一般不必明确写出。模板部分我们注意到有一个@message
的写法,这是Razor语法,它将一个变量显示在了指定位置,而组件的变量则在@code {}
代码块定义。
此外,我们将这个Hello.razor
放在了Components/Shared
文件夹下,这个目录结构不是强制性的,而是我们习惯于将全局共享的组件放在一个叫Shared
的文件夹里。
在其它页面或组件中,我们可以引入这个Hello.razor
组件,下面是一个例子。
Components/Pages/Home.razor
@page "/"
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello />
这是一个页面组件,当访问/
时该组件被渲染(有关路由系统将在后续章节介绍),指令中我们还使用了@using
引入了Hello.razor
组件所在的命名空间,最后我们直接以<Hello />
形式引入了组件。
Blazor的组件有生命周期回调函数,通过这些回调函数我们可以在组件创建、销毁等生命周期节点上插入自定义逻辑。
方法 | 说明 |
---|---|
SetParametersAsync |
设置由父组件或级联参数提供的参数值。 |
OnInitialized |
组件初始化时调用,适用于执行同步初始化操作。 |
OnInitializedAsync |
组件初始化时调用,适用于执行异步初始化操作,例如从服务器加载数据。 |
OnParametersSet |
在参数设置后且组件准备渲染之前调用,适用于处理参数变更后的同步逻辑。 |
OnParametersSetAsync |
在参数设置后且组件准备渲染之前调用,适用于处理参数变更后的异步逻辑。 |
OnAfterRender |
组件完成渲染后调用,适用于需要在渲染后操作DOM的同步操作。 |
OnAfterRenderAsync |
组件完成渲染后调用,适用于需要在渲染后操作DOM的异步操作。 |
ShouldRender |
决定组件是否需要重新渲染,返回 true 表示需要重新渲染,返回 false 跳过渲染。 |
Dispose |
当组件被销毁时调用,通常用于释放非托管资源或取消订阅事件等。 |
下面例子中,我们在组件初始化时,使用OnInitializedAsync
异步查询了一条数据,并显示在界面上。
@rendermode InteractiveServer
@inject StudentService studentService
@if (loading)
{
<div>Loading...</div>
}
else
{
<div>ID: @student?.Id</div>
<div>Name: @student?.Name</div>
}
@code {
private bool loading = true;
private Student? student;
protected override async Task OnInitializedAsync()
{
student = await studentService.GetStudentByIdAsync(1);
loading = false;
}
}
代码中,@inject
指令使用依赖注入系统注入了一个Service。对于从数据库加载数据这种逻辑,我们应该在异步版本的OnInitializedAsync()
中编写,而非使用同步版本的OnInitialized
。查询数据库是一个阻塞式的操作,如果我们全部使用阻塞式写法,这可能造成组件的渲染也被阻塞了,影响用户体验。
除了上面的生命周期方法,Blazor还有一个StateHasChanged()
方法用于手动重新渲染组件,在第一章中我们曾编写过类似如下代码。
@page "/hello"
@rendermode InteractiveServer
<h3 style="@colorStyle">Hello, Blazor!</h3>
@code {
private string colorStyle = "color: red";
private Timer? _timer;
protected override void OnInitialized()
{
_timer = new Timer(ToggleColor, null, 0, 1000);
}
private void ToggleColor(object? state)
{
colorStyle = colorStyle == "color: red" ? "color: blue" : "color: red";
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
_timer?.Dispose();
}
}
这个ToggleColor()
方法实际上是在定时器创建的子线程中运行的。在Blazor中,组件的状态变量在渲染线程中被改变时组件通常都会自动重新渲染,然而如果在其它线程修改了状态,我们就需要手动让Blazor组件重新渲染了。此外,在组件外修改了组件状态也需要调用StateHasChanged()
手动更新组件,后面我们使用@ref
从父组件调用子组件方法更新状态就是一个例子。
Blazor中组件的参数可以从外层传递进来,组件的参数传递如果想要暴露到外层,它就必须是一个public
的属性,并用[Parameter]
注解标注。下面例子中,我们的Hello.razor
组件支持传入Title
参数。
Components/Shared/Hello.razor
@rendermode InteractiveServer
<h1>@Title</h1>
@code {
[Parameter]
public string? Title { get; set; }
}
Components/Pages/Home.razor
@page "/"
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello Title="Hi, Tom!" />
注意:这里Home.razor
中我们没有加@rendermode InteractiveServer
,因为Home.razor
内没有交互式逻辑,它是静态渲染的,静态渲染的组件可以嵌套交互式组件。不过如果我们要在Home.razor
中编写交互式逻辑,就需要指定它为交互式渲染组件,否则会出现交互式代码无论如何都不生效的情况,Blazor的初学者可能普遍会遇到类似的问题。
如何想要传递一个函数作为参数也是可以的,这种情况下,我们需要声明一个EventCallback
类型的属性接收传入的参数,它是Blazor中对委托的封装。
Components/Shared/Hello.razor
@rendermode InteractiveServer
<button @onclick="OnButtonClick">Click me</button>
@code {
[Parameter]
public EventCallback OnButtonClick { get; set; }
}
Components/Pages/Home.razor
@page "/"
@rendermode InteractiveServer
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello OnButtonClick="HandleButtonClick" />
@code {
private string? message;
private void HandleButtonClick()
{
Console.WriteLine("Button clicked");
}
}
代码中,我们将函数HandleButtonClick
传递给了子组件,子组件的按钮点击时回调该函数。这里由于我们的Home.razor
包含了交互式逻辑,因此它也被声明为了@rendermode InteractiveServer
。
级联参数可以跨越多个组件的嵌套层级传递一个变量,这类似于React中的Context API。级联参数传递可以用于传递主题变量、登录信息等。
父组件中我们使用<CascadingValue>
配置级联参数。
@page "/"
@rendermode InteractiveServer
@using Gacfox.DemoBlazorServer.Components.Shared
<CascadingValue Value="theme">
<Hello />
</CascadingValue>
@code {
private string? theme = "light";
}
子组件中我们使用[CascadingParameter]
注解接收级联参数。
@rendermode InteractiveServer
<div>@theme</div>
@code {
[CascadingParameter] private string? theme { get; set; }
}
Blazor中,父组件调用子组件可以通过ref
实现,下面是一个例子。
@rendermode InteractiveServer
<div>@text</div>
@code {
private string? text;
public void SetText(string text)
{
this.text = text;
StateHasChanged();
}
}
@page "/"
@rendermode InteractiveServer
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello @ref="hello" />
<button @onclick="HandleButtonClick">Set Text</button>
@code {
private Hello? hello;
private void HandleButtonClick()
{
hello?.SetText("Hello from Home");
}
}
代码中,子组件提供了一个SetText()
用于设置组件内的text
状态变量,父组件使用@ref
获取了子组件的实例并在按钮点击时调用了这个方法。
注意这里text
状态的修改实际上是父组件触发的,因此我们需要调用StateHasChanged()
手动重新渲染子组件。当然,实际开发中其实较少用到@ref
,这种方式直接操作了子组件的内部状态,它对于组件的封装具有一定破坏性,不是一种符合直觉的代码执行流程。上面例子完全可以在子组件中封装参数并暴露给父组件,当参数改变时子组件会自动重新渲染。
RenderFragment渲染片段(前端中通常也被称为插槽)能够在父组件调用子组件时传入额外的子组件树,具体如何使用我们看下面例子。
默认插槽要求插槽变量名必须叫ChildContent
,作为插槽变量,它的类型是RenderFragment
。
@rendermode InteractiveServer
<h1>标题</h1>
<div>@ChildContent</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
父组件中,我们传入了额外的<p>...</p>
信息,这些内容在子组件中将以插槽变量的形式被接收。
@page "/"
@rendermode InteractiveServer
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello>
<p>这是一个段落</p>
</Hello>
如果我们有多个插槽或者不想使用ChildContent
这个默认插槽名,那么定义的就是具名插槽了,具名插槽使方式如下。
@rendermode InteractiveServer
<h1>标题</h1>
<div>@Content</div>
@code {
[Parameter]
public RenderFragment? Content { get; set; }
}
@page "/"
@rendermode InteractiveServer
@using Gacfox.DemoBlazorServer.Components.Shared
<Hello>
<Content>
<p>这是一个段落</p>
</Content>
</Hello>
父组件中,我们用插槽变量名<Content>...</Content>
包裹了插槽内容,这里我们也可以添加多个具名插槽。