本章导读

xtemplate 把"数据 + 模板 → 文本"标准化:编译/渲染分离——xrtTemplateCompile 把模板解析成不可变的节点树(引用计数,与第 30 章正则同款生命周期),xrtTemplateRender 用值树(第 31 章 xvalue)渲染出拥有式文本。三种插值前缀覆盖三个常见需求:{$path} 直接输出、{%path:fmt} 数字格式化(格式串即第 26 章语法)、{&path:fmt} 时间格式化;控制结构(循环/条件)与函数调用在 tour 范例里展开。配置模板、报表头、通知消息的标准用法是:启动编译、高频渲染——解析成本只付一次。本章与第 30 章正则共用同一套两层生命周期的思维模型,学会一个另一个零成本上手。

35.1引入

通知系统的需求(几乎所有线上系统都有这个模块):"亲爱的 {用户名},您的订单 {单号} 已于 {时间} 发货。"——三个字段、三种类型(字符串、数字、时间),还要按渠道变体(短信短版、邮件长版)。——用 xrtFormat 手拼要逐个字段取值、逐个格式化、逐段拼接,字段一多就是面条;用第 25 章构建器好一些,但"模板"仍然散在代码里——改一个措辞要改代码、重新编译、重新部署。

模板引擎把"形状"从代码里抽出来:模板是数据(文件、配置里的字符串),改措辞改模板即可——不发版、不停机;渲染是纯函数——同一模板同一数据永远同一输出,可测可缓存。编译/渲染分离再补上性能一环(这是它与"字符串替换"的本质差距):模板解析(构建节点树、校验路径)只在编译时做一次,渲染只做"取值 + 格式化 + 写出"——高频路径(每请求渲染一个通知)的成本与模板复杂度基本无关。

35.2概念

编译/渲染分离(先记模型再看函数)

1编译xrtTemplateCompile(模板文本) → 不可变节点树(引用计数 Retain/Release)
2数据xvalue 值树(第 31 章)——ObjectSet/SetNew 构造
3渲染xrtTemplateRender(编译对象, 值树, ...) → 拥有式输出
4复用同一编译对象配合不同值树渲染——模板一次、数据万变

编译对象不可变可共享——多线程渲染同一个模板只要各自的值树(渲染只读模板)。这与第 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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/template/core/main.c -lws2_32 -liphlpapixrt handled 42 requests on 2026-04-02

刚才发生了什么。 ① 数据侧用第 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,列表循环与条件分支:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/template/control/main.c -lws2_32 -liphlpapiUsers: Alice,Bob; range: 13

刚才发生了什么。 ① 循环结构遍历值树里的用户数组——每个元素进循环变量、模板段重复渲染;逗号分隔的输出形态由循环体内的模板片段决定。② 条件与范围: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 章的组合章会把这个"接口"思想扩展成完整管线的数据契约。

常见错误

坑 1:每次渲染重新编译(与正则坑 1 同构)

症状:高频通知/报表路径 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);   /* 只走轻路径 */
}
坑 2:把业务计算塞进模板(展示层的越界)

症状:模板越来越长、越来越像另一门编程语言;改业务规则要改模板;模板测试无法覆盖逻辑分支。

原因:模板引擎提供了表达力,团队把数据准备也搬了进去——"展示决策"与"业务计算"的边界失守。

错误写法
<!-- 模板里做业务计算:满减规则、运费、税费全在模板表达式里 -->
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 个条目)

点击查看完整签名、参数约束、返回值与错误说明