构建更精简、更节能、更环保的Blazor组件

目录

介绍

Repo

为什么?

那么为什么每个人都使用ComponentBase呢?

精益、平均、绿色战略

简化生命周期流程

管理参数更改

不需要时不渲染

你需要AfterRender吗?

精益、平均、绿色的组件

UIBase

UIComponentBase

添加自动UI呈现

添加后渲染

渲染级联

一些演示实现

计数器页面

CounterState

CounterComponent.razor

Counter.Razor

天气记录视图

结论

附录

组件库


介绍

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能够主导BlazorUI。查看整个组件环境,看看是否可以发现任何不同的基类。

下面是市场上可用的两个常用Blazor库的基类:

public class RadzenComponent : ComponentBase, IDisposable

public abstract class MudComponentBase : ComponentBase

了解ComponentBase的优秀开发人员正在质疑组件的使用。他们认为简单的组件太了。他们有太多的头顶,背着太多的行李。他们编写重复的代码以避免在页面中构建太多组件。

我的答案是:不要扔掉组件:编写适合用途的基本组件。

我有两个主要的基本组件。它们基于我所谓的精益平均绿色组件——从现在开始的LMGC——我将在下面详细介绍。

精益、平均、绿色战略

简化生命周期流程

您的组件中有多少使用完整的生命周期方法?1%,如果那样的话。简化并删除大量代码和昂贵的任务构建,无目的。

管理参数更改

渲染组件时,渲染器必须确定是否需要重新渲染任何子组件。它通过ParametersView对象管理组件的参数状态。它检查是否有任何子组件参数已更改,如果是,则在ParametersView对象中传递调用SetParametersAsync

SetParametersAsync的第一行使用ParametersView来设置组件的参数。

parameters.SetParameterProperties(this);

此过程有两个问题。两者都不容易解决:

  1. 设置参数是一项成本高昂的工作,因为ParameterView使用反射来查找和分配参数值。
  2. 优化了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测试。

我的策略是:

  1. 尽可能坚持使用不可变类型。
  2. 忍受它。
  3. 如果组件被大量使用并且性能存在问题,请手动执行分配和更改检查。您通常可以编写回调和渲染片段,这些回调和渲染片段在最初分配后不会更改。
  4. 停止不必要的自上而下的组件树渲染级联。请参阅下一个策略。

不需要时不渲染

是的,双重否定——您应该只在需要时渲染组件。默认情况下不要这样做,这就是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

这是最低功能核心组件。

里面有什么:

  1. 它继承自IComponent
  2. 所有内部类字段都是protected,可以在子组件中访问和设置。
  3. 它没有用于驱动自动呈现请求的UI事件处理程序。当您想要发出渲染请求时调用StateHasChanged
  4. 没有AfterRender基础设施。如果需要,请实现它。
  5. 有两种StateHasChanged方法。
    1. StateHasChanged和熟悉的StateHasChanged一样。
    2. InvokeStateHasChanged确保在UI线程上调用StateHasChanged
  6. 没有生命周期事件。
  7. 与Razor组件兼容的BuildRenderTree方法。
  8. 它缓存renderFragment以提高效率。
  9. 一个Hidden参数,用于模拟可在外部设置的隐藏html属性。
  10. 可以在子类内部设置的类hide字段。
  11. 组件内容的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。它:

  1. bool中传递以指示首次渲染。
  2. 期望用于控制组件呈现的bool返回。它将始终呈现一次。
  3. ValueTask是为了节省开销。

OnParametersChangedAsync可用于:

  1. 做你在OnInitialized{Async}OnParametersSet{Async}中所做的一切。
  2. 检查已设置的参数并确定是否需要渲染。

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子组件。除非您在这些组件中实现了停止策略,否则渲染将在树中级联。

最小化这种情况的主要方法是:

  1. 将状态对象与事件一起使用以驱动更新。
  2. 在渲染树中的正确点中调用StateHasChanged
  3. 使用树顶部不会自动触发呈现事件的基本组件。

一些演示实现

计数器页面

此演示演示如何重新生成计数器页面。

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 &deg;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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值