本章导读
xtemplate 把"数据 + 模板 → 文本"标准化:编译/渲染分离——xrtTemplateCompile 把模板解析成不可变的节点树(引用计数,与第 30 章正则同款生命周期),xrtTemplateRender 用值树(第 31 章 xvalue)渲染出拥有式文本。三种插值前缀覆盖三个常见需求:{$path} 直接输出、{%path:fmt} 数字格式化(格式串即第 26 章语法)、{&path:fmt} 时间格式化;控制结构(循环/条件)与函数调用在 tour 范例里展开。配置模板、报表头、通知消息的标准用法是:启动编译、高频渲染——解析成本只付一次。本章与第 30 章正则共用同一套两层生命周期的思维模型,学会一个另一个零成本上手。
35.1引入
通知系统的需求(几乎所有线上系统都有这个模块):"亲爱的 {用户名},您的订单 {单号} 已于 {时间} 发货。"——三个字段、三种类型(字符串、数字、时间),还要按渠道变体(短信短版、邮件长版)。——用 xrtFormat 手拼要逐个字段取值、逐个格式化、逐段拼接,字段一多就是面条;用第 25 章构建器好一些,但"模板"仍然散在代码里——改一个措辞要改代码、重新编译、重新部署。
模板引擎把"形状"从代码里抽出来:模板是数据(文件、配置里的字符串),改措辞改模板即可——不发版、不停机;渲染是纯函数——同一模板同一数据永远同一输出,可测可缓存。编译/渲染分离再补上性能一环(这是它与"字符串替换"的本质差距):模板解析(构建节点树、校验路径)只在编译时做一次,渲染只做"取值 + 格式化 + 写出"——高频路径(每请求渲染一个通知)的成本与模板复杂度基本无关。
35.2概念
编译/渲染分离(先记模型再看函数)
编译对象不可变可共享——多线程渲染同一个模板只要各自的值树(渲染只读模板)。这与第 30 章正则的两层模型完全同构:重操作(解析)一次、轻操作(渲染)高频。
三种插值前缀(每种一个字符的语义分工)
| 前缀 | 语法 | 语义 |
|---|---|---|
| 直接输出 | {$user.name} | 字符串原样;路径按点号导航值树 |
| 数字格式化 | {%count:,d} | 冒号后是第 26 章格式串(千分位、进制、百分比) |
| 时间格式化 | {&updated:%F} | 时间值按格式串渲染(ISO 日期等) |
路径语法 a.b.c 在值树上导航(嵌套对象的点号取值,与多数模板语言一致)——渲染时按路径取值,取不到(路径不存在或类型不符)按渲染配置处理(报错或空串,显式选择)。类型化格式化是模板与"字符串替换"的分水岭(数据保持类型、展示保持灵活——同一个数字字段,短信模板用紧凑格式、报表模板用千分位,模板说了算):数字与时间的格式化在模板里声明,不是在代码里预格式化成字符串再塞进模板——数据保持类型,展示保持灵活。
控制结构与函数(让形状适应数据)
模板不只是插值:循环(遍历值树数组,每项一个循环变量)、条件(按值真假分支)让"数据形状决定输出结构"成为可能——报表按行循环、通知按有无附件分支。函数调用(tour 范例的 call 形态)把可复用的格式化逻辑注册为模板可调用的单元——大小写转换、复数形式这类"展示逻辑"留在模板侧。控制与函数的边界纪律:模板做展示决策,不做业务计算——模板里出现复杂表达式就该挪回代码层准备数据。
错误定位(模板的编译期防线)
编译错误带行号(tour 范例验证 error-location line=1)——模板里的语法错误在编译期暴露,不是渲染出残缺文本。渲染错误(路径取不到)同样结构化报告——第 4 章错误体系照常生效。
35.3示例
完整程序:编译、值树与三种插值
来自仓库范例 examples/template/core/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pData = xrtValueObject(); /* 渲染数据:一个对象 */ xtemplate* pTemplate; str sOutput; xtime Time; */ if ( (pData == NULL) || !xrtDateTime( 2026, 4, 2, 12, 30, 0, 0, &Time ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("name"), xrtValueString(XRT_STR_LITERAL("xrt")) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("requests"), xrtValueInt(42) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("created"), xrtValueTime(Time) ) ) { xrtValueRelease(pData); return 1; } /* 编译:一次解析成不可变对象;模板语法错在此步报错。 */ pTemplate = xrtTemplateCompile( XRT_STR_LITERAL( "{$name} handled {%requests:,d} requests on {&created:%F}" ) ); if ( pTemplate == NULL ) { xrtValueRelease(pData); return 2; } /* 渲染:NULL 配置 = 默认预算(防失控输出的上限仍生效)。 */ sOutput = xrtTemplateRender(pTemplate, pData, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 3; } puts(sOutput); /* 三个拥有式资源逆序释放。 */ xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 0; }
刚才发生了什么。 ① 数据侧用第 31 章值树构造:ObjectNew + SetNew(String/Int/Time 各类型字段)——模板吃的就是值树,JSON/XLON 解析的产物(第 32/33 章)直接可用,零转换。② {$name} 直接输出字符串;{%count:,d} 数字走第 26 章千分位格式化(42 无分隔符也合法);{&day:%F} 时间渲染成 ISO 日期——三种前缀一次各走一遍。③ Render 返回拥有式文本、xrtFree 释放;编译对象 Release 归还。同一份代码换个值树就是另一个通知——模板与数据的分离在此最直观。
完整程序:控制结构与循环
来自 examples/template/control/main.c,列表循环与条件分支:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pRoot = xrtValueObject(); xvalue* pUsers = xrtValueArray(); xtemplate* pTemplate; str sOutput; */ if ( (pRoot == NULL) || (pUsers == NULL) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Alice")) ) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Bob")) ) || !xrtValueObjectSetTake( pRoot, XRT_STR_LITERAL("users"), &pUsers ) ) { xrtValueRelease(pUsers); xrtValueRelease(pRoot); return 1; } /* 一条模板串演示全部控制语句(见文件头语法速查)。 */ pTemplate = xrtTemplateCompile(XRT_STR_LITERAL( "{#if:users}Users: {#foreach:users}" "{?loop.first::, }{$loop.value}{#end}{#else}No users{#end}; " "range: {#for:1:5}" "{#if:loop.value = 2}{#continue}{#end}" "{#if:loop.value = 4}{#break}{#end}" "{%loop.value}{#end}" )); if ( pTemplate == NULL ) { xrtValueRelease(pRoot); return 2; } sOutput = xrtTemplateRender(pTemplate, pRoot, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 3; } puts(sOutput); xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 0; }
刚才发生了什么。 ① 循环结构遍历值树里的用户数组——每个元素进循环变量、模板段重复渲染;逗号分隔的输出形态由循环体内的模板片段决定。② 条件与范围:range: 13 展示了条件/聚合类结构的输出——控制结构让模板适应数据形状,而不是数据迁就固定模板。③ 把 tour 范例留作进阶阅读:它覆盖文件加载模板、流式渲染输出、函数调用与注册表——四组能力是模板在真实项目里的完全体,输出四行 ok 各对应一组。
35.4契约
- 两层模型:编译对象不可变、引用计数共享;渲染只读模板、高频可并发(各线程各自的值树)。
- 三种前缀:
{$}直接输出 /{%:fmt}第 26 章数字格式串 /{&:fmt}时间格式串。 - 数据即值树:渲染输入是 xvalue——JSON/XLON 产物零转换直用。
- 错误定位:编译错误带行号;渲染错误(路径/类型)结构化报告。
- 边界纪律:模板做展示决策不做业务计算——复杂表达式挪回数据准备层。
- 产物拥有式:渲染输出
str、xrtFree释放;流式渲染可直连 sink。
从示例到工程:模板的三个宿主
通知/消息生成(最常见):启动编译全局模板、事件到来时构造值树、渲染即发——坑 1 的 good 形态就是它的骨架;多语言场景按语言各编译一份、按用户语言选择。报表与页面骨架:数据行循环渲染、表头条件分支——控制结构的主战场;大报表用流式渲染直连 sink(tour 范例的 streamed 形态),百万行不在内存里攒整页。配置驱动 UI 文案:文案模板存配置(改文案不发版)、字段从配置值树取——模板与数据都成了可热更的资产。三宿主共同的演进路径:模板从代码常量 → 配置文件 → 可热更资产,每一步都依赖"编译对象可替换、渲染无状态"的两层设计。
与 JSON/XLON/正则的分工地图
卷四后半的四个模块(外加值树)经常同场,一张分工地图防止混淆。JSON/XLON(第 32/33 章):结构化数据进出——解析到值树、值树序列化出去;值树(第 31 章):中间表示——所有模块的共同货币;正则(第 30 章):从非结构化文本里提取结构(日志字段、配置行)→ 产出值树;模板(本章):从值树生成文本(通知、报表)→ 产出字符串。提取与生成方向相反、互为镜像——第 36 章的组合章会把"提取 → 值树 → 模板生成"串成完整管线。地图上每个模块只做一件事,组合靠值树这个中间货币。
一个设计观:模板也是接口
模板与渲染代码之间有一个隐式接口:模板引用的路径集合与类型。接口变了(字段改名、类型从 int 变 string)而模板没跟上,渲染就在运行时报错。三个习惯把隐式变显式:渲染冒烟测试(最小值树 + 全前缀渲染断言输出)进 CI;模板文件与代码同库同评审;字段变更清单里加一栏"受影响模板"。第 36 章的组合章会把这个"接口"思想扩展成完整管线的数据契约。
常见错误
症状:高频通知/报表路径 CPU 异常;热点在 xrtTemplateCompile——与第 30 章正则坑 1 完全同构。
原因:编译(解析成节点树)是重操作;渲染是轻操作。重操作进了循环。
str render(const xvalue* pData, xstrview Tpl) { xtemplate* T = xrtTemplateCompile(Tpl); /* 每次渲染都编译 */ str s = xrtTemplateRender(T, pData, NULL, NULL); xrtTemplateRelease(T); return s; }
/* 启动编译一次;模板变更走"重编译+原子替换" */ static xtemplate* gTpl; str render(const xvalue* pData) { return xrtTemplateRender(gTpl, pData, NULL, NULL); /* 只走轻路径 */ }
症状:模板越来越长、越来越像另一门编程语言;改业务规则要改模板;模板测试无法覆盖逻辑分支。
原因:模板引擎提供了表达力,团队把数据准备也搬了进去——"展示决策"与"业务计算"的边界失守。
<!-- 模板里做业务计算:满减规则、运费、税费全在模板表达式里 -->
Total: {%cart.subtotal - discount(cart.promo) + shipping(cart.weight) + tax(...):,.2f}<!-- 模板只展示;计算在代码层完成、结果进值树 -->
Total: {%order.total:,.2f}
(order.total 由代码层按业务规则算好再构造值树)编程练习
构造含字符串/数字/时间三个字段的值树,写模板用三种前缀各渲染一个;换一组数据重渲染验证模板复用。
值树装一个用户数组(名字 + 各自订单数),模板循环渲染出"各位 {name}({count}) "的汇总行;空数组时输出"无人下单"——条件与循环的组合。
JSON 配置(第 32 章)定义报表元数据(标题、列、数据路径),模板按配置动态渲染表头与数据行——列数量与列名全部来自配置而非硬编码。验收标准:同一模板+两份不同配置渲染出两张不同的表;列缺失时错误带路径报告;编译只发生一次(多次渲染验证)。
小结
| 知识点 | 速查 |
|---|---|
| 两层模型 | 编译不可变可共享(Retain/Release)/ 渲染只读、可高频并发 |
| 三前缀 | {$path} 直接输出 / {%path:fmt} 数字格式串 / {&path:fmt} 时间格式串 |
| 数据 | 值树直用——JSON/XLON 产物零转换;值树是全模块共同货币 |
| 控制 | 循环遍历数组 / 条件分支 / 函数调用(展示逻辑归模板侧,业务计算归代码) |
| 错误 | 编译错误带行号;渲染错误结构化(路径/类型) |
| 性能纪律 | 编译一次全局共享;热路径只渲染;与正则同款两层模型 |
| 边界 | 展示决策归模板,业务计算归代码层 |
| 三宿主 | 通知消息生成 / 报表页面(流式渲染)/ 配置驱动文案——演进都靠两层设计 |
| 分工地图 | JSON/XLON 进出结构、值树是货币、正则做提取、模板做生成 |
| 接口观 | 模板引用的路径与类型是隐式接口——渲染冒烟测试进 CI 防漂移 |
📖 本章涉及的 API 参考手册:template(51 个条目)value(159 个条目)
点击查看完整签名、参数约束、返回值与错误说明