公司动态

Blazor组件式开发实战:从零构建C#全栈应用

📅 2026/8/31 16:27:33
Blazor组件式开发实战:从零构建C#全栈应用
简介本资源是一套基于.NET 6.0与Blazor Server模式的组件化Web开发实战案例面向C# Web开发者及Blazor初学者聚焦数据库驱动型应用的标准化开发实践。项目采用VS2022开发后端依托SQL Server 2012与Entity Framework Core实现数据持久化核心亮点在于通过高度复用的单一Razor组件统一承载增、删、改、查及明细展示功能显著降低代码冗余与维护成本。压缩包共169个文件5.1MB涵盖20个C#业务逻辑文件、11个Razor组件页、39个运行时DLL、17个CSS样式资源及配套JSON配置、SQL初始化脚本等结构完整可直接编译运行。已有517人学习下载附带详细说明文档与运行效果截图帮助开发者快速掌握Blazor组件通信、EF数据绑定、服务注入及CRUD模块化封装等关键技能。 做 .NET 这几年经常被问到有没有用 Blazor 写过东西。Blazor 这个技术方向其实已经不算新了但它一直处在一个微妙的节点上——愿意用的团队觉得它是“C# 一条路走到底”的答案观望的人又总觉得它生态不够热闹。我的看法比较直接如果你主要在 C# 技术栈里做业务系统Blazor 组件式开发的这套思路是值得花时间吃透的。这里说的“组件式开发”不是简单地把页面拆成几个文件而是从设计层面把界面、状态、行为、复用边界都按组件维度重新组织。Blazor 的组件模型和 React/Vue 很像但又多了一个天然优势整个前后端逻辑可以统一用 C# 写不用在 JS 和 C# 之间来回切换上下文。这篇内容我就围绕一个实际案例展开把这个从零搭建 Blazor 组件式应用的过程、关键细节、踩坑点完整捋一遍。1. 组件式开发的核心思路与案例设计1.1 为什么推荐 Blazor 组件式开发很多传统 .NET 开发者的日常是后端写 API前端用 jQuery 或者 Vue 拼页面中间还要维护一套接口文档、DTO 定义、枚举映射。项目小的时候还能撑住一旦业务复杂起来前后端各改各的联调成本会明显上升。Blazor 最直接的价值就是把这个“前后端两套语言”的割裂感干掉了。但组件式开发这件事比“语言统一”更值得关注。它的本质是把界面拆成可独立开发、独立测试、独立复用的单元。每个组件就是一个“小而完整”的模块有自己的结构和状态对外只暴露几个参数和事件。这样做的收益有几个层面第一可复用性。一个做好的表格组件、分页组件、表单校验组件换一个项目照样用。只要把数据源抽成参数连 CSS 都能一起打包带走。第二可维护性。几百行代码的页面拆成十几个小组件之后每个组件只负责一件事。改一个筛选条件不需要去看几千行代码定位问题的范围大幅缩小。第三团队协作。组件边界清晰之后成员之间可以并行开发。你负责表格组件我负责表单组件只要提前把参数和事件约定好互相不干扰。我见过不少团队用 Blazor但写出来的代码还是“页面式思维”的写法——一个 .razor 文件里堆了上千行里面全是业务逻辑和数据请求。这本质上是用 Blazor 的语法写 WebForm 时代的页面组件化的好处一点没吃到。所以这篇文章的核心不是教你怎么用 Blazor 写一个页面而是教你怎么用“组件思维”去拆解和构建一个应用。1.2 一个完整案例的整体结构设计我们用一个管理系统里最常见的“设备信息管理”场景来当案例它的功能包括设备列表展示支持分页和关键字搜索设备状态标签在线/离线/告警设备详情弹窗和编辑表单状态筛选下拉框和列表之间的联动实时刷新最近一条设备状态这个案例不复杂但刚好覆盖了组件式开发里最重要的问题如何拆组件、如何传参、如何通讯、如何管理状态。我的组件拆分思路是这样的DevicePage.razor页面入口负责协调整体状态和数据加载。DeviceTable.razor纯展示型组件接收设备数据列表渲染表格不关心数据从哪来。StatusTag.razor最小组件接收一个枚举状态值输出对应的标签样式。SearchBox.razor搜索输入组件只负责把输入内容抛出去。DeviceEditForm.razor表单组件负责新增和编辑的校验逻辑。StatusFilter.razor筛选下拉组件选中状态后通知父页面。拆完之后可以看到一个共同点大部分组件是“受控组件”也就是自己不持有数据数据从父组件传进来操作通过事件抛出去。这种设计的好处是状态来源单一任何时候出了 bug往上找到第一个持有状态的地方就能定位问题。这里有一个很关键的实操经验拆分组件的时候不要一开始就追求“完美复用”。我见过有人为了做一个“通用弹窗组件”花了两周时间设计参数结果项目里的实际使用场景就两种过度设计反而拖慢了进度。更好的做法是先按业务边界拆等出现“两个地方长得差不多”的需求时再抽公共组件。2. 环境准备与项目搭建2.1 开发环境与模板选择做 Blazor 开发最省事的组合是 Visual Studio 2022 .NET 8。关于 .NET 版本我建议直接上 .NET 8它的 Blazor 统一了 Server 和 WebAssembly 的组件模型新增的RenderMode可以让你灵活地控制整个应用或者某个组件是跑在服务端还是跑在浏览器端。如果你还在用 .NET 6/7Blazor 的大部分概念也通用但在交互模式和项目结构上会有一些差异。安装完 VS 之后创建项目的路径是新建项目 → 搜索“Blazor” → 选择“Blazor Web App”。这里有个容易忽略的点在 .NET 8 的模板里项目建完之后默认是一个“启用全局交互”的应用页面默认用的是 Server 渲染InteractiveServer但你的交互组件如果希望以 WebAssembly 方式运行需要另外加InteractiveWebAssembly支持。我的建议是业务后台类系统优先用 Server 模式因为数据访问都在服务端安全性好控制开发效率也高。如果你的应用需要部署为纯静态站点或者需要极强的离线能力再考虑 WebAssembly。这里不是二选一.NET 8 允许同一个页面里混用两种模式。2.2 项目结构解读与“组件通信”初识模板创建完之后你会看到大致这样的结构DeviceManager/ ├─ Components/ │ ├─ Layout/ │ │ ├─ MainLayout.razor │ │ └─ NavMenu.razor │ ├─ Pages/ │ │ └─ Home.razor │ └─ App.razor ├─ Program.cs └─ DeviceManager.csprojComponents/Pages文件夹下是路由页面Components下其他没有page指令的.razor文件就是普通组件。在这里我要重点说一个容易绕晕的地方在 .NET 8 Blazor Web App 模板里路由页面和组件的关系比想象中更灵活。页面本身也可以理解为一个“大组件”它通过page指定路由地址然后在内部组合其他组件。这种“页面即组件”的思想是组件式开发的基础。接下来就是组件通信。最常见的通信方式有三种通信场景推荐方式说明父子组件数据传递组件参数[Parameter]父组件通过属性传值给子组件子组件通知父组件EventCallback子组件通过回调事件向上抛消息跨层级/全局状态CascadingValue或注入服务避免一层一层手动传参这三种方式是整个组件式开发的基石。后面写实操的时候每一步都会用到它们。3. 核心组件开发实操3.1 从最小组件开始写一个 StatusTag 状态标签组件我们先从最简单、也最能体现组件价值的StatusTag.razor开始。它的作用就是接收一个状态值输出带颜色的标签。code { [Parameter] public DeviceStatus Status { get; set; } private string CssClass Status switch { DeviceStatus.Online tag tag-online, DeviceStatus.Offline tag tag-offline, DeviceStatus.Warning tag tag-warning, _ tag tag-default }; private string DisplayName Status switch { DeviceStatus.Online 在线, DeviceStatus.Offline 离线, DeviceStatus.Warning 告警, _ 未知 }; } span classCssClassDisplayName/span这里的[Parameter]是组件对外暴露的唯一入口。父组件使用时非常简洁StatusTag Statusdevice.Status /写这个小组件的时候有一个细节值得注意我把CssClass和DisplayName都做成了计算属性而不是直接写在OnInitialized里。原因是组件参数可能会被父组件更新计算属性能保证每次渲染时都是取最新值。如果你在OnInitialized里把它赋值给一个普通字段父组件改参数时你的 UI 就不会刷新了——这是我见过非常典型的 Blazor 新手错误。3.2 数据列表组件DeviceTable 的参数设计与模板化StatusTag只是开胃菜真正的核心组件是DeviceTable.razor。它的目标是把“表格展示”这件事从业务页面上剥离开。typeparam TItem table classtable thead tr th设备名称/th th状态/th th最后上报时间/th th操作/th /tr /thead tbody foreach (var item in Items) { tr tditem.Name/td tdStatusTag Statusitem.Status //td tditem.LastReportTime.ToString(yyyy-MM-dd HH:mm:ss)/td td button onclick() OnEdit.InvokeAsync(item)编辑/button /td /tr } /tbody /table code { [Parameter] public IReadOnlyListTItem Items { get; set; } []; [Parameter] public EventCallbackTItem OnEdit { get; set; } }我用了typeparam TItem把这个组件做成了泛型组件理论上任何列表数据都可以复用它。实际开发中如果不同实体的表格列差异太大泛型组件往往会为了“通用”而变得很别扭。所以更平衡的做法是固定列用通用代码渲染业务列用RenderFragment模板让父组件自定义。这里再补充一个 RenderFragment 的典型用法typeparam TItem table tbody foreach (var item in Items) { tr RowTemplate(item) /tr } /tbody /table code { [Parameter] public IReadOnlyListTItem Items { get; set; } []; [Parameter] public RenderFragmentTItem RowTemplate { get; set; } default!; }父组件使用时DeviceTable Items_devices RowTemplate Contextdevice tddevice.Name/td tddevice.Location/td /RowTemplate /DeviceTable这样表格的外壳逻辑是通用的每一行展示什么列由父组件自己决定。很多优秀的第三方 Blazor 组件库都是这么设计表格组件的。我在实际项目里的经验是这种“泛型 模板”的组合是组件设计里最常用、也最容易被忽略的技巧。3.3 组件状态联动SearchBox 和 StatusFilter 如何与页面交互设备和表格这两个组件本身不持有数据它们只负责“展示”。那数据在哪里在页面里。页面作为“状态协调者”持有设备列表、搜索关键词、筛选状态然后把这些数据分发给各子组件。先写SearchBox.razor它不需要管数据在哪只负责把用户的输入告诉父组件input classform-control placeholder输入设备名称搜索 value_keyword oninputOnInput onkeydownOnKeyDown / code { private string _keyword string.Empty; [Parameter] public string Keyword { get; set; } string.Empty; [Parameter] public EventCallbackstring KeywordChanged { get; set; } private async Task OnInput(ChangeEventArgs e) { _keyword e.Value?.ToString() ?? string.Empty; await KeywordChanged.InvokeAsync(_keyword); } private async Task OnKeyDown(KeyboardEventArgs e) { if (e.Key Enter) { await KeywordChanged.InvokeAsync(_keyword); } } }这里有个组件设计上的常见争议SearchBox里的输入框到底应该用“即时响应”还是“回车响应”我的经验是如果搜索是查数据库一定要用回车或者防抖触发否则用户每敲一个字符就发一次请求压力太大。如果搜索是在内存里筛选比如List.Where(x x.Contains(...))那即时响应反而体验更好。Blazor Server 模式下默认的事件通信走 SignalR频繁触发事件会有一定开销所以这类交互要多考虑一下触发频率。然后在页面上把它们组合起来div classtoolbar SearchBox Keyword_keyword KeywordChangedOnKeywordChanged / StatusFilter Value_statusFilter ValueChangedOnStatusFilterChanged / /div DeviceTable Items_filteredDevices OnEditOnEditDevice /页面里的状态和处理逻辑code { private string _keyword string.Empty; private DeviceStatus? _statusFilter; private ListDeviceInfo _allDevices []; private ListDeviceInfo _filteredDevices []; protected override async Task OnInitializedAsync() { _allDevices await DeviceService.GetAllAsync(); ApplyFilter(); } private void OnKeywordChanged(string keyword) { _keyword keyword; ApplyFilter(); } private void OnStatusFilterChanged(DeviceStatus? status) { _statusFilter status; ApplyFilter(); } private void ApplyFilter() { _filteredDevices _allDevices .Where(d string.IsNullOrEmpty(_keyword) || d.Name.Contains(_keyword)) .Where(d _statusFilter null || d.Status _statusFilter) .ToList(); } }这个模式叫做“状态提升”。子组件本身不保存数据数据统一由父组件管理子组件只通过事件汇报“用户做了什么”。这种模式下整个页面的数据流永远是单向的排查问题只需要看父组件的逻辑即可不用在组件之间追踪状态变化。3.4 生命周期与数据刷新别让组件“只加载一次”Blazor 组件的生命周期顺序是SetParametersAsync→OnInitialized/OnInitializedAsync→OnParametersSet/OnParametersSetAsync→OnAfterRender/OnAfterRenderAsync。理解这些节点的执行时机对写组件来讲特别重要。最常见的困境是页面跳转时通过路由参数传了一个设备 ID组件在OnInitializedAsync里加载了数据。但如果用户在详情页点了“下一个设备”URL 变了、ID 变了组件却不会重新走OnInitializedAsync界面上的数据还是旧的。解决办法是重写OnParametersSetAsync当 ID 改变时重新加载数据[Parameter] public int DeviceId { get; set; } private DeviceInfo? _device; private int _loadedId; protected override async Task OnParametersSetAsync() { if (DeviceId ! _loadedId) { _loadedId DeviceId; _device await DeviceService.GetByIdAsync(DeviceId); } }为什么不直接在OnParametersSetAsync里无脑加载因为每次父组件刷新、甚至点击无关按钮都可能导致参数重新设置。如果每次都查接口会造成大量无用请求。用一个“上次已加载ID”做对比只有真正变化的参数才触发数据加载这个小技巧能省下不少无谓的调用。另外还有一点OnAfterRenderAsync是在组件渲染完成后执行的这里常用于操作 DOM 或调用 JS。如果在OnInitializedAsync里调用 IJSRuntime会报“JavaScript interop calls cannot be issued at this time”的错误提示原因就是这时候组件还没完成首次渲染。4. 场景化组件封装设备数据的加载与实时刷新4.1 把数据访问封装成服务而不是直接写在组件里我刚接触 Blazor 的时候图省事直接把HttpClient或数据库上下文写在组件里结果项目一复杂就后悔了。组件和数据逻辑混在一起既不方便单元测试也无法在多个组件间复用。更合理的方式是单独做一个服务类通过依赖注入给组件使用。public interface IDeviceService { TaskListDeviceInfo GetAllAsync(); TaskPagedResultDeviceInfo SearchAsync(string keyword, DeviceStatus? status, int page, int pageSize); TaskDeviceInfo? GetByIdAsync(int id); Task SaveAsync(DeviceInfo device); } public class DeviceService : IDeviceService { private readonly IDbConnection _db; public DeviceService(IDbConnection db) { _db db; } public async TaskListDeviceInfo GetAllAsync() { // 你的查询逻辑 } }在Program.cs里注册builder.Services.AddScopedIDeviceService, DeviceService();组件里使用时inject IDeviceService DeviceService这里依赖注入的生命周期值得多说一句。Blazor Server 模式下AddScoped的生命周期和 SignalR 连接是绑定的也就是说在同一个用户会话里这个服务实例基本可以看作单例。如果服务里不小心存了“上一个用户的临时状态”就可能出现数据串号。我的建议是服务保持无状态每次方法调用自行获取数据不要用字段缓存会话级别的数据。如果确实需要缓存要考虑用IMemoryCache带 key 区分或者明确标注作用范围。4.2 实时刷新组件用 SignalR 和 IAsyncDisposable 解决问题设备管理场景里往往有“实时状态”的诉求。Blazor Server 本身就跑在 SignalR 连接之上所以做实时推送是顺理成章的。比较推荐的做法是注入一个DeviceHubClient服务组件里订阅它的状态变化事件然后在组件销毁时退订。public class DeviceHubClient : IAsyncDisposable { private readonly HubConnection _connection; public event ActionDeviceStatus? OnStatusChanged; public DeviceHubClient(IConfiguration configuration) { _connection new HubConnectionBuilder() .WithUrl(configuration[SignalR:DeviceHub]!) .Build(); _connection.OnDeviceStatus(StatusChanged, status OnStatusChanged?.Invoke(status)); } public async Task StartAsync() { await _connection.StartAsync(); } public async ValueTask DisposeAsync() { await _connection.DisposeAsync(); } }组件使用时implements IAsyncDisposable protected override async Task OnInitializedAsync() { await _hubClient.StartAsync(); _hubClient.OnStatusChanged HandleStatusChanged; } private void HandleStatusChanged(DeviceStatus status) { InvokeAsync(async () { _latestStatus status; await LoadLatestDataAsync(); StateHasChanged(); }); } public async ValueTask DisposeAsync() { _hubClient.OnStatusChanged - HandleStatusChanged; await _hubClient.DisposeAsync(); }这里必须注意的是组件销毁时一定要退订事件。如果不退订组件实例虽然被销毁了但事件处理器的引用还留在DeviceHubClient里导致组件无法被垃圾回收每次进入页面就新增一个订阅内存泄漏非常隐蔽。我排查过好几个 Blazor Server 项目的内存持续增长问题最后都是这类订阅没释放导致的。4.3 表单组件的校验与远程验证DeviceEditForm.razor这个组件既涉及展示也涉及数据交互。Blazor 自带的EditForm和DataAnnotationsValidator已经能覆盖大多数表单校验需求模板自带的方式就能自动生成验证消息。有一个场景在“设备信息管理”里很典型用户填写的设备编号需要到后端检查是否重复。这时用 DataAnnotations 的Remote类校验不太够需要在表单提交前做一次异步检查。我的做法是在OnValidSubmit之前做一个远程校验EditForm Model_formModel OnValidSubmitHandleValidSubmit DataAnnotationsValidator / ValidationSummary / div classform-group label设备编号/label InputText bind-Value_formModel.DeviceCode / ValidationMessage For(() _formModel.DeviceCode) / /div button typesubmit disabled_submitting保存/button /EditFormprivate async Task HandleValidSubmit() { var exists await DeviceService.CheckCodeExistsAsync(_formModel.DeviceCode, _editingId); if (exists) { // 把错误附加到验证消息上 _errorMessage 设备编号已存在请更换; return; } _submitting true; try { await DeviceService.SaveAsync(_formModel); await OnSaved.InvokeAsync(_formModel); } finally { _submitting false; } }这种“模型校验 远程检查 事件回调”的组合覆盖了实际开发中 80% 的表单场景。记住一个原则EditForm本身只解决“格式和必填”的校验涉及数据库唯一性、关联关系等业务规则一定要在服务端做二次确认不能只靠前端校验兜底。4.4 和 JavaScript 互操作读取设备能力时的浏览器 API 封装虽然 Blazor 的组件化很顺手但浏览器端有些 API 还是绕不过去。比如设备地图定位、摄像头参数读取、打印控制等场景必须写一点 JS。好在 Blazor 提供了 JS 互操作机制而且推荐做法是把 JS 封装成“隔离模块”避免污染全局作用域。在wwwroot/js/device-camera.js里export function getCameraCapabilities() { return new Promise((resolve, reject) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { reject(new Error(浏览器不支持摄像头访问)); return; } navigator.mediaDevices.getUserMedia({ video: true }) .then(stream { const track stream.getVideoTracks()[0]; const capabilities track.getCapabilities(); const settings track.getSettings(); track.stop(); resolve({ capabilities, settings }); }) .catch(reject); }); }在组件里调用private IJSObjectReference? _cameraModule; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _cameraModule await JS.InvokeAsyncIJSObjectReference(import, /js/device-camera.js); } } private async Task ReadCameraInfo() { var info await _cameraModule!.InvokeAsyncCameraInfo(getCameraCapabilities); _cameraResolution ${info.Settings.Width} x {info.Settings.Height}; }这段代码的细节是import方法配合.js文件里的export这是 Blazor 推荐的 JS 隔离方案。比全局window上挂函数更干净也不会造成同名函数冲突。另外调用完 JS 模块后记得在DisposeAsync里释放_cameraModule的引用。5. 常见问题与排查技巧实录5.1 第一次加载正常刷新后就 404这个问题主要出在部署环境的 URL 重写配置上。Blazor Server 是 SignalR 实时连接刷新时浏览器会去请求一个 Page 路由地址如果服务器没有把未知路径回退到入口页面就会 404。排查思路是先走一遍本地 IIS Express 是否复现再检查服务器上 Web.config 里的aspNetCore配置是否带preserveHostHeader和路径回退规则。绝大多数情况是 Web 服务器重写规则没配好和业务代码无关。5.2 “无法加载一个或多个请求的类型”错误这个错误是很多 .NET 初学者初次进入项目时最常见的拦路虎。触发场景通常有两种一种是解决方案中某些项目没有引用或者引用的程序集版本不匹配另一种是编译后某个依赖缺失。排查方法很直接打开 Visual Studio 的“输出”窗口在“加载”或“程序集绑定日志”里查看是哪个程序集加载失败也可以在命令窗口里用dotnet build检查编译输出。如果日志提示某程序集的版本对不上优先检查 NuGet 包引用是否统一了版本号。跨项目引用的类型不一致也会导致这个错误清理解决方案然后重新生成即可。5.3 组件刷新了但 UI 没有更新Blazor 的组件渲染有一套自己的比较逻辑。最常见的问题是你在组件内部直接修改了一个被ListT或者普通对象持有的数据但没有调用StateHasChanged这时候页面不会自动刷新。例如private async Task OnDeviceLoaded(DeviceInfo device) { _devices.Add(device); // 忘掉调用 StateHasChanged() }还有一种是修改了引用类型里的属性的属性比如device.Status DeviceStatus.WarningBlazor 自己感知不到这种深层变化。解决办法很简单手动调用StateHasChanged()或者保持“赋值新对象”的写法让组件的状态更新走正常链路。5.4 组件重复初始化、生命周期执行多次这个现象在 Blazor Server 里很典型尤其是当你用了key指令之后。key的作用是告诉 Blazor“这个位置上的组件是根据这个 key 来识别的”如果 key 变了Blazor 会认为组件需要重建于是重新走一遍生命周期。如果 key 没变组件则会被复用。如果不想重建组件就要检查是不是 unintentionally 把每次刷新都会变化的值放进了key。5.5 常见问题速查表问题可能原因快速排查手段刷新后 404URL 重写/回退规则未配置检查 Web.config 和服务器重写规则编译报错“无法加载一个或多个请求的类型”依赖缺失或版本冲突查输出窗口的程序集绑定日志组件 UI 不更新引用类型深层属性变更未通知调用StateHasChanged()或替换对象引用重复初始化key设置不当检查 key 是否在每次刷新时变化内存持续增长事件订阅未取消在DisposeAsync里退订事件SignalR 连接频繁断开负载均衡未开启粘性会话配置 Sticky Sessions 或改用 WebAssembly 模式6. 实战心得与进阶扩展6.1 组件拆分粒度别太细也别太粗关于组件拆分的粒度我一直觉得“能在一个文件里看清完整逻辑”是一个重要判断标准。一个组件如果超过 300 行且它做的事情不止一件这时候就该考虑拆分了。但反过来如果一个组件拆得特别碎父组件里全是参数传递和事件接收代码也不利于阅读。我的经验是一个组件只做一件事并且对外暴露的参数控制在 5 个以内。如果超过 5 个参数可能是组件职责过宽或者应该改为子内容配置方式。另一种方法是把多个相关参数封装成一个配置对象类型比如表格配置、表单配置能有效降低使用方的认知负担。6.2 性能优化从渲染机制入手Blazor Server 的性能大头在 SignalR 通信上每个 UI 事件、每次渲染差异都要走网络所以要注意减少不必要的组件渲染。常见手段有用int作为状态标识避免传递复杂对象导致各层组件重复刷新。对频繁更新的组件通过“分片显示”或者“节流”减少渲染次数。合理使用key帮助 Blazor 判断组件复用边界。数据量大时使用Virtualize组件做虚拟化渲染只渲染可视区域内的行而不是一次性渲染几千行。Virtualize的用法非常简单Virtualize Items_largeList Contextitem tr tditem.Name/td tditem.Description/td /tr /Virtualize对于几十上百条的数据看不出差别一旦数据量上万这个组件的收益非常明显。6.3 从案例到通用后续可以怎么扩展这个设备管理案例做完后你能很自然地迁移到很多业务场景。比如把DeviceTable扩展成支持排序、多选、列配置的通用表格把SearchBox扩展成带防抖的远程搜索框把StatusTag扩展成支持图标的通用状态组件。这些扩展方向本质上都是在“组件模型”上不断加能力。我个人在实际项目里还有个体会Blazor 的学习曲线不在语法上而在于你能否逼自己用“组件思维”想问题。很多人写着写着就回到了“页面调用服务、服务返回数据、页面循环渲染”的老模式。如果每一行代码都思考一下“这个功能会不会在别的地方用到”“这个逻辑应该由谁持有状态”组件化开发才能真正带来效率提升。最后再分享一个小技巧调试组件时不要只靠断点可以多利用 Blazor 的渲染日志和rendermode配置来确认当前组件到底跑在 Server 还是 WebAssembly 上。很多诡异的表现差异最终都指向“渲染模式没搞清楚”这一个原因。做 Blazor 项目先把渲染模式这件事彻底弄明白了后面能少走不少弯路。本文还有配套的精品资源点击获取