十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Blazor组件式开发实战:从组件拆分到状态管理完整指南

Blazor组件式开发实战:从组件拆分到状态管理完整指南 简介本资源是一套基于.NET 6.0与Blazor Server模式的组件化开发实战案例面向C# Web开发者及Blazor初学者聚焦数据库交互场景下的代码复用与架构优化问题。项目采用VS2022开发后端集成SQL Server 2012及以上版本通过Entity Framework Core实现数据访问完整覆盖增删改查功能并将CRUD操作统一封装至同一可复用Blazor组件中显著降低冗余代码量与组件维护成本。压缩包共169个文件含20个核心C#业务逻辑文件、11个Razor组件页、39个运行时DLL、17个CSS样式资源及配套JSON配置、SQL脚本与说明文档等整体体积5.1MB结构清晰便于快速上手。已有517人学习下载解压后“说明”文件夹提供运行效果截图、组件调用关系图及关键代码注释帮助读者深入理解Blazor组件通信、参数绑定与EF上下文生命周期管理等核心实践要点。 .NET 生态这几年最让我觉得“值回票价”的一件事就是 Blazor 的出现。它让写惯 C# 的人终于不用再捏着鼻子去碰 JavaScript 那一大摊工具链直接在浏览器里用组件化的方式把前端页面搭起来。这篇文章我想围绕“Blazor 组件式开发”这个主题从一个实战项目的角度把组件拆分、组件通信、生命周期、路由布局、依赖注入、状态管理这些核心环节完整过一遍。内容不追求大而全而是聚焦到“怎么设计一个能真正跑起来、能维护、能扩展的 Blazor 组件系统”这件事上适合已经入门 C# 和 Asp.net、想往 Blazor 深入一步的开发者参考。1. 为什么说组件式开发是 Blazor 的灵魂而不只是普通页面先聊点理念层面的东西。很多人刚接触 Blazor 时会把它当成“用 C# 写网页”觉得 Razor 语法跟 Razor Pages 差不多套个模板、绑个数据就完事了。这个理解方向没错但会错过 Blazor 最值钱的那部分——它的组件模型。Blazor 里的组件本质是一个继承了ComponentBase的类加上一个.razor视图文件。每个组件拥有自己的状态、自己的渲染逻辑、自己的生命周期。更重要的是组件之间通过参数和回调协作就像拼积木一样一个积木块的内部怎么实现完全不影响另一个积木块只要接口稳定就行。我最早用 Blazor 做项目时犯过一个典型错误把所有逻辑都堆在一个大页面里一个 .razor 文件写了两千多行页面上同时存在表单、表格、图表、弹窗。结果就是改一处绑定时变量名时需要全局搜索替换生怕漏掉一个地方导致编译报错。后来按组件化思路重构把表单拆出来、表格拆出来、弹窗拆出来每个组件单独维护页面文件只剩几行组件引用和布局代码。那种“牵一发而动全身”的焦虑感瞬间消失了。组件式开发的真正价值不只是代码好看了而是把复用和隔离变成了默认行为。你在项目 A 里写的分页组件稍微调几个参数就能用到项目 B 里你改分页组件的内部实现只要对外接口不变所有引用了它的页面都不需要动。这种开发体验才是 Blazor 对比传统 Web Forms 和早期 MVC 时代最大的进步。2. 环境准备与第一个组件的完整落地过程说干就干。先准备好开发环境这部分其实没什么坑但有一个容易忽略的细节值得提一下。2.1 开发环境搭建我用的是 .NET 8这已经是 LTS 版本各方面都比较成熟。到 dotnet 官网下载 SDK 安装包一路下一步就行。装完在终端里执行dotnet --version确认一下版本号。然后是 IDE。Windows 上用 Visual Studio 2022 体验最好社区版免费创建项目时直接有 Blazor Web App 模板。如果你在 Mac 或 Linux 上用 JetBrains Rider 也很顺手。至于 VS Code配合 C# Dev Kit 扩展也能开发但调试体验没有前两者流畅我个人建议能上 IDE 就上 IDE。2.2 创建项目时要注意的模板差异创建项目这一步要特别留意.NET 8 的 Blazor 模板不再是以前那种单独的 Blazor Server 或 Blazor WebAssembly 模板供你直接选而是统一成了一个Blazor Web App模板。创建完之后你需要在项目文件里指定渲染模式有这几种选择Server默认模式在服务器端运行通过 SignalR 实时通信WebAssembly编译后下载到浏览器运行Auto先以 Server 模式交互后续访问切换为 WebAssembly实操建议如果你做的是企业级内部系统、工业控制后台这类需要访问内网数据库、调用 PLC 或者串口的应用直接选 Server 模式最省心。因为信号是实时的逻辑在服务端跟本机硬件交互非常顺畅。纯 WebAssembly 模式虽然部署灵活但首次加载时间和浏览器兼容性都是额外的成本。Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework Nullableenable/Nullable ImplicitUsingsenable/ImplicitUsings /PropertyGroup /Project2.3 手写第一个业务组件为了演示组件式开发的核心套路我直接手写一个简单的“设备状态卡片”组件——就是你在工业监控系统里经常看到的那个小卡片设备名称、运行状态、当前温度、启动/停止按钮。代码不长但组件化的几个关键点都覆盖到了。* DeviceCard.razor * div classdevice-card (IsRunning ? running : stopped) h3DeviceName/h3 p状态(IsRunning ? 运行中 : 已停止)/p p当前温度Temperature ℃/p button onclickToggleStatus(IsRunning ? 停止 : 启动)/button /div code { [Parameter] public string DeviceName { get; set; } string.Empty; [Parameter] public double Temperature { get; set; } private bool IsRunning { get; set; } private void ToggleStatus() { IsRunning !IsRunning; // 这里可以扩展为调用后台 API 或 PLC 指令 } }在页面里使用这个组件只需要这样DeviceCard DeviceName1号注塑机 Temperature63.5 / DeviceCard DeviceName2号贴片机 Temperature42.1 /看到没每台设备对应一个组件实例互不干扰。设备 1 的状态切换不会影响设备 2。这个就是组件隔离性的直观体现。3. 组件通信的几种模型数据绑定、事件回调、级联参数组件拆出来之后通信就成了核心问题。父子组件之间怎么传数据子组件怎么通知父组件“我这边被点击了”跨多层组件怎么共享数据这节我从实际使用频率出发逐一拆解。3.1 父传子参数绑定与单向数据流父组件往子组件传数据标准姿势是[Parameter]上面已经演示过。但这里有个很重要的设计原则单向数据流。父组件通过参数把数据交给子组件子组件不应该反过来直接修改这个参数的值否则会产生不可预估的渲染混乱。如果子组件需要维护一个基于参数内部的副本就把它赋值到自己的私有字段里而不是直接修改参数本身。* ChildComponent.razor * code { [Parameter] public int Count { get; set; } private int localCount; protected override void OnParametersSet() { localCount Count; } }OnParametersSet是 Blazor 生命周期里一个很重要的节点每次父组件重新渲染、参数发生变化时都会触发这个方法。你在这里同步本地副本就能保证子组件始终拿到最新的外部数据。3.2 子传父EventCallback 的正确用法子组件要跟父组件通信标准答案是EventCallback。看着有点绕但理解之后其实很直白——你给子组件传一个“回调函数”子组件在合适的时机调用它就像你给朋友留了个电话号码让他有事打给你。* DeviceCard.razor * button onclickTriggerStatusChanged点击/button code { [Parameter] public EventCallbackbool StatusChanged { get; set; } private async Task TriggerStatusChanged() { await StatusChanged.InvokeAsync(true); } }父组件使用* ParentPage.razor * DeviceCard StatusChangedOnDeviceStatusChanged / code { private void OnDeviceStatusChanged(bool isRunning) { Console.WriteLine($设备状态变化{isRunning}); // 这里可以做全局记录、统计等逻辑 } }有几个细节经验值得记一下EventCallback是异步的调用时用await否则某些情况下可能出现异常。回调方法里尽量不要写太重的逻辑——它是在 UI 线程上执行的会影响渲染。如果你需要传多个值不要硬拆成多个回调。定义一个事件参数类比如DeviceStatusChangedEventArgs既清晰又便于扩展。3.3 跨多层组件共享级联参数业务系统里很常见的一种需求是外层布局组件拿到用户登录信息内层某个按钮组件也要用这个信息来决定显示还是隐藏。如果把用户信息一层一层传下去每个中间组件都要多定义一遍参数麻烦且容易漏。Blazor 提供了级联参数Cascading Parameter解决这个问题。在顶层组件包裹一个CascadingValue内部所有层级的组件都能直接拿到这个值。* MainLayout.razor * CascadingValue NameUserContext ValuecurrentUser Body /CascadingValue code { private UserInfo currentUser new() { Name 张三, Role Admin }; }内层任意组件* ToolbarButton.razor * code { [CascadingParameter(Name UserContext)] private UserInfo? User { get; set; } }我做过一个多租户项目租户信息就是用级联参数传递的。页面任何角落需要展示租户名称、根据租户权限控制菜单直接拿参数判断完全不用中间组件帮忙“传话”。这个功能只要用上就会上瘾。3.4 组件通信方式选型接触的初级开发者多了经常会被问到“到底该用哪种通信”。我整理了一个速查表方便大家直接对照选型通信场景推荐方案适用说明父组件 → 子组件[Parameter]绑定最常见的单向数据流子组件 → 父组件EventCallbackT通过回调通知外部事件跨多层共享级联参数避免逐层传递的繁琐同层级或跨页面状态容器 事件需要共享状态或触发刷新全局跨组件业务事件AppStateCascadingValue类似全局状态管理4. 组件生命周期与渲染链路理解组件如何工作Blazor 组件的生命周期听起来很理论但它是排查各种 bug 的基础。很多莫名其妙的“页面没刷新”“数据没加载”“回调被调了两次”问题最后追根溯源都是生命周期没理解透。4.1 生命周期执行顺序一个 Blazor 组件从初始化到销毁会依次执行这些方法SetParametersAsync设置参数OnInitialized/OnInitializedAsync组件初始化只执行一次OnParametersSet/OnParametersSetAsync参数设置完毕父组件每次渲染时都会触发OnAfterRender/OnAfterRenderAsync组件渲染完成后可以操作 DOMDispose组件销毁时释放资源关键理解在于OnInitialized只在组件首次创建时执行而OnParametersSet只要父组件重新渲染就会执行。这就是为什么你的初始化代码如果写在OnParametersSet里可能被重复调用多次。4.2 一个常见陷阱初始化数据和页面刷新来看一个真实踩坑场景。我写过一组件的职责是加载设备列表代码如下protected override async Task OnInitializedAsync() { await LoadDevices(); }第一次打开页面数据正常加载。但当你从另一个页面导航到这个页面时数据没有刷新——还是旧数据。原因就在于组件可能被 Blazor 复用了特别是路由切换时OnInitializedAsync不会被再次调用。解决办法是用OnParametersSetAsync来判断参数是否变化如果有变化就重新加载。或者更直接一点很多场景下服务端的真实数据源应该通过 SignalR 推送或查询实现实时刷新而不仅是靠生命周期钩子触发一次。4.3 手动控制渲染StateHasChanged 的进阶操作StateHasChanged是最常用的渲染触发方法。通常你在事件处理器中修改了字段值Blazor 会自动帮你调用它。某些场景下不会自动触发比如在定时器回调中修改字段在事件订阅回调中修改字段在异步后台任务完成后修改字段我写上位机程序时经常需要定时刷新监控数据代码类似这样private System.Timers.Timer _timer; protected override void OnInitialized() { _timer new System.Timers.Timer(1000); _timer.Elapsed (_, _) { currentTemp ReadTemperature(); InvokeAsync(StateHasChanged); }; _timer.Start(); }注意这里用了InvokeAsync(StateHasChanged)而不是直接调用StateHasChanged()。原因在于定时器的回调线程不是 UI 线程Blazor 的组件实例不允许跨线程直接操作渲染状态。InvokeAsync会把委托投递到 UI 线程上执行这才是安全的做法。5. 路由、布局与动态组件让多个组件协同组织成页面组件拆好之后下一步就是把它们组织成完整的页面和应用导航结构。Blazor 的路由和布局体系设计得相当清爽但如果没摸清机制也会碰到一些隐蔽的问题。5.1 路由定义与页面集成在 Blazor 中页面本质上也是一种特殊组件——它有一个page /path指令。比如你建一个Pages/DeviceList.razor顶部声明page /devices访问/devices时这个组件就会被渲染到导航结构中。路由表是在Routes.razor中通过Router组件统一管理的它会自动扫描全部带page指令的组件不需要手工注册。5.2 布局的组件化思维布局系统本身也是组件这个设计我非常喜欢。MainLayout.razor本质上就是一个带Body的普通组件你可以把页头、菜单、底部信息全部做成小组件然后在布局里组合。* MainLayout.razor * div classapp-layout HeaderBar / SideMenu / main Body /main FooterInfo / /div这样菜单组件和页头组件可以独立维护。哪个菜单需要根据用户权限显示隐藏改SideMenu内部逻辑就行其他页面和布局完全不用动。5.3 动态组件场景化组合的利器有些页面的内容区域是根据用户选择的模块动态变化的。典型场景是一个主页面左侧是业务列表右侧是根据选中项展示对应详情。详情形态不同可能是设备参数、可能是订单汇总、可能是图表。这时候用一个DynamicComponent会很方便div classdetail-panel DynamicComponent TypedetailComponentType ParametersdetailParameters / /divdetailComponentType是一个Type对象根据当前选中项动态赋值。我在一个工单管理项目里用到过这个模式工单类型从 3 种涨到 10 种前端页面代码一行都没加——每种工单类型就是一个独立组件后台注册一下类型映射即可。组件化开发到这个程度系统的扩展性和维护性就开始体现出来了单个页面不是“写死”的而是由数据驱动、动态组装出来的。6. 状态管理与数据持久化组件之外的数据层设计组件是有边界的但业务数据往往需要跨越边界流动。用过 React 的人对状态管理工具一定不陌生Blazor 这边虽然生态没那么花哨但常见的状态管理思路是清晰且实用的。6.1 服务端状态与客户端状态的边界首先要想清楚一个核心问题哪些状态属于组件内部哪些属于全局我的判断依据很简单——如果这个状态只有组件自己在意就留在组件内部如果多个组件都需要感知或修改它就提升到全局。比如“当前页面是否需要显示加载动画”这个词属于页面内部搞全局状态纯粹浪费性能。而“当前登录用户”“当前选中项目”整个应用各处都要用必须提升为全局状态。6.2 用 AppState 实现全局状态管理Blazor 中实现全局状态的常规做法是创建一个普通类通过依赖注入注册为单例作用域再配合事件通知让组件响应变化。public class AppState { public event Action? OnChange; public string CurrentProject { get; private set; } string.Empty; public void SetCurrentProject(string projectName) { CurrentProject projectName; OnChange?.Invoke(); } }在组件中订阅protected override void OnInitialized() { appState.OnChange OnAppStateChanged; } private void OnAppStateChanged() { InvokeAsync(StateHasChanged); } public void Dispose() { appState.OnChange - OnAppStateChanged; }6.3 防内存泄漏事件订阅记得取消上面这段代码有个关键点所有事件订阅都必须在Dispose中取消。如果漏掉了做过多的订阅/不注销组件被销毁后事件源仍然持有着旧组件的引用就会造成内存泄漏。长期运行的服务端应用这种情况特别危险——内存只涨不降最终程序卡死。这个坑我踩过一次当时排查了很久才发现是某个页面没有反订阅全局事件。教训是能用Dispose的地方不要偷懒该实现 IDisposable 就实现。6.4 与后端数据的持久化交互状态管理只是内存中的事真正的数据持久化还是得靠后端。Blazor Server 端直接注入HttpClient或者更常见的DbContextEF Core都不难。比较方便的方式是定义一个服务层接口组件只与接口打交道具体是读数据库还是调外部 API对组件不可见。public interface IDeviceService { TaskListDeviceInfo GetDevicesAsync(); Taskbool StartDeviceAsync(string deviceId); } public class DeviceService : IDeviceService { private readonly HttpClient _http; public DeviceService(HttpClient http) { _http http; } public async TaskListDeviceInfo GetDevicesAsync() { return await _http.GetFromJsonAsyncListDeviceInfo(api/devices); } }在 Program.cs 里注册builder.Services.AddScopedIDeviceService, DeviceService();组件中使用inject IDeviceService DevService code { private ListDeviceInfo devices new(); protected override async Task OnInitializedAsync() { devices await DevService.GetDevicesAsync(); } }这样做的价值在于如果以后业务从单机版改为分布式部署设备数据的获取从直接访问数据库变成调用远程 API只需要改DeviceService的实现组件代码完全不用动。这就是依赖注入 接口抽象带来的解耦效果。7. JS互操作与三方库集成Blazor 的边界外延必须承认Blazor 生态在某些领域仍然依赖 JavaScript 生态。地图、富文本编辑器、图表库、打印机控制、摄像头监控这些场景Blazor 原生支持还不够全面需要通过 JS 互操作JSInterop调用现有 JS 库。这部分是实战中绕不开的也是很多人觉得麻烦的地方但其实掌握套路之后并不神秘。7.1 基本用法C# 调 JS最常见的场景是 C# 代码里需要调用一段 JS 方法。典型流程是先在wwwroot下放一个 JS 文件然后在组件里注入IJSRuntime通过InvokeAsync调用。// wwwroot/js/app.js window.blazorHelper { showAlert: function (message) { alert(message); }, getWindowSize: function () { return { width: window.innerWidth, height: window.innerHeight }; } };inject IJSRuntime JS code { private async Task ShowAlert() { await JS.InvokeVoidAsync(blazorHelper.showAlert, 设备已启动); } private async Task GetSize() { var size await JS.InvokeAsyncWindowSize(blazorHelper.getWindowSize); Console.WriteLine($宽{size.width}高{size.height}); } private class WindowSize { public int width { get; set; } public int height { get; set; } } }7.2 高级注意点JS互操作的调用时机有个细节很多人不知道IJSRuntime.InvokeAsync在组件首次渲染之前调用是会抛异常的。因为此时浏览器端的 JS 环境还没准备好。如果你需要在组件初始化时执行 JS应该放在OnAfterRenderAsync里protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync(initChart); } }7.3 组件封装把 JS 库包装成可复用的 Blazor 组件单独调用 JS 是小事真正的价值在于把某个 JS 库封装成一个 Blazor 组件让上层调用者完全感受不到 JS 的存在。举一个我封装 ECharts 图表的简化例子。* EChart.razor * div refchartContainer stylewidth:100%;height:400px;/div code { private ElementReference chartContainer; [Parameter] public object ChartOption { get; set; } new(); [Parameter] public EventCallbackstring OnChartClick { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync(initEChart, chartContainer); } await JS.InvokeVoidAsync(setEChartOption, chartContainer, ChartOption); } public async Task RefreshChart() { await JS.InvokeVoidAsync(setEChartOption, chartContainer, ChartOption); } }然后调用者可以这样使用EChart ChartOptionGetLineOption() /这个模式非常适合团队协作负责 JSTS 的同事专心维护 JS 封装C# 开发只管传配置对象互不干扰。这也是组件化思想的延伸——JS 边界也被封装成一个组件。8. 依赖注入与性能调优组件系统跑得稳的基础组件系统写出来容易要在真实业务里跑得稳、跑得快依赖注入作用域理解和性能优化是必须过的两道关。8.1 作用域选择AddSingleton 还是 AddScoped做过 ASP.NET Core 的开发者对依赖注入一定不陌生Blazor 中基本一致但作用域语义稍有不同AddSingleton全局单例。在 Blazor Server 模式中意味着所有用户共享一个实例适合存放全局配置、静态数据等无状态服务。AddScoped在 Blazor Server 中作用域是“一个用户连接”。每个用户建立 SignalR 连接后拿到的都是同一实例不同用户之间互相隔离。这是最常见的服务注册方式。容易犯的错误是把有状态的、按用户隔离的数据服务注册成 Singleton导致用户 A 的修改影响用户 B 的数据。排查的时候画面会很痛苦。8.2 渲染模式与性能优化思路Blazor Server 的性能瓶颈通常出现在网络往返和服务器渲染压力上。优化思路有几条都是我实测有效的第一减少不必要的组件渲染。如果某个组件的参数没有变化可以重写ShouldRender方法返回 false跳过渲染。protected override bool ShouldRender() { return !_isLoading; }第二避免在OnInitializedAsync里做过多阻塞等待。如果多个数据源没有依赖关系用Task.WhenAll并行加载而不是逐条 await。var devicesTask service.GetDevicesAsync(); var usersTask service.GetUsersAsync(); await Task.WhenAll(devicesTask, usersTask);第三大数据列表使用虚拟化。Blazor 内置的Virtualize组件可以只渲染可视区域内的行对于几百上千条数据的表格非常有效。Virtualize Itemsdevices Contextdevice DeviceCard DeviceNamedevice.Name Temperaturedevice.Temp / /Virtualize8.3 一个容易忽略的坑SignalR 连接与状态保持Blazor Server 依赖 SignalR 维持前端与后端的连接。如果用户打开页面后长时间不操作连接可能被网关断开导致页面出现“掉线重连”的提示。优化手段包括配置合理的KeepAliveInterval时间、在客户端appsettings.json里设置重连策略、对重要数据做持久化缓存避免重连后状态丢失。这个坑在办公系统里尤其常见——用户开着页面去吃午饭回来发现页面断了数据还是半小时前的。后来我在布局组件中加入了连接状态监听并自动重新加载最新数据体验才好转。9. 三种典型应用场景的组件化设计示例理论讲了不少这节我用三个真实业务场景来展示组件化设计的具体落地。每个场景我都给出组件拆分的思路因为这是最容易犯糊涂的地方。9.1 工业监控看板工业场景的数据监控关键词里提到的 PLC、串口、摄像头、打印机监控等跟 Blazor Server 是绝配。看板页面的组件拆法通常是DeviceStatusPanel设备状态总览卡片集合RealtimeChart实时趋势图封装 EChartsAlarmList告警列表CameraViewer摄像头画面封装视频流每个组件负责一块独立的数据展示通过AppState共享最新采集值。设备数据通过后台服务从 PLC 或数据库获取推送到AppState所有订阅组件同步刷新。这种模式下新增一种设备类型只需要新增一个卡片组件看板主页面代码完全不变。9.2 后台管理系统通用列表页后台管理的列表页形态高度类似顶部搜索栏、中间表格、底部分页、右侧详情抽屉。组件化设计后这套东西可以抽象成通用组件SearchBar接收搜索字段配置输出搜索条件DataTable接收表格列配置和数据源输出排序、选中、分页的行为Pagination接收页码信息输出翻页事件DetailDrawer接收实体 ID异步加载详情展示这样定义一个通用配置对象每个列表页就变成“写配置”而不是“写代码”去博客看到这个套路时我自己实践过一次后来做十几个管理页面每个页面代码量大幅减少而且修复表格排序的 bug 只需要改一个组件。9.3 表单驱动的数据采集页面做工业软件和上位机的人经常会碰到“现场参数配置”这类页面。配置项多、类型杂字符串、数值、枚举、布尔、校验规则不一。组件化的思路是把表单拆成一组字段级组件TextField文本输入NumberField数值输入带单位EnumSelectField下拉选择SwitchField布尔切换FormGroup字段分组与布局ValidationSummary校验汇总每个字段组件接收字段元数据名称、类型、默认值、校验规则和当前值输出变更事件。表单页面只负责组织字段列表不用关心每个字段具体怎么渲染、怎么校验。这样配置项的增删改只需要改后端返回的字段定义前端组件零修改。10. 我踩过的几个值得公开的坑与对应解法最后这部分把我在 Blazor 实战中踩过、并且花了不少时间才解决的几个坑分享出来希望能帮你省下一些排查时间。10.1 无法加载一个或多个请求的类型项目运行时偶尔会抛FileLoadException或TypeLoadException提示“无法加载一个或多个请求的类型”。我的排查过程大致是这样的先把异常信息里的类型名记下来然后去bin/Debug输出目录检查对应的 DLL 是否存在。大多数时候问题出在版本不一致——比如一个类库引用了Newtonsoft.Json的 12.0.0而主项目强制绑定 13.0.0运行时加载不到期望版本就会抛这种错。解法是直接在csproj里统一版本或者在 NuGet 包管理器里把所有项目的同名包改成相同版本。遇到这种报错先不要想复杂90% 是版本冲突。10.2 EventCallback 回调一次被触发两次我曾在界面上点击一次按钮后台日志却显示事件回调执行了两次。排查过程如下第一反应是按钮事件重复绑定检查代码没问题。然后跟踪链路发现父组件在渲染时把子组件实例复制了两份——因为我在foreach循环里没用key导致 Blazor 复用组件实例时出现问题。加上key之后问题解决。所以经验是动态渲染列表组件时务必给每个元素设置稳定唯一的key让 Blazor 的 diff 算法能够准确识别实例。10.3 打印机状态监控的场景有段时间项目需要实时监控 Windows 打印机状态这也是热词里出现过的场景。要点是后台服务用 WMI 或者System.Printing查询打印队列状态然后通过 SignalR 推送给前端 Blazor 页面更新 UI。这里有个设计上的关键选择不要在页面组件里直接做 WMI 查询因为 WMI 查询是同步阻塞的会卡住 UI 线程。正确做法是把查询放到后台服务里定时执行把结果通过AppState事件通知推给页面。前端组件只需要订阅事件刷新界面即可。10.4 摄像头画面接入时的属性控制跟摄像头相关的场景控制摄像头属性增益、白平衡、分辨率等是通过封装好的 SDK 实现的。Blazor 端要做的就是把属性面板做成一组滑块和下拉框通过服务层转发设置指令。组件化之后不同品牌摄像头的差异被完全隔离在服务层前端不用关心协议细节。最后基于我的实际经验再啰嗦两句组件式开发的本质不是把页面拆碎而是给软件的长期演进留出空间。拆得好的组件系统需求变化时你只动一个文件拆得不好一个需求可能牵动十几个页面的改动。Blazor 的组件模型已经足够成熟关键要看你有没有养成“先设计再编码”的习惯。我现在的做法是拿到需求先画组件树想清楚每个组件的数据来源和行为边界再动手写代码。虽然前期要多花一点时间但到了后期开发和维护阶段省下来的时间远不止这一点。如果你刚接触 Blazor 组件开发建议从一个不起眼的小功能开始尝试组件化重构比如把某个页面的按钮弹窗拆出来。逐步习惯参数、回调、生命周期的配合节奏后再去做大型业务系统的组件规划会从容得多。这也是我一路走过来觉得最平稳的进阶路线。本文还有配套的精品资源点击获取
返回列表