第 19 章 / 共 51 章
卷三 · 协议开发

模板与受控页面:动态内容的两件武器

服务端渲染的最后一块拼图:xrtTemplate 管渲染,page/ 目录管"渲染产物的可见性"。demo 的 /template 与 /page 两个路由就是本章的活样本。

本章导读

站位:API 之外的另一条路——服务端出 HTML。本章顺带解决一个安全问题:哪些文件可以被 URL 直达、哪些必须经脚本。读 10 章的读者会在这里重逢"穿越防御"。

19.1 引入

不是所有动态内容都值得上前端框架:管理后台、报告页、邮件渲染天然适合服务端模板。XServer 的答案分工明确——xrtTemplate(32 个 API 的编译型模板引擎)管"怎么渲染",page/ 目录(wwwroot 之外)管"能不能被直接访问":模板与受控页面不暴露在静态层里,只能经你的路由 LoadPage/渲染后输出。

19.2 模板语法与渲染

渲染流水线(demo Handle_Template 真实流程)实机验证出处:demo-single/route_http/basic.h
/* 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 实样):

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/ 目录内
  • 拒绝 ..、绝对路径、符号链接逃逸
  • 文件名来自你的代码而不是请求参数时风险更低——若文件名取自请求,等于把信任边界交给用户(见练习进阶)
URL 直达能力: wwwroot/** ✓ 静态层直接服务 page/** ✗ 只能经 LoadPage(脚本控制) template/** ✗ 只能经 LoadTemplate 渲染 db/** ✗ 文件系统(SQLite)

19.4 编译缓存策略

两个策略按场景选:

策略做法适合
每请求全链路demo 现状:LoadTemplate 每次读+编译+渲染开发期(改模板立即生效);低频页面
ServiceInit 缓存Init 里 Compile 存 static,请求只 Render高频页面;模板随代隔离(换代自动重编译,无陈旧缓存)

缓存策略与第 5 章"静态变量按代隔离"天然咬合:缓存的 xtemplate* 是代的状态,换代即重建——不存在"改了模板不生效"的陈旧问题(改模板属于内容变化,配合 reload 使用)。

19.5 契约

  • 可见性线:URL 直达仅 wwwroot;page/template 经脚本受控读取。
  • 所有权线:xtemplate* 由编译者持有(Release 释放);Render 输出的 str 由调用方释放。
  • 缓存线:缓存的模板是代的状态——换代重建,勿手工跨代共享。

常见错误

把模板放进 wwwroot
症状
用户直接下载到原始模板源文件(含 {$} 标记)。
原因
模板不是静态资源——放 wwwroot 等于公开源码。模板属于 template/。
文件名取自请求参数
症状
LoadPage(pReq, sUserInput) —— 即使有根锚定,也在信任边界上打洞(枚举全部页面、探测文件存在性)。
原因
受控页面的"受控"来自你决定读什么;把决定权交给请求就只剩防逃逸一层防线。
✗ 危险
LoadPage(pReq, sQueryPage);
✓ 正确
/* 白名单映射:请求选键,代码选文件 */

编程练习

基础列表页渲染

用 xrtValue 数组 + foreach 渲染一个 20 行的数据表页面。

验收:页面 20 行;改模板内容立即生效(每请求全链路)。
进阶白名单页面路由

实现 /page/{name}:name 经白名单映射到 page/ 下的文件,白名单外 404。

验收:/page/../secret、/page/unknown 均 404;白名单页正常输出。
挑战模板缓存切换

给 LoadTemplate 加"Init 期编译缓存"开关,压测两种策略的 RPS 差异。

验收:缓存策略 RPS 提升 ≥ 3 倍;reload 后模板更新仍然生效。

小结

要点结论
流水线数据树(xvalue)→ Compile → Render → str
语法{$var} 插值;{#foreach:list} 循环
可见性wwwroot 可直达;page/template 经脚本受控
缓存每请求全链路(开发)或 Init 缓存(生产);代隔离防陈旧
xrt 延伸阅读