AI重構Razor Pages網站完成

AI重構Razor Pages網站完成

從Blazor靜態SSR回歸Razor Pages,原始碼解讀網站架構設計與核心實作

最後更新 2026/4/16 下午11:00
沙漠尽头的狼
預計閱讀 6 分鐘
分類
.NET 前端
標籤
.NET C# ASP.NET Core Blazor Razor Pages

網站又從 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/       # 擴充方法

CodeWF架構圖

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. 頁面請求處理流程

頁面請求處理流程

  1. 使用者請求 /timestamp
  2. 路由系統匹配到 @page "/timestamp"
  3. 執行 OnGetAsync() 方法(若存在)
  4. 透過 AppService 取得業務資料
  5. 渲染檢視並回傳 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

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2025/3/10

挪車二維碼生成工具開發實戰

本文介紹如何開發一個挪車二維碼生成工具,包括C#和Avalonia實現的桌面版以及Blazor前端和.NET Web API實現的線上版,涵蓋需求分析、核心程式碼實作、UI設計和MVVM模式的應用。

繼續閱讀
同分類 / 同標籤 2024/6/20

CodeWF.EventBus:輕量級事件匯流排,讓通訊更流暢

CodeWF.EventBus,一款靈活的事件匯流排庫,實現模組間解耦通訊。支援多種.NET專案類型,如WPF、WinForms、ASP.NET Core等。採用簡潔設計,輕鬆實現命令的發布與訂閱、請求與回應。透過有序的事件處理,確保事件得到妥善處理。簡化您的程式碼,提升系統可維護性。

繼續閱讀
同分類 / 同標籤 2024/1/19

基於 .NET 的 FluentValidation 驗證教學

FluentValidation 是一個基於 .NET 開發的驗證框架,開源免費,而且優雅,支援鏈式操作,易於理解,功能完善,還可與 MVC5、WebApi2 和 ASP.NET CORE 深度整合,組件內提供十幾種常用驗證器,可擴展性好,支援自訂驗證器,支援本地化多語言。

繼續閱讀