目录
介绍
Blazor附带一个开发人员“Component”。如果添加Razor文件,则默认情况下它会继承自该文件。
ComponentBase统治着Blazor UI世界。您不必使用它,但可能99.x的所有开发人员构建的组件直接或间接继承自它。
在一个多元化的世界中,我们有一个一刀切的瑞士军刀解决方案。万事通,一无是处。
大多数文章将ComponentBase和“Blazor组件”视为同义词。
ComponentBase应该只是工具箱中的一个工具,而不是工具箱。我可能是少数。但我很少使用它。
Repo
本文的存储库位于此处:Blazr.Components。
为什么?
有效的问题。我的应用程序与ComponentBase一起运行得非常好。我的大多数人都这样做,但这不是使用它的理由。
考虑一下:
- 组件内存占用量中的大多数代码永远不会运行。这只是英国媒体报道软件:内存占用什么都不做。
- 组件生成的大多数呈现事件不会导致UI更改。使用的CPU周期一无所获。
- 它没有解决一些关键的继承问题。
总结一下为什么不:它占用了它没有使用的内存空间,并且毫无目的地消耗CPU周期。这是金钱和精力付诸东流。
你真的写精益、节能、绿色的代码吗?
让我说明一下我的观点。
这是一个“简单”的组件。它是一个引导容器。
<div class="container">
@ChildContent
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
}
看起来非常简单,可能会通过代码审查。
现在看看这个?我还没有给你看150+行——我不想要TLDR!
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
// 150+ lines
// See Appendix for the 150+ lines
}
这就是上述组件的真实外观。你认为这会通过代码审查吗?
那么为什么每个人都使用ComponentBase呢?
从来没有想过,问了正确的问题,懒惰,不知道更好。组件库供应商——不知道,这些组织中有很多聪明的人。它们可能会显示绿色凭据,但每次渲染其中一个组件时,它都会消耗更多的能量。
如果您编写的每个组件都派生自ComponentBase,则需要认真考虑原因。
引用ComponentBase源代码:
大多数面向开发人员的组件生命周期概念都封装在此基类中。核心组件渲染系统不知道它们(它只知道IComponent)。这使我们能够灵活地轻松更改生命周期概念,或者让开发人员将自己的生命周期设计为不同的基类。
我不认为那个评论的作者期望ComponentBase能够主导Blazor的UI。查看整个组件环境,看看是否可以发现任何不同的基类。
下面是市场上可用的两个常用Blazor库的基类:
public class RadzenComponent : ComponentBase, IDisposable
public abstract class MudComponentBase : ComponentBase
了解ComponentBase的优秀开发人员正在质疑组件的使用。他们认为简单的组件太贵了。他们有太多的头顶,背着太多的行李。他们编写重复的代码以避免在页面中构建太多组件。
我的答案是:不要扔掉组件:编写适合用途的基本组件。
我有两个主要的基本组件。它们基于我所谓的精益平均绿色组件——从现在开始的LMGC——我将在下面详细介绍。
精益、平均、绿色战略
简化生命周期流程
您的组件中有多少使用完整的生命周期方法?1%,如果那样的话。简化并删除大量代码和昂贵的任务构建,无目的。
管理参数更改
渲染组件时,渲染器必须确定是否需要重新渲染任何子组件。它通过ParametersView对象管理组件的参数状态。它检查是否有任何子组件参数已更改,如果是,则在ParametersView对象中传递调用SetParametersAsync。
SetParametersAsync的第一行使用ParametersView来设置组件的参数。
parameters.SetParameterProperties(this);
此过程有两个问题。两者都不容易解决:
- 设置参数是一项成本高昂的工作,因为ParameterView使用反射来查找和分配参数值。
- 优化了ParameterView检测状态变化的方法,但相对粗糙。
代码如下:
public static bool MayHaveChanged<T1, T2>(T1 oldValue, T2 newValue)
{
var oldIsNotNull = oldValue != null;
var newIsNotNull = newValue != null;
// Only one is null so different
if (oldIsNotNull != newIsNotNull)
return true;
var oldValueType = oldValue!.GetType();
var newValueType = newValue!.GetType();
if (oldValueType != newValueType)
return true;
if (!IsKnownImmutableType(oldValueType))
return true;
return !oldValue.Equals(newValue);
}
private static bool IsKnownImmutableType(Type type)
=> type.IsPrimitive
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Type)
|| type == typeof(decimal)
|| type == typeof(Guid);
回调和RenderFragment是对象,总是无法通过IsKnownImmutableType测试。
我的策略是:
- 尽可能坚持使用不可变类型。
- 忍受它。
- 如果组件被大量使用并且性能存在问题,请手动执行分配和更改检查。您通常可以编写回调和渲染片段,这些回调和渲染片段在最初分配后不会更改。
- 停止不必要的自上而下的组件树渲染级联。请参阅下一个策略。
不需要时不渲染
是的,双重否定——您应该只在需要时渲染组件。默认情况下不要这样做,这就是ComponentBase所做的。
下面是UI事件的ComponentBase处理程序:
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
如果您不实现IHandleEvent,那么您有责任在需要时调用StateHasChanged。
你需要AfterRender吗?
ComponentBase实现一组呈现后事件。
Task IHandleAfterRender.OnAfterRenderAsync()
{
var firstRender = !_hasCalledOnAfterRender;
_hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
}
可能99%的组件不需要它们。因此,在极少数需要的情况下手动实现IHandleAfterRender。
精益、平均、绿色的组件
基于我们上面讨论的内容,我们可以构建一组新的基本组件。
UIBase
这是最低功能核心组件。
里面有什么:
- 它继承自IComponent。
- 所有内部类字段都是protected,可以在子组件中访问和设置。
- 它没有用于驱动自动呈现请求的UI事件处理程序。当您想要发出渲染请求时调用StateHasChanged。
- 没有AfterRender基础设施。如果需要,请实现它。
- 有两种StateHasChanged方法。
- StateHasChanged和熟悉的StateHasChanged一样。
- InvokeStateHasChanged确保在UI线程上调用StateHasChanged。
- 没有生命周期事件。
- 与Razor组件兼容的BuildRenderTree方法。
- 它缓存renderFragment以提高效率。
- 一个Hidden参数,用于模拟可在外部设置的隐藏html属性。
- 可以在子类内部设置的类hide字段。
- 组件内容的ChildContent参数。
Hidden/hide构建就是在这个级别,因此可以在组件renderFragment中有效地实现。
public abstract class UIBase : IComponent
{
protected RenderFragment renderFragment;
protected internal RenderHandle renderHandle;
protected bool hasPendingQueuedRender = false;
protected internal bool hasNeverRendered = true;
protected bool hide;
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public bool Hidden { get; set; } = false;
public UIBase()
{
renderFragment = builder =>
{
hasPendingQueuedRender = false;
hasNeverRendered = false;
if (!(Hidden | hide))
BuildRenderTree(builder);
};
}
protected virtual void BuildRenderTree(RenderTreeBuilder builder) { }
protected void StateHasChanged()
{
if (hasPendingQueuedRender)
return;
hasPendingQueuedRender = true;
renderHandle.Render(renderFragment);
}
protected void InvokeStateHasChanged()
=> renderHandle.Dispatcher.InvokeAsync(StateHasChanged);
public void Attach(RenderHandle renderHandle)
=> this.renderHandle = renderHandle;
public virtual Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
StateHasChanged();
return Task.CompletedTask;
}
}
UIComponentBase
UIComponentBase添加单个生命周期事件OnParametersChangedAsync。它:
- 在bool中传递以指示首次渲染。
- 期望用于控制组件呈现的bool返回。它将始终呈现一次。
- ValueTask是为了节省开销。
OnParametersChangedAsync可用于:
- 做你在OnInitialized{Async}和OnParametersSet{Async}中所做的一切。
- 检查已设置的参数并确定是否需要渲染。
public abstract class UIComponentBase : UIBase
{
protected bool initialized;
protected virtual ValueTask<bool> OnParametersChangedAsync(bool firstRender)
=> ValueTask.FromResult(true);
public override async Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
var dorender = await this.OnParametersChangedAsync(!initialized)
|| hasNeverRendered
|| !hasPendingQueuedRender;
if (dorender)
this.StateHasChanged();
this.initialized = true;
}
}
添加自动UI呈现
如果需要自动UI呈现,请实现IHandleEvent。
对于单个渲染:
@implements IHandleEvent
//...
@code {
public async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
await callback.InvokeAsync(arg);
StateHasChanged();
}
}
对于双重事件:
@implements IHandleEvent
//...
@code {
public async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
if (task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled)
{
StateHasChanged();
await task;
}
StateHasChanged();
}
}
添加后渲染
如果需要实现OnAfterRender事件,请实现IHandleAfterRender。
@implements IHandleAfterRender
//...
@code {
private bool _hasCalledOnAfterRender;
public Task OnAfterRenderAsync()
{
var firstRender = !_hasCalledOnAfterRender;
_hasCalledOnAfterRender |= true;
// your code here
return Task.CompletedTask;
}
}
渲染级联
要实现的最重要的策略之一是避免渲染级联。
如果渲染具有具有对象参数的子组件的组件,则无论实际状态更改如何,渲染器都将调用SetParametersAsync子组件。除非您在这些组件中实现了停止策略,否则渲染将在树中级联。
最小化这种情况的主要方法是:
- 将状态对象与事件一起使用以驱动更新。
- 在渲染树中的正确点中调用StateHasChanged。
- 使用树顶部不会自动触发呈现事件的基本组件。
一些演示实现
计数器页面
此演示演示如何重新生成计数器页面。
CounterState
我们需要一个状态对象来跟踪计数器状态。
public class CounterState
{
public int Counter { get; private set; }
public Action<int>? CounterUpdated;
public void IncrementCounter()
{
this.Counter++;
this.CounterUpdated?.Invoke(this.Counter);
}
}
CounterComponent.razor
CounterComponent显示计数器。它继承并实现UIComponentBaseIDisposable。
它比标准组件复杂一些,但非常不言自明。
@namespace Blazr.Components
@implements IDisposable
@inherits UIComponentBase
<div class="alert alert-info">
@this.Counter
</div>
@code {
[CascadingParameter] private CounterState State { get; set; } = default!;
private int Counter;
protected override ValueTask<bool> OnParametersChangedAsync(bool firstRender)
{
if (firstRender)
{
if (this.State is null)
throw new NullReferenceException
($"State cannot be null in Component {this.GetType().Name}");
this.State.CounterUpdated += this.OnCounterUpdated;
}
return ValueTask.FromResult(true);
}
private void OnCounterUpdated(int counter)
{
this.Counter = counter;
this.StateHasChanged();
}
public void Dispose()
=> this.State.CounterUpdated -= this.OnCounterUpdated;
}
Counter.Razor
Counter实现UIBase:它不需要生命周期事件。它会创建CounterState的实例,并级联它,并在单击按钮时对其进行更新。有三个CounterComponent实例用于演示事件的多播功能。
我保留了旧的计数器代码,因此您可以看到它不再更新。IncrementCounter不再触发路由组件的渲染,因此不再触发渲染级联。
@page "/counter"
@inherits UIBase
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<CascadingValue Value="this.counterState">
<CounterComponent />
<CounterComponent />
<CounterComponent />
</CascadingValue>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private CounterState counterState = new CounterState();
private void IncrementCount()
{
currentCount++;
this.counterState.IncrementCounter();
}
}
天气记录视图
这演示了SetParametersAsync中的选择性呈现。前进和后退按钮在记录集上上下移动并重新加载路由。组件用_id跟踪当前记录,并在OnParametersChangedAsync中检查更新后的参数Id。它只在Id更改时呈现(返回true)。
@page "/WeatherView/{Id:int}"
@inherits UIComponentBase
@inject NavigationManager NavManager
<h3>WeatherViewer</h3>
<div class="row mb-2">
<div class="col-3">
Date
</div>
<div class="col-3">
@this.record.Date
</div>
</div>
<div class="row mb-2">
<div class="col-3">
Temperature °C
</div>
<div class="col-3">
@this.record.TemperatureC
</div>
</div>
<div class="row mb-2">
<div class="col-3">
Summary
</div>
<div class="col-6">
@this.record.Summary
</div>
</div>
<div class="m-2">
<button class="btn btn-dark" @onclick="() => this.Move(-1)">Previous</button>
<button class="btn btn-primary" @onclick="() => this.Move(1)">Next</button>
</div>
@code {
private int _id;
private WeatherForecast record = new();
[Parameter] public int Id { get; set; } = 0;
protected override async ValueTask<bool> OnParametersChangedAsync(bool firstRender)
{
var recordChanged = !this.Id.Equals(_id);
if (recordChanged)
{
_id = this.Id;
this.record = await GetForecast(this.Id);
}
return recordChanged;
}
private static async ValueTask<WeatherForecast> GetForecast(int id)
{
await Task.Delay(100);
return new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(id)),
TemperatureC = id,
Summary = "Testing"
};
}
private void Move(int value)
=> this.NavManager.NavigateTo($"/WeatherView/{_id + value}");
}
结论
如果本文不是对严肃的Blazor开发人员重新思考组件的警钟,那么我失败了!
走出ComponentBase的舒适区需要什么。您正在一个万事通、万事通的基类上构建整个UI。它几乎涵盖了几乎所有可能性。
很高兴能帮助您入门。学习绳索,看看引擎盖下。但随后继续前进。
有关如何从这些基本组件生成窗体和组件库的更多文章。
附录
组件库
这是随您构建的每个继承自ComponentBase的组件一起加载的代码。
public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
{
private readonly RenderFragment _renderFragment;
private RenderHandle _renderHandle;
private bool _initialized;
private bool _hasNeverRendered = true;
private bool _hasPendingQueuedRender;
private bool _hasCalledOnAfterRender;
public ComponentBase()
{
_renderFragment = builder =>
{
_hasPendingQueuedRender = false;
_hasNeverRendered = false;
BuildRenderTree(builder);
};
}
protected virtual void BuildRenderTree(RenderTreeBuilder builder) { }
protected virtual void OnInitialized() { }
protected virtual Task OnInitializedAsync() => Task.CompletedTask;
protected virtual void OnParametersSet() { }
protected virtual Task OnParametersSetAsync() => Task.CompletedTask;
protected virtual bool ShouldRender() => true;
protected virtual void OnAfterRender(bool firstRender) { }
protected virtual Task OnAfterRenderAsync(bool firstRender) => Task.CompletedTask;
protected Task InvokeAsync(Action workItem) =>
_renderHandle.Dispatcher.InvokeAsync(workItem);
protected Task InvokeAsync(Func<Task> workItem) =>
_renderHandle.Dispatcher.InvokeAsync(workItem);
protected void StateHasChanged()
{
if (_hasPendingQueuedRender)
return;
if (_hasNeverRendered || ShouldRender() ||
_renderHandle.IsRenderingOnMetadataUpdate)
{
_hasPendingQueuedRender = true;
try
{
_renderHandle.Render(_renderFragment);
}
catch
{
_hasPendingQueuedRender = false;
throw;
}
}
}
void IComponent.Attach(RenderHandle renderHandle)
{
if (_renderHandle.IsInitialized)
throw new InvalidOperationException
($"The render handle is already set. Cannot initialize a
{nameof(ComponentBase)} more than once.");
_renderHandle = renderHandle;
}
public virtual Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (!_initialized)
{
_initialized = true;
return RunInitAndSetParametersAsync();
}
else
return CallOnParametersSetAsync();
}
private async Task RunInitAndSetParametersAsync()
{
OnInitialized();
var task = OnInitializedAsync();
if (task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled)
{
StateHasChanged();
try
{
await task;
}
catch
{
if (!task.IsCanceled)
throw;
}
}
await CallOnParametersSetAsync();
}
private Task CallOnParametersSetAsync()
{
OnParametersSet();
var task = OnParametersSetAsync();
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
}
catch
{
if (task.IsCanceled)
return;
throw;
}
StateHasChanged();
}
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}
Task IHandleAfterRender.OnAfterRenderAsync()
{
var firstRender = !_hasCalledOnAfterRender;
_hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
}
}
https://www.codeproject.com/Articles/5345758/Building-Leaner-Meaner-Greener-Blazor-Components