網站又從 Blazor 回歸 Razor Pages,本文從原始碼角度解讀網站當前的架構設計。
1. 為什麼從 Blazor 回歸 Razor Pages
網站前台之前使用 Blazor 靜態 SSR 開發,隨著對內容展示類網站的深入思考,認為 Razor Pages 可能更適合這類場景。
2. 網站專案結構
原始碼倉庫:CodeWF,採用前後台分離架構:
CodeWF/src/
├── WebApp/ # 前台站點(Razor Pages)
│ ├── Pages/ # 頁面檔案
│ ├── Components/ # View Components
│ ├── Controllers/ # API控制器
│ └── wwwroot/ # 靜態資源
│
└── CodeWF/ # 核心類別庫
├── Models/ # 資料模型
├── Services/ # 業務服務
└── Extensions/ # 擴充方法
3. Razor Pages 核心實作
3.1 頁面模型(PageModel)
以工具頁面為例,展示典型的 PageModel 寫法:
// 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 路由參數綁定
文章詳情頁使用路由範本語法:
@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 共享佈局(Layout)
Layout 檔案定義全域頁面結構:
@inject CodeWF.Services.AppService AppService
<!DOCTYPE html>
<html lang="zh-TW">
<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. 服務層設計
4.1 AppService 核心服務
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 資料模型
// 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. 頁面請求處理流程
- 使用者請求
/timestamp - 路由系統匹配到
@page "/timestamp" - 執行
OnGetAsync()方法(若存在) - 透過
AppService取得業務資料 - 渲染檢視並回傳 HTML
6. Program.cs 設定
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. 線上工具範例:時間戳記轉換
以 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. 總結
原始碼已開源,歡迎交流學習。
倉庫地址:https://github.com/dotnet9/CodeWF