news 2026/10/15 19:53:45

practical-aspnetcore 练习三实战:用 Razor Pages + HttpClient 为菜谱 JSON 后端搭建纯服务端 Web 界面

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
practical-aspnetcore 练习三实战:用 Razor Pages + HttpClient 为菜谱 JSON 后端搭建纯服务端 Web 界面
  • 示例工程

【免费下载链接】practical-aspnetcore

Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

本篇指南围绕 exercises/pathway-1/exercise-3.md 展开,讲解如何在前两个练习(控制台菜谱应用、JSON REST API 后端)的基础上,用Razor Pages实现一个基于Bootstrap 5.2的 HTML 用户界面,并通过HTTP Client(IHttpClientFactory)与既有 JSON 后端通信。读完本文你将掌握:Razor Pages 项目的搭建与路由规则、PageModel与 Handler 方法的用法、IHttpClientFactory的三种注册方式,以及"零 AJAX、零 JavaScript"的纯服务端表单交互模型,可直接照此完成该练习并迁移到自己的项目中。

练习目标与整体架构

Exercise 3 的技术要求非常明确,共五条:

  • 保留你在 Exercise 2 中创建的 JSON 后端(不得重写);
  • 基于 Razor Pages 实现 HTML UI;
  • 使用 HTTP Client 连接 JSON 后端;
  • 采用 Bootstrap 5.2 作为 CSS 框架;
  • 不使用 AJAX 与 JavaScript。

这实际上勾勒出一个经典的三层结构:浏览器 → Razor Pages(服务端渲染)→ HTTP 调用 → JSON Web API → JSON 文件存储。Razor Pages 页面在服务器端完成对后端的请求、取数、拼装 HTML 并整体返回给浏览器;浏览器端不需要任何fetch、XMLHttpRequest或脚本逻辑,所有交互都通过标准的表单 POST / 链接 GET 触发。

注意练习的附注:"This is not a race. Don't take on this exercise until your previous exercises are solidly implemented."——务必确保 Exercise 1(控制台版菜谱管理)与 Exercise 2(JSON REST API 后端)稳定可用后再开始本练习,因为本练习的 UI 完全依赖后端 API 的可用性。

第一步:保留 Exercise 2 的 JSON 后端

Exercise 2(exercise-2.md)要求把菜谱控制台应用拆分为后端(REST API)与控制台客户端两部分:后端负责全部业务逻辑(JSON 读写、新建菜谱等),通过 REST API 暴露;客户端只发 HTTP 请求。本练习不修改后端,只新增一个与之对话的 Web 前端。

仓库中 projects/minimal-api/hello-world/Program.cs 展示了最小 Web API 的极简形态——WebApplication直接以app.Run(...)响应请求。你的菜谱后端在此基础上扩充即可,例如提供:

  • GET /api/recipes—— 列出全部菜谱;
  • GET /api/recipes/{id}—— 获取单个菜谱;
  • POST /api/recipes—— 新建菜谱;
  • PUT /api/recipes/{id}—— 更新菜谱;
  • GET/POST /api/categories—— 管理菜谱分类。

通信格式统一为 JSON(后端序列化/反序列化建议使用System.Text.Json,这也是 Exercise 1 明确要求的库)。本练习的 UI 将消费以上端点,因此请先确认这些端点可以独立用浏览器或curl访问,再继续下一步。

第二步:搭建 Razor Pages 前端项目

项目骨架与依赖

在解决方案中新建一个 Web 项目(例如RecipeWeb),项目文件的核心配置只需Microsoft.NET.Sdk.WebSDK。仓库中的 razor-pages-basic.csproj 是标准范例:

<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>true</ImplicitUsings> </PropertyGroup> </Project>

说明:本仓库的示例当前以net10.0为目标框架;本练习在设计时面向 .NET 6 时代的 API,但 Razor Pages 与IHttpClientFactory的核心用法在 .NET 6 至 .NET 11 各版本中保持一致,可按你的 SDK 环境调整TargetFramework。

注册 Razor Pages 服务

Razor Pages 只需要两行配置。参考 projects/razor-pages/razor-pages-basic/Program.cs 与 projects/razor-pages/hello-world/Program.cs:

var builder = WebApplication.CreateBuilder(); builder.Services.AddRazorPages(); // 注册 Razor Pages 服务 var app = builder.Build(); app.MapRazorPages(); // 映射 Pages 目录下的所有页面 app.Run();

Razor Pages 的默认路由约定

Razor Pages 的页面统一放在项目根目录的Pages/文件夹下,凡带@page指令的.cshtml文件都会自动获得一条路由:/Pages/Index.cshtml对应/,/Pages/AboutUs.cshtml对应/AboutUs。仓库 projects/razor-pages/README.md 对此有明确说明。

若想自定义 URL,可在@page指令中直接写路由模板,每个页面只能有一个@page定义。参考 projects/razor-pages/routing/Pages/About.cshtml 与 projects/razor-pages/routing/Pages/Blog.cshtml:

@page "/AboutOurSchool" <!-- 该页面将通过 /AboutOurSchool 访问 --> @page "/Blog/2018/4/20/itsmybirthday" <!-- 固定路径模板 -->

对于菜谱界面,合理的页面划分是:Pages/Index.cshtml(菜谱列表)、Pages/Recipes/Create.cshtml(新建)、Pages/Recipes/Edit.cshtml(编辑)、Pages/Categories.cshtml(分类管理)。

第三步:通过 IHttpClientFactory 连接 JSON 后端

本练习的核心要求之一是"Connect to the JSON backend using HTTP Client"。直接new HttpClient()会带来 socket 耗尽、DNS 变更不感知等经典问题,因此 ASP.NET Core 推荐通过依赖注入使用IHttpClientFactory。仓库 projects/httpclientfactory/README.md 整理了四种用法,下面逐一说明并给出与本练习的结合方式。

方式一:默认客户端

先注册IHttpClientFactory,再从容器中取出并创建客户端。参考 projects/httpclientfactory/httpclientfactory-1/Program.cs:

var builder = WebApplication.CreateBuilder(); builder.Services.AddHttpClient(); // 注册 IHttpClientFactory var app = builder.Build(); app.Run(async context => { var httpClient = context.RequestServices.GetService<IHttpClientFactory>(); var client = httpClient.CreateClient(); // 默认客户端 var result = await client.GetStringAsync("http://scripting.com/rss.xml"); context.Response.Headers.Append("Content-Type", "application/rss+xml"); await context.Response.WriteAsync(result); }); app.Run();

在你的 Razor Pages 场景中,更规范的做法是把这个逻辑放进PageModel的 Handler 方法(见第五步),而不是app.Run中间件里。

方式二:命名客户端

当页面需要访问多个后端(例如菜谱服务与分类服务)时,可以用AddHttpClient("name", ...)预配置各自的BaseAddress、请求头与超时,再按名取用。参考 projects/httpclientfactory/httpclientfactory-2/Program.cs:

builder.Services.AddHttpClient("recipe", c => { c.BaseAddress = new Uri("http://localhost:5000/"); c.DefaultRequestHeaders.Add("Accept", "application/json"); });

使用时httpClient.CreateClient("recipe")即可拿到这份预配置。

方式三:类型化客户端(推荐)

类型化客户端把HttpClient的配置与业务方法封装进一个类,Razor Pages 的PageModel只需注入这个类,代码最整洁。参考 projects/httpclientfactory/httpclientfactory-3/Program.cs:

var builder = WebApplication.CreateBuilder(); builder.Services.AddTransient<RecipeApiClient>(); builder.Services.AddHttpClient<RecipeApiClient>(); // 类型化客户端 public class RecipeApiClient { readonly HttpClient _client; public RecipeApiClient(HttpClient client) { _client = client; } public Task<string> GetAll() => _client.GetStringAsync("/api/recipes"); }

方式四:基于接口的类型化客户端

若想让PageModel依赖抽象而非具体类(便于测试与替换),可用AddHttpClient<IRecipes, RecipeApiClient>()。参考 projects/httpclientfactory/httpclientfactory-4/Program.cs:

builder.Services.AddTransient<RecipeApiClient>(); builder.Services.AddHttpClient<IRecipes, RecipeApiClient>(); public interface IRecipes { Task<string> GetAll(); } public class RecipeApiClient : IRecipes { readonly HttpClient _client; public RecipeApiClient(HttpClient client) => _client = client; public Task<string> GetAll() => _client.GetStringAsync("/api/recipes"); }

对于本练习,建议采用方式三/方式四:在RecipeApiClient中封装GetRecipes()、GetRecipe(id)、CreateRecipe(...)、UpdateRecipe(...)等方法,JSON 序列化继续使用System.Text.Json(与 Exercise 1、2 保持一致)。

第四步:Razor Pages 的代码模型与 Handler 方法

两种 code-behind 写法

Razor Pages 支持把PageModel写在独立的.cs文件(推荐),也可内联在@functions块中。

分离 code-behind:参考 SeparateCodebehindFile.cshtml 与 SeparateCodebehindFile.cshtml.cs:

// Pages/Recipes/Index.cshtml.cs using Microsoft.AspNetCore.Mvc.RazorPages; public class IndexModel : PageModel { public List<Recipe> Recipes { get; set; } = new(); public async Task OnGetAsync() // GET / 请求 { Recipes = await _api.GetRecipes(); } }
@page @using RecipeWeb.Pages @model IndexModel <h1>@Model.Recipes.Count 道菜谱</h1>

内联 code-behind:参考 InlineCodebehindFile.cshtml,将模型类写在@functions块内,适合页面很小、逻辑很少的场景:

@functions { public class InlineCodebehindFileModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel { public string Title => "Page with inline codebehind file model"; } }

Handler 方法与表单提交

Razor Pages 通过Handler 方法响应请求:OnGet系列处理 GET,OnPost系列处理 POST。仓库 projects/razor-pages/handler/Pages/Index.cshtml 演示了最简的 Get/Post 处理器:

public void OnGet() { Method = this.HttpContext.Request.Method; } public IActionResult OnPost() { Method = this.HttpContext.Request.Method; return Page(); // 返回当前页面重新渲染 }

对应的表单通过asp-page标签助手指定提交目标(同时记得启用表单验证令牌,Razor Pages 模板默认会为<form method="post">注入反伪造令牌):

<form class="form" method="post" asp-page="Index"> <button type="Submit" class="btn btn-primary">Post</button> </form>

对于菜谱界面,Pages/Recipes/Edit.cshtml.cs可这样组织:

public class EditModel : PageModel { [BindProperty] public Recipe Recipe { get; set; } // 表单回填绑定 public async Task OnGetAsync(int id) // 进入编辑页:拉取数据 { Recipe = await _api.GetRecipe(id); } public async Task<IActionResult> OnPostAsync() // 提交编辑:写回后端 { await _api.UpdateRecipe(Recipe); return RedirectToPage("/Recipes/Index"); } }

关键点:所有写操作都通过 Handler 方法转发给 JSON 后端,由后端统一做 JSON 文件读写与业务校验——这正是练习要求"Backend handles all the business logic"(Exercise 2 的原则)在本练习中的延续。表单只负责收集输入,不做任何客户端校验之外的逻辑,因为不允许 JavaScript。

第五步:接入 Bootstrap 5.2 与页面骨架

练习明确指定使用Bootstrap 5.2。最直接的方式是在Pages/下建立共享的_Layout.cshtml(Razor Pages 默认通过_ViewStart.cshtml应用布局),并在<head>中引入 Bootstrap 5.2 的 CDN 样式。仓库示例(如 razor-pages-basic 的 Index.cshtml)使用的是 Bootstrap 5.1.3 CDN,按练习要求替换为 5.2 版本即可:

<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2/dist/css/bootstrap.min.css" rel="stylesheet"> <title>菜谱管理</title> </head> <body> <div class="container"> @RenderBody() </div> </body> </html>

说明:Bootstrap 5.2 的 CDN 链接格式与 5.1 一致,只需调整版本号路径;若你的环境无法访问公网 CDN,也可将 bootstrap.css 下载后放入项目的wwwroot目录,通过<link href="~/css/bootstrap.min.css" ...>引用(~由 Razor 自动解析到wwwroot)。

之后页面中的表格、按钮、导航直接使用 Bootstrap 的.table、.btn btn-primary、.navbar等类名,例如菜谱列表:

<table class="table table-striped"> <thead> <tr><th>标题</th><th>分类</th><th>操作</th></tr> </thead> <tbody> @foreach (var r in Model.Recipes) { <tr> <td>@r.Title</td> <td>@string.Join(", ", r.Categories)</td> <td><a class="btn btn-sm btn-secondary" asp-page="/Recipes/Edit" asp-route-id="@r.Id">编辑</a></td> </tr> } </tbody> </table>

第六步:坚持"零 AJAX、零 JavaScript"的交互模型

这是本练习最需要自律的一条约束。它意味着:

  • 页面的跳转、表单提交全部使用原生<a href>与<form method="post">;
  • 页面渲染所需的全部数据都应在OnGet/OnPostHandler 中提前从后端取好,通过Model输出到 HTML;
  • 不得引入任何fetch、XMLHttpRequest、<script>逻辑或前端框架来做局部刷新。

每完成一次交互,浏览器都会向 Razor Pages 发起一次完整请求,页面在服务端重新组装后整页返回。这个模型的好处是:实现简单、可访问性好、易于调试与缓存,且与后端 API 的边界非常清晰——Razor Pages 只是 JSON API 的一个"服务端渲染外壳"。

运行与验证清单

  1. 先启动 Exercise 2 的 JSON 后端(例如dotnet run --project <BackendProject>),确认GET /api/recipes在浏览器或curl下返回 JSON;
  2. 再启动本练习的 Razor Pages 项目(dotnet run或dotnet watch run),访问http://localhost:<port>/;
  3. 验证链路:列表页能展示后端数据 → 新建表单提交后返回列表并能看到新条目 → 编辑表单能回显并保存 → 分类管理增删改生效;
  4. 确认页面上没有任何内联脚本或外部 JS 依赖,样式完全来自 Bootstrap 5.2;
  5. 用dotnetCLI 或浏览器开发者工具确认页面发出的请求只有GET/POST(即 HTML 表单与链接),没有fetch请求。

如果列表、新建、编辑、分类管理都能走通,说明你已把"REST API 后端 + 服务端渲染 UI"这条链路完整打通,可以进入 Exercise 4(基于 Alpine.js + Fetch API 的单页界面)或继续巩固本练习。

仓库参考资源汇总

  • 练习原文与相邻练习:exercise-3.md、exercise-1.md、exercise-2.md、exercise-4.md
  • Razor Pages 入门:razor-pages-basic(两种 code-behind)、handler(Get/Post Handler)、routing(@page自定义路由)、razor-pages 总览
  • HTTP Client:httpclientfactory-1(默认客户端)、httpclientfactory-2(命名客户端)、httpclientfactory-3(类型化客户端)、httpclientfactory-4(接口类型化客户端)、总览
  • 最小 Web API 后端参考:hello-world 及 minimal-api 总览
  • 示例工程

【免费下载链接】practical-aspnetcore

Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 19:51:06

MetaHuman买资产踩坑实录:从下载到能用的关键关卡

MetaHuman 买资产踩坑实录&#xff1a;从下载到能用&#xff0c;这几关必须趟过去搞虚拟人项目绕不开 MetaHuman&#xff0c;这套工具链确实强&#xff0c;但也谈不上省心。我最近做一个仿生数字人项目&#xff0c;需要在虚幻引擎里用一套相对完整的超写实角色资产&#xff0c;…

作者头像 李华
网站建设 2026/10/15 19:50:50

Qt XML配置管理实战:QDomElement核心API与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 19:50:43

文本匹配中的双塔模型:从原理到工程实践全解析

文本匹配里那组“双塔”&#xff0c;其实水很深。别急着反驳&#xff0c;我知道一提到双塔&#xff0c;很多人第一反应就是推荐系统里的用户向量和物品向量。但放到自然语言处理里&#xff0c;双塔结构的另一个名字&#xff0c;就是今天要聊的“表示型模型”。它干的事情很纯粹…

作者头像 李华
网站建设 2026/10/15 19:50:33

Apollo-11制导计算机源码复现:从环境搭建到雷达中断调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 19:49:32

HDU 2059龟兔赛跑:动态规划多阶段决策优化实战

1. 项目概述&#xff1a;一道经典动态规划题的实战拆解“HDU 2059 龟兔赛跑”不是某个新上线的App&#xff0c;也不是某款网红硬件&#xff0c;而是杭州电子科技大学&#xff08;HDU&#xff09;在线判题系统&#xff08;Online Judge&#xff0c;简称OJ&#xff09;中编号为20…

作者头像 李华