Replies: 6 comments
-
|
@shakugans Thank you for reporting. We will give triage later. |
Beta Was this translation helpful? Give feedback.
-
|
@shakugans 简单看了下你的代码,作者可能太忙,最近都没咋回复,这是我最近学习整理的资料,建议也看一下,提交组件的话,建议保持代码风格与作者的 文档由本人认真和AI互动讨论产生,我个人认为没有什么大的问题,限于水平有限,仅供参考。 BootstrapBlazor 组件 JavaScript 与 Blazor 状态同步机制
目录
1. 核心设计原则1.1 单向数据流 + 回调通知BootstrapBlazor 采用 "单向数据流 + 回调通知" 的设计模式来避免 Server/WASM diff 时的状态错乱问题: 1.2 职责分离
1.3 核心原则总结2. Blazor Diff 机制详解2.1 Blazor Diff 管理的内容2.2 属性类型对比表
2.3 JS 添加的 class 处理情况 1:模板中的 class 没变化 → 保留@* C# 模板 *@
<div class="drawer collapse"> @* 始终是这个值 *@// JS 添加 show 类
element.classList.add('show');
// DOM 变成: class="drawer collapse show"情况 2:模板中的 class 变化了 → 被覆盖@* C# 模板 - IsActive 从 false 变 true *@
<div class="@(IsActive ? "drawer active" : "drawer")">2.4 渲染树结构// C# 模板
<div id="@Id" class="@ClassString" style="@StyleString">
@foreach (var item in Items)
{
<span>@item.Name</span>
}
</div>Blazor 生成的渲染树: Diff 时只比较这个树的变化,不关心 DOM 的实际运行时状态。 3. JavaScript 模块架构3.1 核心模块3.2 Data 模块 - 独立状态存储// src/BootstrapBlazor/wwwroot/modules/data.js
const elementMap = new Map()
export default {
set(element, instance) {
if (!elementMap.has(element)) {
elementMap.set(element, instance)
}
},
get(element) {
if (elementMap.has(element)) {
return elementMap.get(element)
}
return null
},
remove(element) {
if (!elementMap.has(element)) {
return
}
elementMap.delete(element)
}
}关键点:JavaScript 状态存储在独立的 3.3 EventHandler 模块 - 事件委托// 事件委托模式
EventHandler.on(el, 'click', '.dropdown-item', e => {
// el 是父容器(稳定)
// '.dropdown-item' 是选择器(子元素可动态变化)
const item = e.delegateTarget;
// 处理逻辑...
});3.4 组件 JS 文件标准结构// ComponentName.razor.js
import Data from "../../modules/data.js"
import EventHandler from "../../modules/event-handler.js"
// 初始化
export function init(id, invoke, options) {
const el = document.getElementById(id)
if (el === null) return
const component = { el, invoke, options }
Data.set(id, component)
// 绑定事件(使用事件委托)
EventHandler.on(el, 'click', '.child-selector', handler)
}
// 更新(可选)
export function update(id, newState) {
const component = Data.get(id)
if (component) {
// 更新逻辑
}
}
// 销毁
export function dispose(id) {
const component = Data.get(id)
Data.remove(id)
if (component) {
EventHandler.off(component.el, 'click')
}
}4. C# 与 JavaScript 双向通信4.1 基类封装// src/BootstrapBlazor/Components/BaseComponents/BootstrapModuleComponentBase.cs
public abstract class BootstrapModuleComponentBase : IdComponentBase, IAsyncDisposable
{
// JS 模块引用
protected JSModule? Module { get; set; }
// .NET 对象引用(供 JS 回调)
protected DotNetObjectReference<BootstrapModuleComponentBase>? Interop { get; set; }
// 首次渲染后加载 JS 模块
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !string.IsNullOrEmpty(ModulePath))
{
Module ??= await JSRuntime.LoadModule(ModulePath);
if (AutoInvokeInit)
{
await InvokeInitAsync();
}
}
}
// 调用 JS init 方法
protected virtual Task InvokeInitAsync()
=> InvokeVoidAsync("init", Id, Interop);
// 销毁时清理
protected virtual async ValueTask DisposeAsync(bool disposing)
{
if (disposing)
{
Interop?.Dispose();
if (Module != null)
{
if (AutoInvokeDispose)
{
await Module.InvokeVoidAsync("dispose", Id);
}
await Module.DisposeAsync();
}
}
}
}4.2 C# → JavaScript// 调用 JS 方法
await InvokeVoidAsync("execute", Id, IsOpen);
// 调用 JS 方法并获取返回值
var result = await InvokeAsync<bool>("checkState", Id);4.3 JavaScript → C#// C# 端标记回调方法
[JSInvokable]
public async Task OnStateChangedAsync(CheckboxState state)
{
State = state; // 更新 C# 状态
StateHasChanged(); // 触发重新渲染
}// JavaScript 端调用
await invoke.invokeMethodAsync('OnStateChangedAsync', newState);4.4 初始化时传递回调方法名// C# 端
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new
{
ConfirmMethodCallback = nameof(ConfirmSelectedItem),
SearchMethodCallback = nameof(TriggerOnSearch),
TriggerCollapsed = nameof(TriggerCollapsed)
});
[JSInvokable]
public async Task ConfirmSelectedItem(int index)
{
// 处理确认选中
}
[JSInvokable]
public async Task TriggerOnSearch(string searchText)
{
SearchText = searchText;
StateHasChanged();
}// JavaScript 端
export function init(id, invoke, options) {
const { confirmMethodCallback, searchMethodCallback } = options;
EventHandler.on(el, 'keydown', e => {
if (e.key === 'Enter') {
invoke.invokeMethodAsync(confirmMethodCallback, index);
}
});
}5. 事件监听器与 Blazor Diff5.1 核心结论5.2 三种情况分析情况 1:只修改属性 → 监听器保留 ✅@* 初始渲染 *@
<div id="box" class="normal">Content</div>
@* 状态变化后 *@
<div id="box" class="active">Content</div>元素 情况 2:元素被删除重建 → 监听器丢失 ❌@if (ShowBox)
{
<div id="box">Content</div>
}
情况 3:列表重排 → 取决于 @key@* 使用 @key 时,Blazor 识别为"移动元素",可能删除重建 *@
@foreach (var item in Items)
{
<div @key="item.Id">@item.Name</div>
}5.3 解决方案:事件委托// ✅ 推荐:绑定到稳定的父容器
EventHandler.on(container, 'click', '.item', handler);
// ❌ 避免:直接绑定到可能变化的子元素
item.addEventListener('click', handler);5.4 保持容器稳定的模板设计@* 外层容器始终存在,ID 稳定 *@
<div id="@Id" class="select">
@* 内部内容可以动态变化 *@
<div class="dropdown-menu">
@foreach (var item in FilteredItems)
{
<div class="dropdown-item">@item.Text</div>
}
</div>
</div>6. 组件生命周期管理6.1 完整生命周期流程6.2 时序图7. 实战案例分析7.1 案例:Drawer 抽屉组件文件结构C# 状态定义// 核心状态:抽屉是否打开
[Parameter]
public bool IsOpen { get; set; }
// 双向绑定回调
[Parameter]
public EventCallback<bool> IsOpenChanged { get; set; }Razor 模板@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]
<div id="@Id" class="@ClassString"
data-bb-keyboard="@KeyboardString"
data-bb-scroll="@BodyScrollString">
@if (ShowBackdrop)
{
<div class="drawer-backdrop"></div>
}
<div class="@DrawerClassString">
@ChildContent
</div>
</div>JavaScript 模块export function init(id, invoke, method) {
const el = document.getElementById(id);
if (el === null) return;
const dw = { el, body: document.querySelector('body') }
Data.set(id, dw)
// 监听 ESC 键
EventHandler.on(el, 'keyup', async e => {
if (e.key === 'Escape') {
const supportESC = el.getAttribute('data-bb-keyboard') === 'true';
if (supportESC) {
// 通知 C# 关闭
await invoke.invokeMethodAsync(method);
}
}
});
}
export function execute(id, open) {
const dw = Data.get(id)
const { el } = dw
const drawerBody = el.querySelector('.drawer-body')
if (open) {
el.classList.add('show')
drawerBody.classList.add('show'); // JS 添加动画类
} else {
drawerBody.classList.remove('show');
// 动画完成后移除
requestAnimationFrame(() => {
el.classList.remove('show');
});
}
}
export function dispose(id) {
const dw = Data.get(id)
Data.remove(id);
EventHandler.off(dw.el, 'keyup');
}状态同步流程7.2 案例:Select 下拉选择组件键盘导航的临时状态// 键盘 ↑↓ 导航时,JS 添加临时 active 类
const keydown = e => {
if (e.key === 'ArrowDown') {
// 移除旧的 active
current.classList.remove('active');
// 添加新的 active(临时视觉效果)
nextItem.classList.add('active');
scrollIntoView(nextItem);
}
if (e.key === 'Enter') {
// 确认选中,通知 C#
invoke.invokeMethodAsync('ConfirmSelectedItem', index);
}
}// C# 计算 active 类(最终状态)
private string? ActiveItem(SelectedItem item) => CssBuilder.Default("dropdown-item")
.AddClass("active", item.Value == CurrentValueAsString) // C# 决定
.Build();
[JSInvokable]
public async Task ConfirmSelectedItem(int index)
{
SelectedItem = Rows[index];
CurrentValueAsString = SelectedItem.Value;
StateHasChanged(); // 重新渲染,C# 的 active 覆盖 JS 的临时状态
}搜索防抖const onSearch = debounce(async v => {
await invoke.invokeMethodAsync('TriggerOnSearch', v);
}, 200);
Input.composition(search, onSearch); // 支持中文输入法[JSInvokable]
public async Task TriggerOnSearch(string searchText)
{
SearchText = searchText;
StateHasChanged(); // 重新渲染过滤后的列表
}8. 最佳实践总结8.1 状态管理8.2 事件处理8.3 CSS 类管理8.4 配置传递8.5 错误处理// JSModule 中的错误处理
public virtual async ValueTask InvokeVoidAsync(string identifier, ...)
{
try
{
await jSObjectReference.InvokeVoidAsync(identifier, ...);
}
catch (JSException)
{
#if DEBUG
throw; // 开发时抛出
#endif
}
catch (JSDisconnectedException) { } // 连接断开,忽略
catch (OperationCanceledException) { } // 操作取消,忽略
catch (ObjectDisposedException) { } // 对象已销毁,忽略
}附录A. 常用工具函数// 防抖
const debounce = (fn, duration = 200) => {
let handler = null;
return function() {
if (handler) clearTimeout(handler);
handler = setTimeout(() => fn.apply(this, arguments), duration);
}
}
// 滚动到可见区域
const scrollIntoView = (el, item) => {
const behavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';
item.scrollIntoView({ behavior, block: "nearest" });
}
// 获取元素索引
const indexOf = (container, element) => {
const items = container.querySelectorAll('.dropdown-item');
return Array.prototype.indexOf.call(items, element);
}B. 组件属性命名约定
C. 相关文件位置
|
Beta Was this translation helpful? Give feedback.
-
|
@ice6 这总结的真好啊,花了不少时间吧,也都非常准确与正确 |
Beta Was this translation helpful? Give feedback.
-
|
@shakugans 工程打开都是乱码,能否重新整理一下文档编码格式 |
Beta Was this translation helpful? Give feedback.
-
是呢,重仓 |
Beta Was this translation helpful? Give feedback.
-
重新编辑了
我没什么经验。能大佬搞就最好了 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
一个是区域拖拽,一个是轮盘展开,组合起来使用感觉效果还可以
https://gitee.com/qq_connect-623772954/blazor_-area-drag_-roulette
Beta Was this translation helpful? Give feedback.
All reactions