- 示例工程
【免费下载链接】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.
本篇指南围绕 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 的一个"服务端渲染外壳"。
运行与验证清单
- 先启动 Exercise 2 的 JSON 后端(例如
dotnet run --project <BackendProject>),确认GET /api/recipes在浏览器或curl下返回 JSON; - 再启动本练习的 Razor Pages 项目(
dotnet run或dotnet watch run),访问http://localhost:<port>/; - 验证链路:列表页能展示后端数据 → 新建表单提交后返回列表并能看到新条目 → 编辑表单能回显并保存 → 分类管理增删改生效;
- 确认页面上没有任何内联脚本或外部 JS 依赖,样式完全来自 Bootstrap 5.2;
- 用
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.
相关推荐
practical-aspnetcore 练习指南:将 Recipe JSON API 后端改造为 gRPC 后端
practical aspnetcore 练习指南:将 Recipe JSON API 后端改造为 gRPC 后端 本文是 practical aspnetco
示例工程practical-aspnetcore 练习指南:用 HTMX 为 JWT 后端构建无刷新 UI(Exercise 7)
practical aspnetcore 练习指南:用 HTMX 为 JWT 后端构建无刷新 UI(Exercise 7) 本指南对应 practical as
示例工程practical-aspnetcore 实战:在 Razor Pages 应用中通过文件夹约定搭建 Area(new-routing-28)
practical aspnetcore 实战:在 Razor Pages 应用中通过文件夹约定搭建 Area(new routing 28) 在 practi
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考