ARTICLE DETAIL

资讯详情

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

ASP.NET Core MVC中Razor组件化实践:从Partial View到ViewComponent

ASP.NET Core MVC中Razor组件化实践:从Partial View到ViewComponent 我最早接触到 Razor 视图引擎的时候其实心里是有点不屑的——那时候前端正流行 Vue、React 那一套组件化玩法回头再看到 ASP.NET 自带的.cshtml总觉得这就是一个做服务端渲染的老古董简单页面套套模板还行真要搞复杂交互肯定得靠 JS 框架撑场面。但后来在实际项目里被需求反复折腾了几轮我发现这种想法很有代表性但也确实有点低估了 Razor 在现代 ASP.NET Core MVC 里的分量。尤其在微软把 Razor 升级到Razor Pages、Razor ComponentsBlazor 的服务端渲染模型这些分支之后很多人容易把它们和经典 MVC 里的 Razor 视图搞混。我这篇文章聚焦的是经典场景ASP.NET Core MVC 架构下用 Razor 把前端拆成可复用的组件。这个场景依然是大量企业级应用、后台管理系统、内部工具平台的绝对主力。你不需要引入一套完整的前端工程化体系也能把重复的页面片段抽出来做成带参数、带逻辑、甚至可以异步加载数据的前端组件只是它们的运行位置从浏览器挪到了服务器上。这篇文章适合谁给那些还在用纯cshtml写页面但总觉得代码重复得难受的同学也给那些想引入前端工程化但暂时没条件彻底重构、只能在现有 MVC 框架里做局部优化的团队。我会把我在项目里的选型思路、踩坑记录、代码片段全部整理出来尽量做到拿过去就能用。1. Razor 组件化的整体思路为什么还在服务端做前端组件很多人一听到前端组件第一反应就是浏览器端的事情。但你要先想清楚一个问题你的页面到底是谁在拼装的如果是纯前端框架页面骨架由浏览器里的 JS 执行并渲染 DOM而 Razor 的模式是服务器把 HTML 字符串完完整整算好再一次性吐给浏览器。这两种方式没有绝对的好坏只有适配场景的差异。1.1 服务端组件和前端框架组件的本质区别前端框架的组件核心是状态驱动视图——组件内部维护状态状态变则视图变事件、生命周期、副作用都发生在浏览器端。Razor 作为服务端组件时它工作在一个完全不同的模型下每次请求进来服务器把 Razor 模板执行一遍生成完整的 HTML 响应然后连接就断开了。组件里的状态要么放在服务器端的 Session 或数据库中要么放在数据库查出来的数据里要么放在请求参数里。所以当你听到用 Razor 做前端组件时你要理解它的本质是服务端模板片段复用而不是浏览器端的动态交互组件。两者不是竞争关系更多是互补关系。举个例子一个后台管理系统的列表页、详情页、表单页它们的页面结构大量重复但这些页面本身的交互复杂度不高主要就是展示跳转提交用 Razor 组件切分足够干净利落。1.2 为什么选 Razor 而不上 Vue/React我在好几个项目里做过技术选型。有些团队一上来就建议反正前端要组件化干脆用 Vue 吧但是冷静下来盘一盘账会发现几个很现实的问题现有团队的前端能力储备不够、系统是服务端渲染为主的老架构、没有精力维护两套代码库和构建链路。这时候 Razor 组件化几乎是性价比最高的平滑方案。我在实际项目里测试过混合方案.NET 服务端提供 API前端用 Vue 做 SPA但这意味着要做接口设计、鉴权对接、跨域处理、状态管理原本 3 天的开发量很容易被拉长到 10 天。而 Razor 的方式不改变整个请求-响应模型你在cshtml里写到的数据就是强类型的、经过编译检查的模型数据开发效率非常高。还有个非常实际的点SEO。服务端渲染天然对搜索引擎友好因为 HTML 已经在响应里了。这对那些需要被索引的公开页面很关键。而 Razor 不需要额外做预渲染之类的事情。1.3 Razor 在 MVC 中的几种组件化手段进入实操之前先把工具盘点清楚。常见的方案有这么几种Partial View分部视图最轻量、最直接的片段复用方式。通过Html.Partial/Html.PartialAsync或partialTag Helper 调用。ViewComponent视图组件重量级一点带自己的业务逻辑能从数据库或服务层取数据适合承载带数据获取逻辑的组件。Tag Helper标签助手把组件包装成类似自定义 HTML 标签的写法本质上底层还是走 Partial 或 ViewComponent但使用体验非常好。Razor Class LibraryRCL把 Razor 组件打包成独立的类库多个项目共享也就是把组件提升到可分发、可复用的维度。这四种手段不是互斥的实际项目中往往是组合使用。我的建议是纯展示型、无业务逻辑的复用片段用 Partial View需要查数据库、有自身业务规则的片段用 ViewComponent希望页面代码更优雅、更像 HTML 的用 Tag Helper 做一层包装多个项目共享的放入 RCL 类库。这套分工方式我在多个后台管理系统里实践过结构很清楚。2. 核心语法细节与组件切片能把这几个语法用熟组件化就通了一半很多人不敢拆组件不是因为不会用 Partial而是对 Razor 的语法边界不够熟悉不知道哪些东西能在组件边界自由传递、哪些会踩坑。这一节我挑几个和组件化直接相关的语法点详细讲每个都是我实际用过的场景。2.1 model 与强类型传递给组件划清楚数据边界每个 Razor 组件Partial View最顶上的第一件事就是声明自己要什么数据。这就是model指令。它不只是语法层面的存在它意味着你在这个组件里写.的时候编辑器有完整的智能提示字段拼错了编译直接报错而不是页面运行时报黄页。model IEnumerableProductCardViewModel div classproduct-grid foreach (var product in Model) { partial name_ProductCard modelproduct / } /div这里有个小细节值得注意modelproduct传单个数据对象进去之后_ProductCard.cshtml顶部的model就必须恰好是ProductCardViewModel类型。这是 Razor 组件最舒服的地方——严格契约类型安全比 JavaScript 那种传什么都能接运行时炸给你看的模式可靠得多。如果组件的某些数据不是必填的建议给属性设置默认值或者用可空类型。我自己在写组件时习惯把核心展示数据设为必填把样式类、配置项等设为可选这样组件的调用方一眼就能看出哪些是必须要准备的。2.2 functions 与局部函数组件内部逻辑封装在cshtml里写代码的方式有两种一种是直接写在页面的代码块{}中另一种是用functions定义带返回值的方法。两者的区别在于作用域和复用性。页面的{}代码块适合一次性逻辑而functions块定义的方法是组件模板内部的辅助函数。我在封装复杂组件时经常用functions来把字符串截断、时间格式化、状态标签颜色映射这类逻辑收敛到组件内部不污染外层页面。model CommentItemViewModel functions { private string FormatTime(DateTime time) { // 如果是今天只显示时分否则显示完整时间 return time.Date DateTime.Today ? time.ToString(HH:mm) : time.ToString(yyyy-MM-dd HH:mm); } } div classcomment-item span classcomment-authorModel.Author/span p classcomment-contentModel.Content/p span classcomment-timeFormatTime(Model.CreatedAt)/span /div这里要注意一个容易被忽略的点functions里的方法是为当前视图模板服务的它不会变成全局公共方法。如果多个组件都要用同一个格式化逻辑应该把它提取到公共静态类里或者写一个自定义 Tag Helper而不是在多个组件里各自复制一份。2.3 HTML 编码与 Html.Raw 的安全边界Razor 默认会对所有输出做 HTML 编码这是框架给的安全带。比如Model.Content里如果包含script标签显示出来的是script的文本字符不会真的执行。这是防止 XSS 的第一道防线。但有些场景我们需要输出真正的 HTML比如富文本编辑器生成的内容、后端拼接好的提示消息。这时候会用到Html.Raw。这块是我在实际项目里最警惕的地方。Html.Raw用错了就是灾难。什么时候绝对不能绕过编码任何来自用户输入、数据库内容、URL 参数的数据直接Html.Raw输出等于把系统大门敞开。// 安全默认编码 Model.UserName // 危险仅当内容来源完全可信时才用 Html.Raw(Model.RichContent)我处理富文本内容时的一个经验是入库前先做白名单过滤只保留 p、br、strong、img 等安全标签出库展示时才敢用Html.Raw。如果你的用户头像 URL 是用户自己填的用Model.AvatarUrl直接输出到src属性是安全的因为默认编码但如果是拿来拼style属性做背景图就必须小心url(...)注入的问题。2.4 ViewData、ViewBag、TempData 在组件间传值的正确姿势做组件化一定会遇到外层页面要给内部组件额外传一些标记数据的场景。比如列表页要给所有卡片组件传一个当前用户角色好在卡片里决定显示哪些操作按钮。粗暴的做法是每个组件调用都加一个参数但这会让调用点变得很啰嗦。Razor 提供了另外几个共享数据的渠道ViewData、ViewBag、TempData。我的使用建议非常明确ViewData / ViewBag同一请求生命周期内的隐形传参给非核心业务数据用。比如当前页面标题、面包屑导航数据、CSS 类名标记。TempData跨请求传递的临时数据典型场景是POST 表单处理完毕后跳转在下一个 GET 请求里显示操作成功的提示信息。注意 TempData 默认只在一次读取后保留Peek 可以保留用的时候要确认读取模式。组件内部读取 ViewData 的方式是{ var currentRole ViewData[CurrentRole]?.ToString() ?? ; } if (currentRole Admin) { button classbtn-delete删除/button }但要克制一点ViewData 用多了组件和页面之间的数据边界就模糊了新同事接手时会很困惑这个键到底是哪里设置的。我的习惯是一个组件最多依赖 1-2 个 ViewData 键而且必须在组件顶部注释里写清楚依赖的键名。3. 实操全过程从 Partial View 到 ViewComponent 再到 Tag Helper 的完整落地光说不练不行。这一节我拿一个真实需求来走一遍完整流程。需求不算复杂但很典型后台管理系统的订单卡片组件要求展示订单号、客户名、金额、状态不同状态有不同颜色标识同时订单列表页是分页的还需要一个复用性很强的分页组件。3.1 第一步创建强类型的 Partial View 组件我习惯为每个组件创建一个配套的 ViewModel 类放在Models/ViewModels/Components目录下。这样组件的输入参数就有了严格的类型定义。public class OrderCardViewModel { public string OrderNo { get; set; } public string CustomerName { get; set; } public decimal Amount { get; set; } public OrderStatus Status { get; set; } } public enum OrderStatus { Pending 0, Paid 1, Shipped 2, Completed 3, Cancelled 4 }对应的 Partial View 文件放Views/Shared/Components/OrderCard/_OrderCard.cshtmlViewComponent 默认会在Views/Shared/Components/{组件名}/下找视图。这一步看似简单实际的收益是重构时的搜索成本大幅下降——你不再需要在一大坨页面里寻找散落的订单卡片代码。3.2 第二步在页面中通过 Partial / PartialAsync 使用组件使用场景通常在订单列表页。列表页拿到 List订单集合循环输出每个订单卡片model IEnumerableOrderListItemViewModel foreach (var order in Model) { partial name_OrderCard modelorder / }这里有一个关键决策点同步还是异步我踩过这样的坑一开始图省事全部用Html.Partial结果页面在某个组件的数据获取逻辑里加了Task.Run或异步服务调用运行时报不支持同步执行的错误。实际经验是除非你的组件仅仅是纯展示逻辑否则直接用PartialAsync。同步 Partial 遇到异步数据源会卡死甚至直接报错排查起来非常痛苦。使用PartialAsync的调用方式长这样foreach (var order in Model) { await Html.PartialAsync(_OrderCard, order) }注意await前面不能漏掉漏了 Razor 会把整行当普通文本输出到页面上这个错误很隐蔽——我第一次出这问题时页面多了一行代码排查了很久才发现是漏了。3.3 第三步用 ViewComponent 承载带数据获取逻辑的组件订单卡的展示数据是列表页传给它的它自己没有获取数据的责任。但如果是今日待处理订单数这种小卡片组件——它需要自己去查数据库——用 Partial 就有点别扭了因为调用方得先自己去查再传进来等于每次都要写重复的查询逻辑。这种场景就该交给 ViewComponent。创建一个 ViewComponentpublic class PendingOrderCountViewComponent : ViewComponent { private readonly IOrderRepository _orderRepository; public PendingOrderCountViewComponent(IOrderRepository orderRepository) { _orderRepository orderRepository; } public async TaskIViewComponentResult InvokeAsync() { var count await _orderRepository.CountPendingOrdersAsync(); return View(new PendingOrderCountViewModel { Count count }); } }在页面里直接调用await Component.InvokeAsync(PendingOrderCount)ViewComponent 可以接收参数await Component.InvokeAsync(PendingOrderCount, new { days 7 })对应的InvokeAsync方法签名变成InvokeAsync(int days)。注意参数名是大小写不敏感的new { days 7 }里的days会正确绑定到int days参数。这背后走的是 ASP.NET Core 的模型绑定机制不是简单字典查找所以字符串、数字、布尔值都能正确转换。ViewComponent 真正的优势在于每个组件自带数据获取逻辑页面调用方不需要关心组件内部怎么拿到数据。这和小程序的自定义组件理念很接近——组件的内部世界对调用方透明。3.4 第四步用 Tag Helper 把组件包装成自定义 HTML 标签Partial View 用着是不错但页面里partial name_OrderCard modelorder /这种写法还是显得模板味儿太重。如果项目里有很多组件我更推荐用 Tag Helper 封装一层调用变成了order-card orderModel /。这个体验非常接近原生 HTML 扩展View 元件的可读性和可维护性都更高。自定义 Tag Helper 的完整代码[HtmlTargetElement(order-card)] public class OrderCardTagHelper : TagHelper { [HtmlAttributeName(order)] public OrderListItemViewModel Order { get; set; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { output.TagName null; // 不输出 order-card 本身 var content await HtmlHelper.PartialAsync(_OrderCard, Order); output.Content.SetHtmlContent(content); } }使用时需要在_ViewImports.cshtml里用addTagHelper注册addTagHelper *, YourProjectName然后页面里就能直接写order-card orderitem /这里有个非常重要的细节TagHelper 的命名约定。类名OrderCardTagHelper对应标签名order-card去掉 TagHelper 后缀转 kebab-case属性名Order对应order属性。如果你的页面里组件名存在嵌套关系比如OrderCardListTagHelper对应order-card-list要特别注意中间的分隔符是连字符而不是下划线。这个规则熟悉了之后基本一眼就能算出标签名不用翻文档。3.5 第五步做一个带请求参数的分页组件分页组件几乎在所有后台列表页都用到我干脆把它也封装成 ViewComponent这样分页计算逻辑就集中在一处不用每个页面都写一遍计算总页数、当前页、上一页、下一页的重复代码。分页组件的参数设计要包含当前页号、每页条数、总条数、基础路由信息。前端展示的是页码按钮、上一页/下一页链接以及一个第 x / y 页的标识。public class PaginationViewComponent : ViewComponent { public IViewComponentResult Invoke(int currentPage, int pageSize, int totalCount, string action, string controller) { var totalPages (int)Math.Ceiling((double)totalCount / pageSize); var vm new PaginationViewModel { CurrentPage currentPage, TotalPages totalPages, Action action, Controller controller }; return View(vm); } }视图文件Views/Shared/Components/Pagination/Default.cshtmlmodel PaginationViewModel if (Model.TotalPages 1) { nav classpagination if (Model.CurrentPage 1) { a asp-actionModel.Action asp-controllerModel.Controller asp-route-page(Model.CurrentPage - 1)上一页/a } for (int i 1; i Model.TotalPages; i) { a asp-actionModel.Action asp-controllerModel.Controller asp-route-pagei class(i Model.CurrentPage ? active : )i/a } if (Model.CurrentPage Model.TotalPages) { a asp-actionModel.Action asp-controllerModel.Controller asp-route-page(Model.CurrentPage 1)下一页/a } /nav }调用方式将列表数据的总条数、当前页等信息传进来分页组件内部计算出总页数。这在多页面上都是同一套逻辑以后要在分页按钮样式上做统一调整只需要改这一个组件文件。asp-action、asp-controller、asp-route-*这些是 ASP.NET Core 的内置 Tag Helper它会在服务端生成正确的路由 URL。注意如果页面所在控制器和分页目标控制器不同必须显式指定asp-controller否则默认使用当前请求的控制器URL 就会指向错误位置这个坑我在用户列表页和订单列表页同时分页的时候踩得很深。4. 遇到的坑与排查方法组件化项目中最典型的 5 类问题封装多了项目里的坑也会跟着来。这一节我梳理了在 Razor 组件化项目中最常遇到的几类问题每个都是真实踩过并且确认了根因的重要部分我会加上排查建议。4.1 模型绑定失效的部分原因属性名不匹配或类型不同问题场景自定义 Tag Helper 属性绑不上Order属性一直是 null页面静默显示空内容不报错。这个问题不查源代码非常难发现。核心原因往往是两个一是属性名和标签属性名不一致比如 C# 属性叫Order标签里写order-item映射规则就对不上二是类型不匹配——C# 属性是OrderListItemViewModel但调用时传的是别的类型Tag Helper 内部绑定失败不会给你编译期或运行期的明显提示。排查建议先在 TagHelper 的ProcessAsync开头打个断点看属性值是否进来。如果压根没进来重点检查[HtmlAttributeName]的映射名和调用时的标签属性名是否完全一致。如果值进来了但类型不对检查调用方传给order属性的对象类型。4.2 Partial 和 ViewComponent 找不到视图文件问题场景组件运行时报InvalidOperationException提示找不到视图。排查第一步看文件名和目录路径。Partial View 默认位置是Views/Shared/下或者在与调用视图的同名 Controller 目录下。ViewComponent 默认位置是Views/Shared/Components/{ComponentName}/{ViewName}.cshtml且ComponentName是类名去掉ViewComponent后缀。一个常见的坑我在创建 ViewComponent 时类名是PendingOrderCountViewComponent然后在Views/Shared/Components/PendingOrderCount/Default.cshtml中写视图但偶发地系统提示找不到。后来发现是文件被放在了错误的文件夹里比如放成了PendingOrderCountViewComponent/。文件夹名字必须去掉 ViewComponent 后缀这是默认约定改配置当然可以但没有必要。4.3 异步和同步混用导致的线程阻塞问题这是 .NET 项目中非常著名的坑之一。当你在一个同步的 Razor 视图中调用Html.PartialAsync但前面漏了await或者反过来在{ }代码块里.Result强行等待一个异步任务表现往往是页面卡死、CPU 高、或者直接抛异常。原因是线程池的线程在等待异步操作完成时被占满出现线程饥饿starvation。我的使用惯例是组件链路上全部保持 async。页面用await Html.PartialAsync(...)ViewComponent 的InvokeAsync内部也用await TagHelper 的ProcessAsync里也始终await。不要在任何一个环节图省事用.Result或.Wait()。这样能避免绝大多数由混合使用导致的线程阻塞问题。4.4 防伪令牌Antiforgery Token在表单组件中的埋点问题涉及到表单的组件必须在表单内包含Html.AntiForgeryToken()。如果你把整个表单封装成了 Partial 组件然后多个页面引用漏掉这个 Token 会导致 POST 提交时出现 400 错误或验证失败。我遇到的坑是某个搜索表单组件在 A 页面引用时一切正常拿到 B 页面引用时就报防伪令牌验证失败。查了很久才发现是 B 页面的布局或某个配置全局关闭了自动生成表单令牌导致组件里的Html.AntiForgeryToken()没有渲染出来。类似的场景要尤其注意组件是共享的但宿主页面的全局配置可能不一样。4.5 缓存策略问题组件输出缓存和页面缓存的冲突如果你用 ResponseCache 缓存了整个页面但页面里有一个跟当前登录用户强相关的组件比如用户专属通知就可能出现 A 用户看到了 B 用户的数据。这就是组件粒度缓存和页面粒度缓存打架的结果。我的处理原则是包含个性化数据的组件不放在被整体缓存的页面区域里或者干脆不开启整页缓存而是对纯静态区域使用分布式缓存或片段缓存。如果实在需要页面级缓存就把个性化组件通过 ViewComponent 在页面缓存之外单独输出或者借助 VaryByUser 之类的配置隔离用户维度。4.6 常见问题速查表现象最可能原因快速定位方法页面显示但组件区域空白组件视图没找到 / TagHelper 属性为 null看 Output 窗口或日志有无视图加载错误打断点确认 Model 值编译报错提示找不到 Razor 指令组件视图不在约定目录检查 Views/Shared 和 Components 目录结构页面卡死 / 高 CPU / 白屏异步方法用.Result同步等待全局搜索.Result和.Wait()改为 async/awaitPOST 提交返回 400表单少了防伪令牌页面源码搜索__RequestVerificationToken字段是否存在标签助手标签没有渲染addTagHelper未注册 / 程序集名不对在_ViewImports.cshtml检查addTagHelper *, {项目集名}URL 链接生成到错误控制器分页组件没指定controllerasp-controller显式传参不要依赖当前请求上下文组件中Model为 nullmodel参数没传 / 类型不匹配检查partial model...与model类型声明是否一致TagHelper 修改了不该改的标签属性对 output 的属性操作逻辑太宽泛在ProcessAsync中用output.Attributes.SetAttribute谨慎处理并加判断5. 组件颗粒度与目录组织项目结构层面的经验沉淀除了语法和具体的 API 使用组件化真正能不能落地很大程度取决于组织方式。这一节我聊一聊项目层级上迭代出来的经验这部分通常不写进官方文档但实战价值非常高。5.1 组件目录的划分方式我现在的项目习惯是在Views/Shared下按业务模块分子目录而不是把所有组件平铺在一个文件夹里。你如果在Shared下堆 100 个组件文件过俩月你一定会崩溃——文件太多名字又相近找起来极麻烦。推荐结构大概是Views/Shared/ ├── Components/ │ ├── OrderCard/ │ │ └── Default.cshtml │ ├── PendingOrderCount/ │ │ └── Default.cshtml │ └── Pagination/ │ ├── Default.cshtml │ └── PaginationViewModel.cs ├── Partials/ │ ├── _OrderCard.cshtml │ ├── _CustomerInfo.cshtml │ └── _AuditTrail.cshtml按是否带数据获取逻辑来区分放 Partial 还是 ViewComponentPartials 目录下放纯展示型部件Components 目录下反而直接放 ViewComponent 的视图和配套 ViewModel。这样在做代码审查时一眼就能从目录层次看出一个组件属于哪种类型避免混淆。5.2 组件与 ViewModel 的关系一个组件对应一个专属 ViewModel我在早期项目里犯过一个错误直接用数据库实体类EF Core 的实体当组件的 Model。结果是实体上的导航属性、字段非常多组件根本用不完而且一旦查询接口改了组件的展示逻辑也跟着受影响。后来我改为一个组件对应一个专属 ViewModel团队协作时彼此的改动边界一下子就清晰了。说是 ViewModel本质就是组件输入参数的 DTO数据传输对象。不需要追求复杂就是一个简单的 POCOPlain Old C# Object字段精简单一反映组件实际需要的数据就够。好处有两个第一组件开发者可以只看 ViewModel 就知道组件需要什么数据不用翻实体类第二当接口变化时只需要在控制器与 ViewModel 之间做映射不影响组件本身的视图逻辑。5.3 用 Razor Class Library 做跨项目共享组件当你在 A 项目里封装了一套很成熟的组件分页、卡片、状态标签、错误提示框B 项目恰好是另一个业务线的后台系统不想再写一遍就可以把这些组件放进 Razor Class LibraryRCL。这是我后来强烈推荐的习惯一个组织级的 UI 基础组件库完全可以用 RCL 实现不需要引入 npm 包和一整套 Webpack/Vite 体系。创建一个 RCL 之后把_OrderCard.cshtml及其 ViewModel 放进去目标项目引用类库再在_ViewImports.cshtml里注册 TagHelper 即可使用。RCL 里还可以附带静态资源——wwwroot 下的 js/css 会自动以_content/{LibraryName}/路径提供给引用项目。这意味着你可以直接把组件的样式和脚本打包进类库里引用项目零成本接入。RCL 在实践中要注意的是命名空间引用和版本管理。类库升级后引用项目的 Razor 视图需要编译更新通常重新构建即可。为了更顺畅地使用此类库组件内部的资源 URL 要尽量使用相对路径或Url.Content()生成不能在wwwroot里硬编码死相对路径否则部署在一个虚拟目录下时就会出现资源 404。5.4 组件化之后对性能的影响与优化每次组件调用都会带来额外的渲染开销。单个组件的开销并不大但如果在一个列表页里循环 100 次调用某个组件每次组件还要去查数据库比如循环里调用 ViewComponent 查关联数据性能就会有明显劣化。这是我在早期版本中亲测过的教训——列表接口返回 5000 条数据每条数据里有一个订单状态统计组件页面直接跑了十几秒。优化策略本质上就是减少组件上下文中的数据库调用把循环内组件需要的数据一次性查好后放进集合组件只做展示。用 ViewComponent 时启用缓存[ViewComponent]特性或者直接在内部查完数据后缓存。如果只是静态数据展示用MemoryCache给组件的查询结果加一层短时缓存。比如分页组件的总条数通常一轮查询就能取到完全没必要让分页组件再调一次Count查询——除非总条数和列表数据来自不同的数据源。6. 项目实践中的体感与心得这节没有代码了是我在实际使用 Razor 做组件化一段时间后最想说的心里话。如果你正在犹豫要不要花精力做这件事这些感受可能会对你有帮助。第一个感受是Razor 组件化的核心收益不一定是性能而是开发心智负担的下降。当你把页面拆成一块块职责清晰的组件新同事接手的成本明显降低——他们不需要通读整页代码才能找到一个按钮只需要找到对应的组件文件改好再回到页面里验证即可。这一点在多人协作的项目里价值非常大。第二个感受是组件边界设计比组件实现更重要。一开始我拆组件时很兴奋看到稍微重复一点就拆结果拆得太碎组件之间互相传参的复杂度反而比不拆还高。后来我定了两个简单的判据第一这段模板是否在三个以上页面出现第二这段模板是否承载了独立的业务语义比如订单卡和列表行两个都符合才拆否则宁可先留着。这个原则帮我避免了不少过度设计造成的麻烦。第三个感受是服务端组件的交互边界要清楚。Razor 组件适合渲染内容但它不适合做复杂的交互驱动拖拽、复杂状态联动、实时大屏刷新。如果一个页面需要高交互性用 Razor 做骨架 局部用 Vue/React 挂载做交互区是一种更好的混合方案。这不代表 Razor 能力不足而是它被设计用来解决另一类问题。如果你正好在一个老的 MVC 项目上做改造或者正要搭建一个新后台系统我建议你从今天提到的 Partial View 和 ViewComponent 开始把重复率最高的两三块模板抽出来封装一遍用几天时间感受一下这套组合拳带来的变化。真正用顺手之后你会发现很多以前觉得前端框架必须干的活儿其实在服务端用 Razor 也能体面地完成。
返回列表