第十八章:模板引擎与视图
一、模板引擎概述
1.1 什么是模板引擎
模板引擎是一种将数据与模板结合生成最终输出的技术。在 Web 开发中,模板引擎广泛应用于以下场景:
- HTML 页面渲染:服务器端生成动态网页
- 邮件内容生成:基于模板生成格式化邮件
- 文档生成:PDF、Word、Excel 等文档的自动生成
- 代码生成:根据模板自动生成代码文件
- 短信/通知内容:动态生成通知消息
1.2 常见模板引擎对比
| 模板引擎 | 语言/平台 | 特点 | 适用场景 |
|---|---|---|---|
| Razor | .NET | 原生支持,功能强大 | MVC 视图、Blazor |
| Scriban | .NET | 轻量、安全、高性能 | 邮件模板、代码生成 |
| Liquid | .NET | 安全沙箱、用户可编辑 | CMS、用户自定义模板 |
| T4 | .NET | 设计时代码生成 | 代码生成器 |
| RazorLight | .NET | 脱离 MVC 使用 Razor | 独立模板渲染 |
1.3 Furion 中的模板引擎支持
Furion 框架提供了开箱即用的模板引擎支持,主要包括:
// Furion 模板引擎核心功能
// 1. 视图引擎 - 基于 Razor 的增强视图
// 2. 字符串模板渲染 - 动态编译和渲染字符串模板
// 3. 模板文件管理 - 统一的模板文件管理方案
二、Razor 视图引擎基础
2.1 Razor 语法基础
Razor 是 ASP.NET Core 的默认视图引擎,使用 .cshtml 文件扩展名。
// Program.cs 中启用 Razor 视图
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(); // MVC + 视图支持
// 或
builder.Services.AddRazorPages(); // Razor Pages 支持
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
2.2 Razor 语法详解
@* Views/Home/Index.cshtml *@
@model HomeViewModel
@{
ViewData["Title"] = "首页";
Layout = "_Layout";
}
<!-- 变量输出 -->
<h1>欢迎, @Model.UserName!</h1>
<!-- 条件判断 -->
@if (Model.IsAdmin)
{
<div class="alert alert-info">
您是管理员,拥有所有权限。
</div>
}
else
{
<div class="alert alert-warning">
普通用户,部分功能受限。
</div>
}
<!-- 循环 -->
<table class="table">
<thead>
<tr>
<th>序号</th>
<th>名称</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.CreatedTime.ToString("yyyy-MM-dd HH:mm")</td>
</tr>
}
</tbody>
</table>
<!-- 表单 -->
<form asp-action="Create" asp-controller="Home" method="post">
<div class="form-group">
<label asp-for="UserName">用户名</label>
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
<!-- 原始 HTML 输出 -->
@Html.Raw(Model.HtmlContent)
<!-- 分部视图 -->
@await Html.PartialAsync("_UserCard", Model.User)
<!-- Section 定义 -->
@section Scripts {
<script src="~/js/home.js"></script>
}
2.3 布局页与分部视图
@* Views/Shared/_Layout.cshtml - 布局页 *@
<!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="~/css/site.css" />
@RenderSection("Styles", required: false)
</head>
<body>
<header>
@await Html.PartialAsync("_Navigation")
</header>
<main class="container">
@RenderBody()
</main>
<footer>
<p>© @DateTime.Now.Year 我的应用</p>
</footer>
<script src="~/js/site.js"></script>
@RenderSection("Scripts", required: false)
</body>
</html>
@* Views/Shared/_UserCard.cshtml - 分部视图 *@
@model UserDto
<div class="card">
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<p class="card-text">
<span>邮箱: @Model.Email</span><br />
<span>部门: @Model.Department</span>
</p>
</div>
</div>
三、Furion 视图引擎增强
3.1 动态编译模板
Furion 支持在运行时动态编译和渲染 Razor 模板字符串:
using Furion.ViewEngine;
/// <summary>
/// 模板渲染服务
/// </summary>
public class TemplateService
{
private readonly IViewEngine _viewEngine;
public TemplateService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 渲染字符串模板
/// </summary>
public async Task<string> RenderStringTemplateAsync()
{
// 简单字符串模板
var template = "你好, @Model.Name! 今天是 @DateTime.Now.ToString(\"yyyy年MM月dd日\")";
var result = await _viewEngine.RunCompileAsync(template, new
{
Name = "张三"
});
return result;
// 输出: 你好, 张三! 今天是 2024年01月15日
}
/// <summary>
/// 渲染复杂模板
/// </summary>
public async Task<string> RenderComplexTemplateAsync()
{
var template = @"
<h1>@Model.Title</h1>
<table>
<tr><th>序号</th><th>名称</th><th>价格</th></tr>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Price.ToString(""C"")</td>
</tr>
}
</table>
<p>总计: @Model.Items.Sum(i => i.Price).ToString(""C"")</p>";
var result = await _viewEngine.RunCompileAsync(template, new
{
Title = "商品列表",
Items = new[]
{
new { Id = 1, Name = "商品A", Price = 99.9m },
new { Id = 2, Name = "商品B", Price = 199.9m },
new { Id = 3, Name = "商品C", Price = 299.9m }
}
});
return result;
}
}
3.2 模板文件渲染
/// <summary>
/// 从文件加载模板并渲染
/// </summary>
public class FileTemplateService
{
private readonly IViewEngine _viewEngine;
public FileTemplateService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 渲染模板文件
/// </summary>
public async Task<string> RenderFromFileAsync(string templateName, object model)
{
// 从 Templates 目录加载模板
var templatePath = Path.Combine(
AppContext.BaseDirectory, "Templates", $"{templateName}.cshtml");
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"模板文件 {templateName} 不存在");
}
var templateContent = await File.ReadAllTextAsync(templatePath);
return await _viewEngine.RunCompileAsync(templateContent, model);
}
/// <summary>
/// 带缓存的模板渲染
/// </summary>
public async Task<string> RenderWithCacheAsync(
string templateName, object model)
{
var templatePath = Path.Combine(
AppContext.BaseDirectory, "Templates", $"{templateName}.cshtml");
var templateContent = await File.ReadAllTextAsync(templatePath);
// 使用模板缓存键,避免重复编译
return await _viewEngine.RunCompileFromCachedAsync(
templateContent, model, cacheFileName: templateName);
}
}
四、邮件模板生成
4.1 HTML 邮件模板
@* Templates/Email/Welcome.cshtml *@
@model WelcomeEmailModel
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: 'Microsoft YaHei', Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px;
text-align: center; border-radius: 5px 5px 0 0; }
.content { padding: 20px; background-color: #f9f9f9; }
.footer { text-align: center; padding: 10px;
color: #888; font-size: 12px; }
.button { background-color: #4CAF50; color: white; padding: 10px 20px;
text-decoration: none; border-radius: 5px; display: inline-block; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>欢迎加入 @Model.AppName</h1>
</div>
<div class="content">
<p>亲爱的 @Model.UserName:</p>
<p>感谢您注册 @Model.AppName,您的账号已创建成功。</p>
<p>账号信息:</p>
<ul>
<li>用户名:@Model.UserName</li>
<li>邮箱:@Model.Email</li>
<li>注册时间:@Model.RegisterTime.ToString("yyyy-MM-dd HH:mm:ss")</li>
</ul>
<p>请点击以下链接激活您的账号:</p>
<p style="text-align: center;">
<a href="@Model.ActivationUrl" class="button">激活账号</a>
</p>
<p>此链接将在 @Model.ExpireHours 小时后失效。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复。</p>
<p>© @DateTime.Now.Year @Model.AppName</p>
</div>
</div>
</body>
</html>
4.2 邮件发送服务
/// <summary>
/// 邮件模板服务
/// </summary>
public class EmailTemplateService
{
private readonly IViewEngine _viewEngine;
private readonly IEmailSender _emailSender;
private readonly ILogger<EmailTemplateService> _logger;
public EmailTemplateService(
IViewEngine viewEngine,
IEmailSender emailSender,
ILogger<EmailTemplateService> logger)
{
_viewEngine = viewEngine;
_emailSender = emailSender;
_logger = logger;
}
/// <summary>
/// 发送欢迎邮件
/// </summary>
public async Task SendWelcomeEmailAsync(User user, string activationToken)
{
var model = new WelcomeEmailModel
{
UserName = user.UserName,
Email = user.Email,
AppName = "我的应用",
RegisterTime = DateTime.Now,
ActivationUrl = $"https://example.com/activate?token={activationToken}",
ExpireHours = 24
};
var templatePath = Path.Combine(
AppContext.BaseDirectory, "Templates", "Email", "Welcome.cshtml");
var template = await File.ReadAllTextAsync(templatePath);
var htmlBody = await _viewEngine.RunCompileAsync(template, model);
await _emailSender.SendEmailAsync(
user.Email,
$"欢迎加入{model.AppName}",
htmlBody,
isHtml: true);
_logger.LogInformation("欢迎邮件已发送至 {Email}", user.Email);
}
/// <summary>
/// 发送密码重置邮件
/// </summary>
public async Task SendPasswordResetEmailAsync(User user, string resetToken)
{
var template = @"
<!DOCTYPE html>
<html>
<body style='font-family: Microsoft YaHei, Arial;'>
<div style='max-width: 600px; margin: 0 auto;'>
<h2>密码重置</h2>
<p>亲爱的 @Model.UserName:</p>
<p>您正在重置密码,验证码为:</p>
<h1 style='color: #e74c3c; text-align: center;
letter-spacing: 5px;'>@Model.Code</h1>
<p>验证码有效期为 @Model.ExpireMinutes 分钟,请勿泄露给他人。</p>
<p style='color: #888; font-size: 12px;'>
如果不是您本人的操作,请忽略此邮件。</p>
</div>
</body>
</html>";
var htmlBody = await _viewEngine.RunCompileAsync(template, new
{
UserName = user.UserName,
Code = resetToken,
ExpireMinutes = 15
});
await _emailSender.SendEmailAsync(
user.Email,
"密码重置验证码",
htmlBody,
isHtml: true);
}
}
/// <summary>
/// 欢迎邮件模型
/// </summary>
public class WelcomeEmailModel
{
public string UserName { get; set; }
public string Email { get; set; }
public string AppName { get; set; }
public DateTime RegisterTime { get; set; }
public string ActivationUrl { get; set; }
public int ExpireHours { get; set; }
}
五、短信模板
5.1 短信模板服务
/// <summary>
/// 短信模板服务
/// </summary>
public class SmsTemplateService
{
private readonly IViewEngine _viewEngine;
/// <summary>
/// 短信模板定义
/// </summary>
private static readonly Dictionary<string, string> Templates = new()
{
["verification"] = "【@Model.AppName】您的验证码为 @Model.Code," +
"@Model.ExpireMinutes 分钟内有效,请勿泄露。",
["order_shipped"] = "【@Model.AppName】您的订单 @Model.OrderNo 已发货," +
"快递公司:@Model.ExpressCompany,单号:@Model.TrackingNo。",
["payment_success"] = "【@Model.AppName】您已成功支付 @Model.Amount 元," +
"订单号:@Model.OrderNo,感谢您的购买。",
["appointment"] = "【@Model.AppName】您有一个预约提醒:@Model.DateTime," +
"地点:@Model.Location,请准时参加。"
};
public SmsTemplateService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 生成短信内容
/// </summary>
public async Task<string> GenerateSmsContentAsync(
string templateName, object model)
{
if (!Templates.TryGetValue(templateName, out var template))
{
throw new ArgumentException($"短信模板 {templateName} 不存在");
}
return await _viewEngine.RunCompileAsync(template, model);
}
/// <summary>
/// 发送验证码短信
/// </summary>
public async Task<string> GenerateVerificationSmsAsync(
string appName, string code, int expireMinutes = 5)
{
return await GenerateSmsContentAsync("verification", new
{
AppName = appName,
Code = code,
ExpireMinutes = expireMinutes
});
}
}
六、PDF 文档生成
6.1 基于模板的 PDF 生成
/// <summary>
/// PDF 文档生成服务(基于 DinkToPdf 或 iTextSharp)
/// </summary>
public class PdfGeneratorService
{
private readonly IViewEngine _viewEngine;
public PdfGeneratorService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 生成发票 PDF
/// </summary>
public async Task<byte[]> GenerateInvoicePdfAsync(InvoiceModel invoice)
{
// 1. 使用模板引擎生成 HTML
var template = await File.ReadAllTextAsync(
Path.Combine(AppContext.BaseDirectory,
"Templates", "Pdf", "Invoice.cshtml"));
var html = await _viewEngine.RunCompileAsync(template, invoice);
// 2. 使用 HTML 转 PDF 工具生成 PDF
var converter = new SynchronizedConverter(new PdfTools());
var doc = new HtmlToPdfDocument
{
GlobalSettings = new GlobalSettings
{
ColorMode = ColorMode.Color,
Orientation = Orientation.Portrait,
PaperSize = PaperKind.A4,
Margins = new MarginSettings { Top = 10, Bottom = 10 }
},
Objects =
{
new ObjectSettings
{
HtmlContent = html,
WebSettings =
{
DefaultEncoding = "utf-8"
}
}
}
};
return converter.Convert(doc);
}
}
/// <summary>
/// 发票模型
/// </summary>
public class InvoiceModel
{
public string InvoiceNo { get; set; }
public DateTime InvoiceDate { get; set; }
public string CompanyName { get; set; }
public string CustomerName { get; set; }
public string CustomerAddress { get; set; }
public List<InvoiceItem> Items { get; set; }
public decimal TotalAmount => Items?.Sum(i => i.Total) ?? 0;
public decimal TaxRate { get; set; } = 0.13m;
public decimal TaxAmount => TotalAmount * TaxRate;
public decimal GrandTotal => TotalAmount + TaxAmount;
}
public class InvoiceItem
{
public string Description { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
6.2 发票 HTML 模板
@* Templates/Pdf/Invoice.cshtml *@
@model InvoiceModel
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: 'SimSun', serif; font-size: 12px; }
.invoice-header { text-align: center; border-bottom: 2px solid #333;
padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { border: 1px solid #333; padding: 8px; text-align: left; }
th { background-color: #f0f0f0; }
.text-right { text-align: right; }
.total-row { font-weight: bold; background-color: #ffd; }
</style>
</head>
<body>
<div class="invoice-header">
<h1>发 票</h1>
<p>发票号码:@Model.InvoiceNo</p>
</div>
<p>开票日期:@Model.InvoiceDate.ToString("yyyy年MM月dd日")</p>
<p>销售方:@Model.CompanyName</p>
<p>购买方:@Model.CustomerName</p>
<p>地址:@Model.CustomerAddress</p>
<table>
<thead>
<tr>
<th>序号</th>
<th>项目描述</th>
<th>数量</th>
<th>单价(元)</th>
<th>金额(元)</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
<tr>
<td>@(i + 1)</td>
<td>@item.Description</td>
<td>@item.Quantity</td>
<td class="text-right">@item.UnitPrice.ToString("N2")</td>
<td class="text-right">@item.Total.ToString("N2")</td>
</tr>
}
<tr class="total-row">
<td colspan="4" class="text-right">小计</td>
<td class="text-right">@Model.TotalAmount.ToString("N2")</td>
</tr>
<tr>
<td colspan="4" class="text-right">
税额(@((Model.TaxRate * 100).ToString("N0"))%)
</td>
<td class="text-right">@Model.TaxAmount.ToString("N2")</td>
</tr>
<tr class="total-row">
<td colspan="4" class="text-right">合计</td>
<td class="text-right">@Model.GrandTotal.ToString("N2")</td>
</tr>
</tbody>
</table>
</body>
</html>
七、报表模板
7.1 数据报表生成
/// <summary>
/// 报表生成服务
/// </summary>
public class ReportService
{
private readonly IViewEngine _viewEngine;
public ReportService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 生成销售月报
/// </summary>
public async Task<string> GenerateMonthlySalesReportAsync(
int year, int month, List<SalesData> salesData)
{
var template = @"
<html>
<head>
<style>
body { font-family: 'Microsoft YaHei'; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.summary { margin: 20px 0; padding: 15px;
background-color: #e8f5e9; border-radius: 5px; }
</style>
</head>
<body>
<h1>@Model.Year 年 @Model.Month 月 销售报表</h1>
<div class='summary'>
<p>总销售额:<strong>@Model.TotalAmount.ToString(""C"")</strong></p>
<p>订单数量:<strong>@Model.OrderCount</strong></p>
<p>平均客单价:<strong>@Model.AverageOrderAmount.ToString(""C"")</strong></p>
</div>
<h2>销售明细</h2>
<table>
<tr>
<th>日期</th>
<th>商品</th>
<th>数量</th>
<th>金额</th>
</tr>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Date.ToString(""MM-dd"")</td>
<td>@item.ProductName</td>
<td>@item.Quantity</td>
<td style='text-align:right'>@item.Amount.ToString(""N2"")</td>
</tr>
}
</table>
<p style='color: #888;'>报表生成时间:@DateTime.Now.ToString(""yyyy-MM-dd HH:mm:ss"")</p>
</body>
</html>";
var model = new
{
Year = year,
Month = month,
Items = salesData,
TotalAmount = salesData.Sum(s => s.Amount),
OrderCount = salesData.Count,
AverageOrderAmount = salesData.Count > 0
? salesData.Average(s => s.Amount) : 0m
};
return await _viewEngine.RunCompileAsync(template, model);
}
}
public class SalesData
{
public DateTime Date { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
八、代码生成器
8.1 基于 Scriban 的代码生成
// 安装 Scriban 包
// dotnet add package Scriban
using Scriban;
/// <summary>
/// 代码生成器服务
/// </summary>
public class CodeGeneratorService
{
/// <summary>
/// 生成实体类代码
/// </summary>
public string GenerateEntityCode(EntityDefinition definition)
{
var template = Template.Parse(@"
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace .Entities
{
/// <summary>
///
/// </summary>
[Table("""")]
public class
{
/// <summary>
///
/// </summary>
[Key]
[MaxLength()]
[Required]
public { get; set; }
}
}");
return template.Render(new
{
Namespace = definition.Namespace,
Description = definition.Description,
TableName = definition.TableName,
ClassName = definition.ClassName,
Properties = definition.Properties
});
}
/// <summary>
/// 生成服务层代码
/// </summary>
public string GenerateServiceCode(EntityDefinition definition)
{
var template = Template.Parse(@"
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
namespace .Services
{
/// <summary>
/// 服务
/// </summary>
[ApiDescriptionSettings(Name = """")]
public class AppService : IDynamicApiController
{
private readonly IRepository<> _repository;
public AppService(IRepository<> repository)
{
_repository = repository;
}
/// <summary>
/// 获取列表
/// </summary>
public async Task<List<>> GetListAsync()
{
return await _repository.AsQueryable().ToListAsync();
}
/// <summary>
/// 获取详情
/// </summary>
public async Task<> GetAsync(int id)
{
return await _repository.FindOrDefaultAsync(id);
}
/// <summary>
/// 新增
/// </summary>
public async Task<> PostAsync( entity)
{
var result = await _repository.InsertAsync(entity);
return result.Entity;
}
/// <summary>
/// 更新
/// </summary>
public async Task PutAsync( entity)
{
await _repository.UpdateAsync(entity);
}
/// <summary>
/// 删除
/// </summary>
public async Task DeleteAsync(int id)
{
await _repository.DeleteByIdAsync(id);
}
}
}");
return template.Render(new
{
Namespace = definition.Namespace,
Description = definition.Description,
ClassName = definition.ClassName
});
}
}
/// <summary>
/// 实体定义模型
/// </summary>
public class EntityDefinition
{
public string Namespace { get; set; }
public string ClassName { get; set; }
public string TableName { get; set; }
public string Description { get; set; }
public List<PropertyDefinition> Properties { get; set; }
}
public class PropertyDefinition
{
public string Name { get; set; }
public string Type { get; set; }
public string Description { get; set; }
public bool IsKey { get; set; }
public bool IsRequired { get; set; }
public int MaxLength { get; set; }
}
8.2 使用代码生成器
// 使用代码生成器的示例
var generator = new CodeGeneratorService();
var definition = new EntityDefinition
{
Namespace = "MyApp",
ClassName = "Product",
TableName = "Products",
Description = "商品",
Properties = new List<PropertyDefinition>
{
new() { Name = "Id", Type = "int", Description = "主键",
IsKey = true },
new() { Name = "Name", Type = "string", Description = "商品名称",
IsRequired = true, MaxLength = 200 },
new() { Name = "Price", Type = "decimal", Description = "价格",
IsRequired = true },
new() { Name = "Description", Type = "string", Description = "描述",
MaxLength = 2000 },
new() { Name = "Stock", Type = "int", Description = "库存数量" },
new() { Name = "CreatedTime", Type = "DateTime", Description = "创建时间" }
}
};
var entityCode = generator.GenerateEntityCode(definition);
var serviceCode = generator.GenerateServiceCode(definition);
// 将生成的代码写入文件
await File.WriteAllTextAsync($"Entities/{definition.ClassName}.cs", entityCode);
await File.WriteAllTextAsync($"Services/{definition.ClassName}AppService.cs", serviceCode);
九、视图组件(ViewComponent)
9.1 创建视图组件
/// <summary>
/// 通知列表视图组件
/// </summary>
public class NotificationListViewComponent : ViewComponent
{
private readonly INotificationService _notificationService;
public NotificationListViewComponent(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task<IViewComponentResult> InvokeAsync(int count = 5)
{
var userId = HttpContext.User.FindFirst("UserId")?.Value;
var notifications = await _notificationService
.GetLatestNotificationsAsync(userId, count);
return View(notifications);
}
}
@* Views/Shared/Components/NotificationList/Default.cshtml *@
@model List<NotificationDto>
<div class="notification-panel">
<h4>最新通知 <span class="badge">@Model.Count</span></h4>
@if (Model.Any())
{
<ul class="list-group">
@foreach (var notification in Model)
{
<li class="list-group-item @(notification.IsRead ? "" : "unread")">
<strong>@notification.Title</strong>
<small class="text-muted">@notification.CreatedTime.ToString("MM-dd HH:mm")</small>
<p>@notification.Content</p>
</li>
}
</ul>
}
else
{
<p class="text-muted">暂无通知</p>
}
</div>
@* 在页面中使用视图组件 *@
@await Component.InvokeAsync("NotificationList", new { count = 10 })
十、前后端分离架构下的模板使用
10.1 模板在前后端分离中的应用场景
在前后端分离架构下,模板引擎主要用于后端生成非视图内容:
| 应用场景 | 说明 | 输出格式 |
|---|---|---|
| 邮件生成 | 注册确认、密码重置、营销邮件 | HTML |
| 报表导出 | 生成可下载的报表文件 | HTML/PDF |
| 通知消息 | 站内通知、推送消息模板 | 纯文本/HTML |
| 代码生成 | 脚手架工具、低代码平台 | 源代码 |
| 文档生成 | 合同、协议、证书 | PDF/Word |
10.2 API 返回渲染后的 HTML
/// <summary>
/// 模板渲染 API
/// </summary>
[ApiDescriptionSettings(Name = "Template")]
public class TemplateAppService : IDynamicApiController
{
private readonly IViewEngine _viewEngine;
public TemplateAppService(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
/// <summary>
/// 预览邮件模板
/// </summary>
[NonUnify]
public async Task<IActionResult> PreviewEmailTemplate(string templateName)
{
var sampleData = GetSampleData(templateName);
var templatePath = Path.Combine(
AppContext.BaseDirectory, "Templates", "Email",
$"{templateName}.cshtml");
var template = await File.ReadAllTextAsync(templatePath);
var html = await _viewEngine.RunCompileAsync(template, sampleData);
return new ContentResult
{
Content = html,
ContentType = "text/html",
StatusCode = 200
};
}
private object GetSampleData(string templateName)
{
return templateName switch
{
"Welcome" => new WelcomeEmailModel
{
UserName = "测试用户",
Email = "test@example.com",
AppName = "示例应用",
RegisterTime = DateTime.Now,
ActivationUrl = "https://example.com/activate?token=sample",
ExpireHours = 24
},
_ => new { }
};
}
}
十一、Blazor 集成概述
11.1 Blazor 基本配置
// Program.cs - Blazor Server 配置
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// 注册 Furion 服务
builder.Services.AddControllers()
.AddDynamicApiControllers();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapControllers(); // 同时支持 API 和 Blazor
app.Run();
11.2 Blazor 组件示例
@* Pages/UserList.razor *@
@page "/users"
@inject HttpClient Http
<h3>用户管理</h3>
@if (users == null)
{
<p>加载中...</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var user in users)
{
<tr>
<td>@user.Id</td>
<td>@user.UserName</td>
<td>@user.Email</td>
<td>
<button class="btn btn-sm btn-primary"
@onclick="() => EditUser(user.Id)">编辑</button>
<button class="btn btn-sm btn-danger"
@onclick="() => DeleteUser(user.Id)">删除</button>
</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<UserDto> users;
protected override async Task OnInitializedAsync()
{
var result = await Http.GetFromJsonAsync<RESTfulResult<List<UserDto>>>(
"/api/user/list");
users = result?.Data;
}
private void EditUser(int id)
{
NavigationManager.NavigateTo($"/users/edit/{id}");
}
private async Task DeleteUser(int id)
{
await Http.DeleteAsync($"/api/user/{id}");
users = users.Where(u => u.Id != id).ToList();
}
}
十二、静态文件服务
12.1 静态文件配置
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 默认静态文件(wwwroot 目录)
app.UseStaticFiles();
// 自定义静态文件目录
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "uploads")),
RequestPath = "/uploads",
// 设置缓存策略
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append(
"Cache-Control", "public, max-age=604800");
}
});
// 启用目录浏览(仅开发环境)
if (app.Environment.IsDevelopment())
{
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "uploads")),
RequestPath = "/uploads"
});
}
12.2 文件 MIME 类型配置
// 自定义 MIME 类型映射
var mimeProvider = new FileExtensionContentTypeProvider();
mimeProvider.Mappings[".dwg"] = "application/acad";
mimeProvider.Mappings[".shp"] = "application/x-shapefile";
mimeProvider.Mappings[".md"] = "text/markdown";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = mimeProvider,
ServeUnknownFileTypes = false // 不提供未知类型的文件
});
十三、模板引擎最佳实践
13.1 最佳实践总结
| 实践项 | 建议 | 说明 |
|---|---|---|
| 模板分类管理 | 按用途分目录存放 | Email/、Pdf/、Sms/、Code/ |
| 模板缓存 | 使用缓存编译结果 | 避免重复编译开销 |
| 模板安全 | 注意 XSS 防护 | 使用 @ 而非 @Html.Raw |
| 模板预编译 | 生产环境预编译 | 提高首次渲染性能 |
| 模板测试 | 编写模板渲染单元测试 | 确保模板正确性 |
| 前后端分离 | 后端模板仅用于非视图场景 | 邮件、报表、文档 |
13.2 模板渲染单元测试
/// <summary>
/// 模板渲染单元测试
/// </summary>
public class TemplateServiceTests
{
private readonly IViewEngine _viewEngine;
public TemplateServiceTests()
{
var services = new ServiceCollection();
services.AddViewEngine();
var provider = services.BuildServiceProvider();
_viewEngine = provider.GetRequiredService<IViewEngine>();
}
[Fact]
public async Task RenderWelcomeEmail_ShouldContainUserName()
{
// Arrange
var template = "你好, @Model.Name!";
var model = new { Name = "张三" };
// Act
var result = await _viewEngine.RunCompileAsync(template, model);
// Assert
Assert.Contains("张三", result);
}
[Fact]
public async Task RenderInvoice_ShouldCalculateTotal()
{
// Arrange
var template = "总计: @Model.Items.Sum(i => i.Price).ToString(\"N2\")";
var model = new
{
Items = new[]
{
new { Price = 100.0m },
new { Price = 200.0m }
}
};
// Act
var result = await _viewEngine.RunCompileAsync(template, model);
// Assert
Assert.Contains("300.00", result);
}
}
通过本章的学习,你已经掌握了 Furion 框架中模板引擎与视图的完整使用方法。无论是传统的 MVC 视图渲染,还是前后端分离架构下的邮件模板、报表生成、代码生成,模板引擎都是不可或缺的工具。在下一章中,我们将学习部署与 DevOps 相关内容。