perf(Message): improve performance (#7094)

* doc: 更新示例

* chore: 更新字典

* refactor: 精简代码

* refactor: 移除 Clear 方法参数传递

* chore: 增加字典

* feat: 支持全局配置自动隐藏时长设置

* doc: 更新注释

* feat: 精简反转逻辑

* refactor: 更新 Clear 方法

* chore: 增加命名空间

* refactor: 更新 init 接口参数

* refactor: 重构 Dismiss 逻辑提高性能

* fix: 修复异步消息未清除 dom 问题

* refactor: 更新关闭逻辑

* refactor: 移除 click 事件委托

* refactor: 精简代码

* test: 更新单元测试

* refactor: 精简代码

* perf: 提高性能
This commit is contained in:
Argo Zhang
2025-11-11 12:48:19 +08:00
committed by GitHub
parent e7d93b0fc2
commit 8b8ef2c009
7 changed files with 121 additions and 89 deletions

View File

@@ -121,3 +121,5 @@ dotx
Modbus
Protocol
vditor
alertdialog
blazorbootstrap

View File

@@ -1,4 +1,4 @@
@page "/message"
@page "/message"
@inject IStringLocalizer<Messages> Localizer
@inject MessageService MessageService
@@ -20,8 +20,18 @@ private MessageService? MessageService { get; set; }
});</Pre>
<DemoBlock Title="@Localizer["MessagesNormalTitle"]" Introduction="@Localizer["MessagesNormalIntro"]" Name="Normal">
<button class="btn btn-primary" @onclick="@ShowMessage">@Localizer["MessagesMessagePrompt"]</button>
<Message @ref="Message" Placement="Placement.Bottom" />
<section ignore class="row form-inline g-3">
<div class="col-12">
<BootstrapInputGroup>
<BootstrapInputGroupLabel DisplayText="Placement"></BootstrapInputGroupLabel>
<RadioList @bind-Value="@_placement" Items="@_items"></RadioList>
</BootstrapInputGroup>
</div>
<div class="col-12">
<button class="btn btn-primary" @onclick="@ShowMessage">@Localizer["MessagesMessagePrompt"]</button>
</div>
</section>
<Message @ref="Message" Placement="@Placement"></Message>
</DemoBlock>
<DemoBlock Title="@Localizer["MessagesAsyncTitle"]" Introduction="@Localizer["MessagesAsyncIntro"]" Name="Async">
@@ -70,7 +80,6 @@ private MessageService? MessageService { get; set; }
<DemoBlock Title="@Localizer["MessagesTemplateTitle"]" Introduction="@Localizer["MessagesTemplateIntro"]" Name="Template">
<button class="btn btn-primary" @onclick="@ShowTemplateMessage">@Localizer["MessagesTemplatePrompt"]</button>
<Message @ref="Message1" Placement="Placement.Bottom" />
</DemoBlock>
<DemoBlock Title="@Localizer["MessagesShowModeTitle"]" Introduction="@Localizer["MessagesShowModeIntro"]" Name="ShowMode">

View File

@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
@@ -18,13 +18,20 @@ public sealed partial class Messages
private readonly MessageOption _option = new();
private long _count = 0;
private string _placement = "Top";
private readonly List<SelectedItem> _items = [new SelectedItem("Top", "Top"), new SelectedItem("Bottom", "Bottom")];
private Placement Placement => _placement == "Top" ? Placement.Top : Placement.Bottom;
private async Task ShowMessage()
{
Message.SetPlacement(Placement.Top);
await MessageService.Show(new MessageOption()
{
Content = "This is a reminder message"
});
Content = $"This is a reminder message {_count++}"
}, Message);
}
private async Task ShowAsyncMessage()
@@ -97,7 +104,7 @@ public sealed partial class Messages
{
await MessageService.Show(new MessageOption()
{
Content = $"This is a reminder message - {DateTime.Now:mm:ss}",
Content = $"This is a reminder message - {_count++}",
Icon = "fa-solid fa-circle-info",
}, Message1);
}
@@ -111,13 +118,11 @@ public sealed partial class Messages
});
}
private int lastCount = 0;
private Task ShowLastOnlyMessage() => MessageService.Show(new MessageOption()
{
ShowShadow = true,
ShowMode = MessageShowMode.Single,
Content = lastCount++.ToString()
Content = $"This is a reminder message - {_count++}"
});
private static AttributeItem[] GetAttributes() =>

View File

@@ -1,59 +1,29 @@
@namespace BootstrapBlazor.Components
@namespace BootstrapBlazor.Components
@inherits BootstrapModuleComponentBase
@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]
<div id="@Id" class="@ClassString" style="@StyleName" role="alert">
@if (Placement == Placement.Top)
@foreach (var item in MessagesForRender)
{
foreach (var item in _messages)
{
<div @key="item" id="@GetItemId(item)" role="alertdialog" class="@GetItemClassString(item)" data-bb-autohide="@GetAutoHideString(item)" data-bb-delay="@item.Delay">
@if (!string.IsNullOrEmpty(item.Icon))
<div @key="item" id="@GetItemId(item)" role="alertdialog" class="@GetItemClassString(item)" data-bb-autohide="@GetAutoHideString(item)" data-bb-delay="@item.Delay">
@if (!string.IsNullOrEmpty(item.Icon))
{
<i class="@item.Icon"></i>
}
<div>
@if (item.ChildContent != null)
{
<i class="@item.Icon"></i>
@item.ChildContent
}
<div>
@if (item.ChildContent != null)
{
@item.ChildContent
}
else
{
@item.Content
}
</div>
@if (item.ShowDismiss)
else
{
<button type="button" class="btn-close" aria-label="close"></button>
@item.Content
}
</div>
}
}
else
{
for (var index = _messages.Count; index > 0; index--)
{
var item = _messages[index - 1];
<div @key="item" id="@GetItemId(item)" role="alertdialog" class="@GetItemClassString(item)" data-bb-autohide="@GetAutoHideString(item)" data-bb-delay="@item.Delay">
@if (!string.IsNullOrEmpty(item.Icon))
{
<i class="@item.Icon"></i>
}
<div>
@if (item.ChildContent != null)
{
@item.ChildContent
}
else
{
@item.Content
}
</div>
@if (item.ShowDismiss)
{
<button type="button" class="btn-close" aria-label="close"></button>
}
</div>
}
@if (item.ShowDismiss)
{
<button type="button" class="btn-close" aria-label="close"></button>
}
</div>
}
</div>

View File

@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
@@ -27,6 +27,10 @@ public partial class Message
private readonly List<MessageOption> _messages = [];
private IEnumerable<MessageOption> MessagesForRender => Placement == Placement.Bottom
? _messages.AsEnumerable().Reverse()
: _messages;
/// <summary>
/// 获得/设置 显示位置 默认为 Top
/// </summary>
@@ -55,7 +59,7 @@ public partial class Message
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(Clear));
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop);
private static string? GetAutoHideString(MessageOption option) => option.IsAutoHide ? "true" : null;
@@ -86,7 +90,7 @@ public partial class Message
}
/// <summary>
/// 设置 Toast 容器位置方法
/// 设置 容器位置方法
/// </summary>
/// <param name="placement"></param>
public void SetPlacement(Placement placement)
@@ -105,8 +109,8 @@ public partial class Message
if (!_messages.Contains(option))
{
_messages.Add(option);
_msgId = GetItemId(option);
}
_msgId = GetItemId(option);
await InvokeAsync(StateHasChanged);
}
@@ -114,11 +118,15 @@ public partial class Message
/// 清除 Message 方法 由 JSInvoke 触发
/// </summary>
[JSInvokable]
public Task Clear()
public void Clear(string id)
{
_messages.Clear();
var option = _messages.Find(i => GetItemId(i) == id);
if (option != null)
{
_messages.Remove(option);
}
StateHasChanged();
return Task.CompletedTask;
}
/// <summary>
@@ -126,12 +134,14 @@ public partial class Message
/// </summary>
/// <param name="id"></param>
[JSInvokable]
public async Task Dismiss(string id)
public async ValueTask Dismiss(string id)
{
var option = _messages.Find(i => GetItemId(i) == id);
if (option is { OnDismiss: not null })
{
await option.OnDismiss();
_messages.Remove(option);
StateHasChanged();
}
}

View File

@@ -1,19 +1,19 @@
import Data from "../../modules/data.js"
import Data from "../../modules/data.js"
import EventHandler from "../../modules/event-handler.js"
export function init(id, invoke, callback) {
export function init(id, invoke) {
const el = document.getElementById(id)
const msg = { el, invoke, callback, items: [] }
const msg = { el, invoke, items: [] }
Data.set(id, msg)
}
export function show(id, msgId) {
const msg = Data.get(id)
const el = document.getElementById(msgId)
if (el === null) {
return
}
const msg = Data.get(id)
let msgItem = msg.items.find(i => i.el.id === msgId)
if (msgItem === void 0) {
msgItem = { el, animationId: null }
@@ -21,7 +21,7 @@ export function show(id, msgId) {
}
if (msgItem.animationId) {
cancelAnimationFrame(msgItem.animationId);
return;
}
const autoHide = el.getAttribute('data-bb-autohide') === 'true';
@@ -46,28 +46,25 @@ export function show(id, msgId) {
el.classList.add('show');
const close = () => {
EventHandler.off(el, 'click')
el.classList.remove('show');
const hideHandler = setTimeout(function () {
clearTimeout(hideHandler);
el.classList.add("d-none");
msg.items.pop();
if (msg.items.length === 0) {
msg.invoke.invokeMethodAsync(msg.callback);
}
}, 500);
msg.items = msg.items.filter(i => i.el.id !== msgId);
msg.invoke.invokeMethodAsync("Clear", msgId);
};
EventHandler.on(el, 'click', '.btn-close', e => {
EventHandler.on(el, 'click', '.btn-close', async e => {
e.preventDefault();
e.stopPropagation();
const alert = e.delegateTarget.closest('.alert');
if (alert) {
EventHandler.off(el, 'click')
alert.classList.add("d-none");
const alertId = alert.getAttribute('id');
msg.invoke.invokeMethodAsync('Dismiss', alertId);
msg.items = msg.items.filter(i => i.el.id !== alertId);
await msg.invoke.invokeMethodAsync('Dismiss', alertId);
}
close();
});
}

View File

@@ -1,9 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Options;
namespace UnitTest.Components;
@@ -57,10 +58,8 @@ public class MessageTest : BootstrapBlazorTestBase
Assert.NotNull(alert.Id);
var message = cut.FindComponent<Message>();
await message.Instance.Dismiss(alert.Id);
await cut.InvokeAsync(() => message.Instance.Dismiss(alert.Id));
Assert.True(dismiss);
await cut.InvokeAsync(() => message.Instance.Clear());
}
[Fact]
@@ -98,7 +97,7 @@ public class MessageTest : BootstrapBlazorTestBase
await cut.Instance.Dismiss(alert.Id);
await cut.Instance.Dismiss("test_id");
await cut.InvokeAsync(() => cut.Instance.Clear());
await cut.InvokeAsync(() => cut.Instance.Clear(alert.Id));
await cut.InvokeAsync(() => service.Show(new MessageOption()
{
@@ -133,4 +132,44 @@ public class MessageTest : BootstrapBlazorTestBase
ShowMode = MessageShowMode.Single
}, cut.Instance));
}
[Fact]
public async Task ForceDelay_Ok()
{
var service = Context.Services.GetRequiredService<MessageService>();
var cut = Context.RenderComponent<Message>();
var option = new MessageOption()
{
Content = "Test Content",
IsAutoHide = false,
ShowDismiss = true,
Icon = "fa-solid fa-font-awesome",
ForceDelay = true,
Delay = 2000
};
await cut.InvokeAsync(() => service.Show(option, cut.Instance));
Assert.Contains("data-bb-delay=\"2000\"", cut.Markup);
var alert = cut.Find(".alert");
Assert.NotNull(alert);
Assert.NotNull(alert.Id);
await cut.InvokeAsync(() => cut.Instance.Clear(alert.Id));
option.ForceDelay = false;
await cut.InvokeAsync(() => service.Show(option, cut.Instance));
Assert.Contains("data-bb-delay=\"4000\"", cut.Markup);
await cut.InvokeAsync(() => cut.Instance.Clear(alert.Id));
// 更新 Options 值
var options = Context.Services.GetRequiredService<IOptionsMonitor<BootstrapBlazorOptions>>();
options.CurrentValue.MessageDelay = 1000;
await cut.InvokeAsync(() => service.Show(option, cut.Instance));
Assert.Contains("data-bb-delay=\"1000\"", cut.Markup);
await cut.InvokeAsync(() => cut.Instance.Clear(alert.Id));
options.CurrentValue.MessageDelay = 0;
await cut.InvokeAsync(() => service.Show(option, cut.Instance));
Assert.Contains("data-bb-delay=\"1000\"", cut.Markup);
await cut.InvokeAsync(() => cut.Instance.Clear(alert.Id));
}
}