The website has returned from Blazor to Razor Pages. This article explains the current architecture and design of the website from the perspective of source code.
1. Why return from Blazor to Razor Pages
The front desk of the website used Blazor static SSR development before. As we thought deeply about content display websites, we thought that Razor Pages might be more suitable for such scenarios.
2. Website project structure
源码仓库:CodeWF,采用前后台分离架构:
CodeWF/src/
├── WebApp/ # 前台站点(Razor Pages)
│ ├── Pages/ # 页面文件
│ ├── Components/ # View Components
│ ├── Controllers/ # API控制器
│ └── wwwroot/ # 静态资源
│
└── CodeWF/ # 核心类库
├── Models/ # 数据模型
├── Services/ # 业务服务
└── Extensions/ # 扩展方法
3. Razor Pages core implementation
3.1 Page Model
Take the tool page as an example to show a typical PageModel writing method:
// Pages/Tool/Index.cshtml.cs
namespace WebApp.Pages.Tool;
public class IndexModel : PageModel
{
private readonly AppService _appService;
public List<ToolItem>? Tools { get; set; }
public IndexModel(AppService appService)
{
_appService = appService;
}
public async Task OnGetAsync()
{
Tools = await _appService.GetAllToolItemsAsync();
}
}
对应视图文件 Pages/Tool/Index.cshtml:
@page
@model WebApp.Pages.Tool.IndexModel
@{
ViewData["Title"] = "工具";
}
<div class="container">
<h1 class="mb-4">在线工具</h1>
<div class="row">
@foreach (var tool in Model.Tools)
{
<div class="col-md-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h4 class="card-title">@tool.Name</h4>
@if (!string.IsNullOrEmpty(tool.Memo))
{
<p class="card-text text-muted">@tool.Memo</p>
}
@if (tool.Children != null && tool.Children.Any())
{
<ul class="list-group list-group-flush mt-3">
@foreach (var child in tool.Children)
{
<li class="list-group-item">
<a href="@child.Slug">@child.Name</a>
</li>
}
</ul>
}
</div>
</div>
</div>
}
</div>
</div>
3.2 Routing parameter binding
The article details page uses routing template syntax:
@page "/{year:int}/{month:int}/{slug}"
@model WebApp.Pages.Blog.Post.IndexModel
对应代码后端 Post/Index.cshtml.cs:
public class IndexModel : PageModel
{
public BlogPost? Post { get; set; }
public async Task OnGetAsync(int year, int month, string slug)
{
Post = await _appService.GetPostBySlug(slug);
}
}
3.3 Shared Layout
The Layout file defines the global page structure:
@inject CodeWF.Services.AppService AppService
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - 码坊</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-lg navbar-dark bg-tech-nav fixed-top">
<!-- 导航栏内容 -->
</nav>
</header>
<div style="padding-top: 80px;">
@RenderBody()
</div>
@await Component.InvokeAsync("FriendLink")
<footer>
<!-- 页脚内容 -->
</footer>
</body>
</html>
4. Service layer design
4.1 AppService Core Services
AppService.cs 是整个应用的核心服务类:
public class AppService(IOptions<SiteOption> siteOption)
{
private List<BlogPost>? _blogPosts;
private List<ToolItem>? _toolItems;
private List<DocItem>? _docItems;
private List<CategoryItem>? _categoryItems;
private List<AlbumItem>? _albumItems;
public async Task<List<BlogPost>?> GetAllBlogPostsAsync()
{
if (_blogPosts?.Any() == true) return _blogPosts;
_blogPosts = [];
var endYear = DateTime.Now.Year;
for (var start = siteOption.Value.StartYear; start <= endYear; start++)
{
var postDir = Path.Combine(siteOption.Value.LocalAssetsDir, start.ToString());
if (!Directory.Exists(postDir)) continue;
var postFiles = Directory.GetFiles(postDir, "*.md", SearchOption.AllDirectories);
foreach (var postFile in postFiles)
{
var blogPost = await ReadBlogPostAsync(postFile);
if (!blogPost.Draft)
{
_blogPosts.Add(blogPost);
}
}
}
_blogPosts = _blogPosts
.OrderByDescending(post => post.Lastmod ?? post.Date ?? DateTime.MinValue)
.ThenByDescending(post => post.Date ?? DateTime.MinValue)
.ToList();
return _blogPosts;
}
public async Task SeedAsync()
{
// 启动时预加载所有数据
await GetAllAlbumItemsAsync();
await GetAllCategoryItemsAsync();
await GetAllBlogPostsAsync();
await GetAllFriendLinkItemsAsync();
await GetAllDocItemsAsync();
await GetAllToolItemsAsync();
}
}
4.2 data model
// BlogPost.cs
public class BlogPostBrief
{
public string? Title { get; set; }
public string? Slug { get; set; }
public string? Description { get; set; }
public DateTime? Date { get; set; }
public DateTime? Lastmod { get; set; }
public string? Author { get; set; }
public string? Cover { get; set; }
public List<string>? Categories { get; set; }
public List<string>? Tags { get; set; }
}
public class BlogPost : BlogPostBrief
{
public string? Content { get; set; } // 原始Markdown
public string? HtmlContent { get; set; } // 转换后的HTML
}
5. Page request processing flow
- 用户请求
/timestamp - 路由系统匹配到
@page "/timestamp" - 执行
OnGetAsync()方法(若存在) - 通过
AppService获取业务数据 - Render the view and return HTML
6. Program.cs Configuration
var builder = WebApplication.CreateBuilder(args);
// 添加Razor Pages服务
builder.Services.AddRazorPages();
builder.Services.AddControllers();
builder.Services.AddHttpClient();
// 依赖注入AppService
builder.Services.AddSingleton<AppService>();
builder.Services.Configure<SiteOption>(builder.Configuration.GetSection("Site"));
var app = builder.Build();
// 启动时预加载数据
using (var serviceScope = app.Services.CreateScope())
{
var service = serviceScope.ServiceProvider.GetRequiredService<AppService>();
await service.SeedAsync();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
7. Online tool example: Timestamp conversion
以 Timestamp.cshtml 为例展示前端交互:
@page "/timestamp"
@{ ViewData["Title"] = "时间戳转换工具"; }
<div class="container">
<h1 class="mb-4">时间戳转换工具</h1>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">时间戳 → 日期</h5>
<div class="row mb-3">
<div class="col d-flex align-items-center gap-2">
<span>时间戳:</span>
<input type="text" class="form-control" style="width: 200px;"
id="inputTimestamp" placeholder="输入时间戳" />
<select class="form-select" style="width: 100px;" id="timestampUnit">
<option value="s">秒</option>
<option value="ms">毫秒</option>
</select>
<button class="btn btn-primary btn-sm"
onclick="convertTimestampToDate()">转换为日期</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function convertTimestampToDate() {
const timestamp = document.getElementById('inputTimestamp').value;
const unit = document.getElementById('timestampUnit').value;
const ms = unit === 's' ? timestamp * 1000 : parseInt(timestamp);
const date = new Date(ms);
document.getElementById('outputDate').value = date.toLocaleString();
}
</script>
}
8. summary
The source code has been open source. Welcome to exchange and learn.
-
- Warehouse address **: https://www.example.com