模板与受控页面:动态内容的两件武器
服务端渲染的最后一块拼图:xrtTemplate 管渲染,page/ 目录管"渲染产物的可见性"。demo 的 /template 与 /page 两个路由就是本章的活样本。
本章导读
19.1 引入
不是所有动态内容都值得上前端框架:管理后台、报告页、邮件渲染天然适合服务端模板。XServer 的答案分工明确——xrtTemplate(32 个 API 的编译型模板引擎)管"怎么渲染",page/ 目录(wwwroot 之外)管"能不能被直接访问":模板与受控页面不暴露在静态层里,只能经你的路由 LoadPage/渲染后输出。
19.2 模板语法与渲染
/* 1. 准备数据(xvalue 树,ch18 的所有权规则)*/
xvalue* pData = xrtValueObject();
xvalue* pList = xrtValueArray();
xrtValueObjectSetNew(pData, XRT_STR_LITERAL("title"),
xrtValueString(XRT_STR_LITERAL("模板页")));
for ( i = 0; i < N; i++ )
xrtValueArrayAppendNew(pList,
xrtValueString(xrtStrView(arrItem[i])));
xrtValueObjectSetNew(pData, XRT_STR_LITERAL("list"), pList);
/* 2. 渲染(LoadTemplate 内部:读文件 → Compile → Render)*/
(void)LoadTemplate(pReq, "page.html", pData);
/* 3. 释放数据树 */
xrtValueRelease(pData);
模板语法(template/page.html 实样):
<h1>{$title}</h1>
<ul>
{#foreach:list}
<li>{$this}</li>
{/foreach}
</ul>
刚才发生了什么:xrtTemplateCompile(xstrview) 把源文本编译为 xtemplate*(不可变、可复用);xrtTemplateRender 以 xvalue 数据渲染出 str。demo 的 LoadTemplate 把"读文件+编译+渲染+输出"打包成一次调用——每请求全链路可见(改模板立即生效,代价是每请求重编译),也可以在 ServiceInit 编译一次缓存(见 19.4)。
19.3 受控页面与防逃逸
page/ 目录的文件不能被 URL 直达(静态层只认 wwwroot)。脚本里的 LoadPage 经 xroot 句柄读取,其防线与第 10 章静态层同源:
- 根锚定:读取只能落在 page/ 目录内
- 拒绝
..、绝对路径、符号链接逃逸 - 文件名来自你的代码而不是请求参数时风险更低——若文件名取自请求,等于把信任边界交给用户(见练习进阶)
19.4 编译缓存策略
两个策略按场景选:
| 策略 | 做法 | 适合 |
|---|---|---|
| 每请求全链路 | demo 现状:LoadTemplate 每次读+编译+渲染 | 开发期(改模板立即生效);低频页面 |
| ServiceInit 缓存 | Init 里 Compile 存 static,请求只 Render | 高频页面;模板随代隔离(换代自动重编译,无陈旧缓存) |
缓存策略与第 5 章"静态变量按代隔离"天然咬合:缓存的 xtemplate* 是代的状态,换代即重建——不存在"改了模板不生效"的陈旧问题(改模板属于内容变化,配合 reload 使用)。
19.5 契约
- 可见性线:URL 直达仅 wwwroot;page/template 经脚本受控读取。
- 所有权线:xtemplate* 由编译者持有(Release 释放);Render 输出的 str 由调用方释放。
- 缓存线:缓存的模板是代的状态——换代重建,勿手工跨代共享。
常见错误
- 症状
- 用户直接下载到原始模板源文件(含 {$} 标记)。
- 原因
- 模板不是静态资源——放 wwwroot 等于公开源码。模板属于 template/。
- 症状
- LoadPage(pReq, sUserInput) —— 即使有根锚定,也在信任边界上打洞(枚举全部页面、探测文件存在性)。
- 原因
- 受控页面的"受控"来自你决定读什么;把决定权交给请求就只剩防逃逸一层防线。
LoadPage(pReq, sQueryPage);/* 白名单映射:请求选键,代码选文件 */编程练习
用 xrtValue 数组 + foreach 渲染一个 20 行的数据表页面。
实现 /page/{name}:name 经白名单映射到 page/ 下的文件,白名单外 404。
/page/../secret、/page/unknown 均 404;白名单页正常输出。给 LoadTemplate 加"Init 期编译缓存"开关,压测两种策略的 RPS 差异。
小结
| 要点 | 结论 |
|---|---|
| 流水线 | 数据树(xvalue)→ Compile → Render → str |
| 语法 | {$var} 插值;{#foreach:list} 循环 |
| 可见性 | wwwroot 可直达;page/template 经脚本受控 |
| 缓存 | 每请求全链路(开发)或 Init 缓存(生产);代隔离防陈旧 |
- xrt 参考 · template — Compile/Render 与渲染配置
- xrt 参考 · html — HTML 转义工具(模板输出安全)