14.4 模板视图
在页面模板中嵌入动态标记并填充展示数据,用可测试的 HTML 输出承接页面结构。
学习目标
- 能解释模板视图如何把页面结构、展示数据和业务决定分到不同边界
- 能修改 TypeScript 模板渲染代码,使外部输入经过转义并能用快照验证最终 HTML
- 能回答:当模板开始查询数据库或执行业务规则时,为什么应把职责移回应用层
先从一张订单页面开始
想象一家工厂要每天装配一张订单单据。前面的人只负责确认“要装哪一单”和“这单允许做什么”;中间的人把编号、金额和商品清单准备好;最后的人拿一张已经印好版式的纸,把空白处填上。版式不会因为某一单金额不同就重新设计。
如果最后填空的人顺手去仓库查库存、决定是否退款,下一次改规则就得翻遍所有单据。我们需要的是一条清楚的流水线:前面准备事实,最后一站只负责把事实摆成可读页面。
模板视图解决的边界问题
这条流水线对应的 ↡把 HTML 结构和动态标记放在模板中,再用展示数据填充它的 Web 表示模式。,重点不是某个框架的文件后缀,而是三种责任的分离:控制器解释请求,应用层决定业务,页面模板负责把已经决定好的数据呈现出来。
它通常位于“应用服务已经返回展示数据”和“HTTP 响应发出”之间。模板包含固定的 HTML 结构,以及变量、条件和列表等标记;渲染时把数据填进去,生成一次完整的页面。模板可以决定“这一块是否显示”,但不应决定“订单是否能取消”。
先把请求链画出来
对订单详情页,先写出一条可追踪的链:
GET /orders/42 → 页面控制器 → 应用服务 → 展示数据 → HTML 模板 → HTTP 响应
每个箭头都应该能回答一个问题:谁校验输入?谁读取订单?谁判断权限?谁把金额放到标题下?如果最后两个问题都由模板回答,模板就已经越过了表示边界。
为什么不直接在控制器里拼 HTML
控制器里拼字符串看起来很快,但页面结构会和路由、错误处理、数据查询混在一起。加一个商品字段会同时影响业务测试和 HTTP 测试;设计人员也不能独立修改页面。
模板视图把结构移到可阅读的文档中,让控制器只传递展示用数据。代价是多了一个模板运行时和一套标记语法,因此必须把模板渲染当作可测试的边界,而不是“字符串魔法”。
三个核心部件
1. Model 只携带展示事实
页面使用的 ↡只携带页面所需展示数据的中间对象,不拥有查询、授权或状态转换流程。 可以是订单编号、格式化后的金额、商品名称列表和导航状态。它不应该暴露数据库连接,也不应该让模板调用 cancel() 之类的业务方法。
把展示数据单独整理出来还有一个好处:模板测试可以构造固定 Model,不需要启动数据库。Model 的字段变化会让编译器、快照或契约测试尽早提醒我们,而不是等浏览器打开页面才发现空白。
2. 模板描述结构与显示条件
模板文件保留 HTML 的文档形状,动态位置使用标记,例如 {{orderId}} 或 {{#each items}}。简单的条件和循环有助于描述“这组数据如何显示”;越过显示边界的查询、授权和状态转换应回到应用服务。
不要把模板标记误认为完整编程语言。标记越强大,越容易在页面里藏进不可测试的规则;当一个条件需要读取多个聚合、写审计事件或决定事务边界时,它已经不属于模板。
3. 模板引擎负责绑定与转义
↡读取模板标记、查找对应展示数据并生成最终 HTML 的渲染程序。负责把 {{ orderId }} 替换成 42,把商品列表展开成多个节点,并按约定执行
↡把外部输入中的 HTML 特殊字符变成普通文本,避免输入被浏览器当作标签或脚本执行。。
转义不是业务规则,却是输出边界的安全契约。订单备注来自用户输入时,模板引擎应该把尖括号当作文字;只有经过明确审查的可信片段才允许使用特殊 HTML。应用层仍负责判断“备注能否保存”,模板层负责“备注怎样安全显示”。
专属交互:观察一份 Model 如何变成 HTML
先预测:把下面的“注入模板越界”打开,哪一段会先失去可测试性?是数据准备、标记绑定,还是最终响应?再按播放或单步观察责任从左到右的变化。
第 1 / 3 步 · 页面控制器准备展示用 Model
按步观察:数据、标记与输出各自承担什么责任。
1. 控制器准备展示 Model
页面控制器接收请求,应用服务完成查询、授权和状态判断,最后只交给模板一份展示数据。此时模板还没有碰到数据库,也没有机会替订单作决定。
代码实践:把渲染边界写成函数
下面的 TypeScript 草图把原始订单转换成展示 Model。真正的系统还会在应用服务里完成权限与状态检查;这里故意只保留页面需要的字段,使模板无法借助对象引用反向访问持久化层。
type Order = {
id: number;
totalCents: number;
items: Array<{ name: string; quantity: number }>;
};
type OrderViewModel = {
orderId: string;
total: string;
items: Array<{ label: string }>;
};
export function toOrderViewModel(order: Order): OrderViewModel {
return {
orderId: String(order.id),
total: `¥${(order.totalCents / 100).toFixed(2)}`,
items: order.items.map((item) => ({
label: `${item.name} × ${item.quantity}`,
})),
};
}这里的 OrderViewModel 没有 save、cancel 或 repository 字段。模板收到它后只能消费数据,不能把数据库调用藏在一个“方便的属性”后面。
渲染函数的关键不是自己发明一套模板语言,而是明确转义发生在哪里。以下伪实现把文本插值和 HTML 片段区分开:
function renderOrderPage(model: OrderViewModel): string {
const title = escapeHtml(`订单 #${model.orderId}`);
const total = escapeHtml(model.total);
const items = model.items
.map((item) => `<li>${escapeHtml(item.label)}</li>`)
.join("");
return orderTemplate
.replace("{{title}}", title)
.replace("{{total}}", total)
.replace("{{items}}", items);
}生产代码应使用经过审计的模板引擎,而不是依赖多个 replace 的顺序。测试至少要覆盖三件事:正常订单能得到稳定结构;商品名中的特殊字符只作为文本出现;模板不能通过隐式调用改变订单状态。
选择、拒绝与相邻模式
模板视图适合页面结构稳定、展示字段较多、需要让页面协作者直接编辑 HTML 的场景。它把“页面长什么样”放在模板中,比在控制器里拼接字符串更容易评审,也比让 Model 暴露完整领域对象更容易测试。
它并不意味着所有页面都必须共享一份大模板。若多个页面重复导航和页脚,可以进一步考虑两步视图;若同一份结构化数据需要稳定地转换成 HTML、PDF 或邮件,转换视图可能更自然;若请求入口本身需要集中认证和路由,那是前端控制器的责任。
| 评审问题 | 采用模板视图的证据 | 应拒绝的信号 |
|---|---|---|
| 数据边界 | 模板消费窄 Model,查询在应用服务完成 | 模板持有 Repository 或领域对象 |
| 输出安全 | 默认插值转义,可信 HTML 有明确审查 | 用户输入直接拼进 HTML |
| 结构变化 | 页面协作者可修改模板并用快照验收 | 一个模板塞入多个页面的业务分支 |
| 模式选择 | 主要输出是页面,结构与数据需要协作 | 需要多格式转换或独立全局布局 |
常见误区
本章小结
- 模板视图让 HTML 结构和展示数据在渲染边界汇合。
- Model 只携带页面事实,不暴露查询和业务动作。
- 模板引擎负责绑定与转义,业务决定留在应用层。
- 快照与安全输入测试为最终 HTML 提供可复现证据。
- 模板拥有业务工作流时,应拆分职责或更换相邻模式。
练习
练习
问题 1:修改代码。 给 renderOrderPage 增加一个订单备注字段。要求备注中的 < 和 > 仍显示为普通文字,并说明测试应断言什么。
问题 2:诊断边界。 设计人员希望在模板里调用 order.isOverdue(),以便决定是否显示“催款”按钮。你会保留这个调用吗?请指出它可能带来的两个问题。
问题 3:选择方案。 同一份订单数据要稳定输出 HTML 页面、PDF 和邮件正文,且三种输出由不同团队维护。应该继续扩展模板视图,还是评估转换视图?请给出判断证据。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 模板视图
把 HTML 结构和动态标记放在页面模板中,再用展示数据填出最终页面的一种表示方式。
- Model
专门给页面使用的一小份数据,例如订单编号、金额和商品名称;它不负责查库或改变状态。
- 模板引擎
读取模板标记、绑定数据并生成 HTML 的程序,通常也负责默认转义文本。
- 输出转义
把输入里的特殊字符变成普通文字,避免浏览器把它误认为标签或脚本。
前后导航
参考资料
- Martin Fowler:Template View 模式摘要:核对模板标记填充展示数据的模式定义。
- Martin Fowler:Patterns of Enterprise Application Architecture:核对 Web Presentation Patterns 的图书范围与模式关系。
- Pearson:Patterns of Enterprise Application Architecture:交叉核对出版信息与章节目录。