组件化

Blazor和现代前端框架如React、Angular等都是类似的,它是组件化的。在Blazor项目的Components目录中包含了许多以.razor结尾的文件,它们都是Blazor组件。这篇笔记我们对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 渲染片段(插槽)

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>包裹了插槽内容,这里我们也可以添加多个具名插槽。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap