[C#] .NET 10 使用 Scriban 動態載入 HTML 模板,執行期修改不用重新發布

  • 18
  • 0

前一篇測試了 RazorEngineCore,主要目的就是希望在 .NET 10 裡面
可以做到像以前 ASPX 一樣,前端頁面可以直接修改,不用每次都重新 Build、Publish、Deploy..

今天測試另一套套件,我個人覺得更合理,而且今年(2026) 還有再更新,所以可能更貼合我的需求
RazorEngineCore 的確可以做到,但是它本質上還是 Razor Runtime 編譯,所以多少還是有一些複雜度
因為他要去改專案檔案,這有一點麻煩..

今天測試的叫做 Scriban 目標就是 /ProdScriban?id=1  這樣可以使用,並且可以在 "執行期" 去改頁面資料
取得商品資料之後,由伺服器端直接輸出完整 HTML
也就是保留 SSR,SEO 一樣可以正常處理,但是前端模板不要再綁在 DLL 裡面

1. 先安裝 Scriban

dotnet add package Scriban

2. 商品 Model 不用改跟上一篇文章,但是我這邊也貼上

namespace TestWebApp.Models;

public sealed class Product
{
    public int Id { get; init; }

    public string Name { get; init; } = string.Empty;

    public decimal Price { get; init; }

    public string Description { get; init; } = string.Empty;

    public string ImageUrl { get; init; } = string.Empty;

    public bool InStock { get; init; }
}

這邊我也附上 DataSet ,畢竟這是測試如果要扯到資料庫有點麻煩,用記憶體資料測試就好

using TestWebApp.Models;

namespace TestWebApp.Data;

public static class DataSet
{
    public static IReadOnlyDictionary<int, Product> Products { get; } =
        new Dictionary<int, Product>
        {
            [1] = new Product
            {
                Id = 1,
                Name = "65W USB-C 快充充電器",
                Price = 990,
                Description = "支援手機、平板及筆電快速充電。",
                ImageUrl = "https://placehold.co/800x600?text=Product+1",
                InStock = true
            },
            [2] = new Product
            {
                Id = 2,
                Name = "藍牙無線喇叭",
                Price = 1680,
                Description = "小型輕量設計,支援藍牙連線。",
                ImageUrl = "https://placehold.co/800x600?text=Product+2",
                InStock = false
            },
            [3] = new Product
            {
                Id = 3,
                Name = "多功能無線充電座",
                Price = 1280,
                Description = "可同時替手機、耳機及智慧手錶充電。",
                ImageUrl = "https://placehold.co/800x600?text=Product+3",
                InStock = true
            }
        };

    public static Product? GetProduct(int id)
    {
        return Products.TryGetValue(id, out var product)
            ? product
            : null;
    }
}

3. 建立 Pages/ProdScriban.html ,這邊就是跟 Razor 語法不同了,邊都是改用 {{ product.name }}
所以完整商品頁面完整程式碼

<!DOCTYPE html>
<html lang="zh-Hant">

<head>
    <meta charset="utf-8">

    <meta name="viewport"
          content="width=device-width, initial-scale=1">

    <title>{{ product.name }}|測試商城</title>

    <meta name="description"
          content="{{ product.description }}">

    <style>
        body {
            margin: 0;
            font-family: Arial, sans-serif;
            background: #f4f4f4;
        }

        .product {
            width: min(900px, calc(100% - 40px));
            margin: 60px auto;
            padding: 30px;
            background: white;
            border-radius: 16px;
        }

        .product img {
            width: 100%;
            max-width: 420px;
        }

        .price {
            margin: 20px 0;
            font-size: 28px;
            font-weight: bold;
        }
    </style>
</head>

<body>

    <main class="product">

        <h1>{{ product.name }}</h1>

        <img src="{{ product.image_url }}"
             alt="{{ product.name }}">

        <p>
            {{ product.description }}
        </p>

        <div class="price">
            NT$ {{ product.price }}
        </div>

        {{ if product.in_stock }}

            <button type="button">
                加入購物車
            </button>

        {{ else }}

            <strong>
                目前缺貨
            </strong>

        {{ end }}

        <hr>

        <small>
            商品編號:{{ product.id }}
        </small>

    </main>

</body>
</html>

這樣子就很方便 AI 去產生前端檔案,只要跟 AI 說好規則就可以當作樣板,可以在執行期處理

4. 這時候跟 RazorEngineCore 不同的就是他不用改 .csproj  ,直接改 Program.cs 中建立商品的路由

app.MapGet(
    "/ProdScriban",
    async (
        int? id,
        IWebHostEnvironment environment,
        CancellationToken cancellationToken) =>
    {
        if (id is null)
        {
            return Results.BadRequest("缺少商品 id。");
        }

        var product = DataSet.GetProduct(id.Value);

        if (product is null)
        {
            return Results.NotFound("找不到商品。");
        }

        var templatePath = Path.Combine(
            environment.ContentRootPath,
            "Pages",
            "ProdScriban.html");

        if (!File.Exists(templatePath))
        {
            return Results.Problem(
                title: "找不到商品模板",
                detail: templatePath);
        }

        var templateContent =
            await File.ReadAllTextAsync(
                templatePath,
                cancellationToken);

        var template = Template.Parse(templateContent);

        if (template.HasErrors)
        {
            return Results.Problem(
                title: "模板解析失敗",
                detail: string.Join(
                    Environment.NewLine,
                    template.Messages));
        }

        var scriptObject = new ScriptObject();

        scriptObject.Add(
            "product",
            product);

        var context = new TemplateContext();

        context.PushGlobal(scriptObject);

        var html =
            await template.RenderAsync(context);

        return Results.Content(
            html,
            "text/html; charset=utf-8");
    });

啟動之後,可以直接使用 /ProdScriban?id=1 讀取到資料,並且你可以動態去修改 Pages/ProdScriban.html

並且就會改變,只要確保那檔案有被 publish 出去就好
 

Scriban 跟 RazorEngineCore 最大的差別,就是它不會直接讓你在模板裡面寫 C#
但是反過來想,這件事情也不一定是缺點

如果核心邏輯本來就放在 .NET 10 裡面,像是商品、訂單、會員、金流、庫存這些都由後端處理,那前端其實只需要:
HTML、CSS、Data Binding 、if 、 for 就夠用了
這樣設計師或 AI Agent 可以自由修改畫面,但是不會去碰到核心程式
而且它不用 Roslyn Runtime Compile,也不用處理 .cshtml 編譯問題
目前測下來,Scriban 反而更接近一個單純的動態 Theme Template
所以這也是一個可以 AI/設計師配合的方式,筆記一下..

 

---

The bug existed in all possible states.
Until I ran the code.