模板组合:把变量当作已渲染的 HTML 块
有时一个模板会大到没法伺候。支付方式页面里几百个 <li>、几十条内联的编辑备注、必须波及每个变体的排序调整——到某个点上,一个巨型嵌套模板就从帮手变成了瓶颈。下一步是把它拆成小模板的流水线,用装着已渲染 HTML 的变量把它们接起来。
思维转换
本系列至今,变量装的都是值:品牌名、年份、逗号分隔的功能列表——渲染时代入模板的普通字符串。
本篇的转换又小又有力:变量的值可以是已经解析完的 HTML。不是 "Acme Co.",而是 <h3>Crypto deposits</h3><ul><li>BTC — fastest…</li>…</ul>。解析器不在乎——它只管代入。
这就解锁了组合。页面由一条小模板流水线搭出来:每个小模板渲染成一块 HTML,再由一个只有几行的编排模板拼装。
不组合的样子
<h3>Crypto deposits</h3>
<ul>
<li>{Bitcoin|BTC} — {fastest|the most popular} option, {confirms in 10–60 min|settles within an hour}.</li>
<li>{Ethereum|ETH} — {smart-contract chain|programmable network}, {2–5 min blocks|fast block times}.</li>
/# … 8 more crypto items #/
</ul>
<h3>Fiat deposits</h3>
<ul>
/# … 12 more fiat items, each with editorial notes #/
</ul>
<h3>Deposit and withdrawal limits</h3>
<table>
/# … 20+ rows #/
</table>
这是个 200 行的巨石。加一种币要钻进一条大枚举链里改。排序是手工活。各币种的编辑备注散落全文件。
组合之后
%CryptoSection%
%FiatSection%
%LimitsSection%
三行。每个变量已经装着页面对应部分的完全解析 HTML。这些值来自在编排模板渲染之前运行的流水线。
为什么可行——引擎流水线
语法参考写明了解析顺序;与组合相关的几行是:
- 剔除注释;
- 提取
#set/#def指令; - 合并变量;
- 展开
%var%引用; - 解析枚举
{a|b|c}; - 解析排列
[a|b|c]; - 后处理。
变量在枚举和排列解析之前展开。轮到枚举/排列时,%CryptoSection% 早已被拼装器算好的 HTML 替换。没有特殊语法——变量代换就是字面的字符串替换。
甚至可以跨层混用:外层排列可以打乱预渲染好的小节。
[<sep="\n\n">%CryptoSection%|%FiatSection%|%LimitsSection%]
每个小节先完全解析,然后排列给这些块换序。
三层流水线
模式住在三层里,每层一道加工:
第 1 层——条目模板(按 id)
最小的可复用单元。一条数据一个模板:一种币、一种支付方式、一个套餐档位、一条 FAQ、一个商品 SKU。
/# spintax.crypto_item.btc #/
<li>{Bitcoin|BTC} — {fastest|the most popular} option, {confirms in 10–60 min|settles within an hour}.</li>
/# spintax.crypto_item.eth #/
<li>Ethereum — {smart-contract chain|programmable network}, {2–5 min blocks|fast block times}.</li>
第 2 层——小节模板
给列表包上结构。拼接好的条目用占位变量表示。
/# spintax.section.crypto #/
<h3>{Crypto deposits|Cryptocurrencies accepted}</h3>
<p>{Pick from|We support} the following coins:</p>
<ul>%CryptoItems%</ul>
%CryptoItems% 就是“每个按 id 的条目解析后拼成的一个字符串”。拼装器负责构建它。
第 3 层——编排模板
页面级模板。只引用预渲染的小节变量。
/# spintax.payment_options #/
<h2>{Accepted payment methods|How to pay}</h2>
%CryptoSection%
%FiatSection%
%LimitsSection%
整个编排模板就这么多。编辑规则:改某种币的描述?改一个条目模板。加新币种?丢进一个新条目模板,把 id 加进启用列表。换序?改排序字段,不改模板。
演练——支付方式页面
商户的加密收款是 BTC、USDT、ETH,法币是 Visa、Mastercard、SEPA。三次查询加一小把模板就能产出整页。
在编排模板渲染之前运行的拼装器伪代码:
function buildPaymentVars(merchantId, lang) {
// 1. Pull active items, in display order.
const cryptos = db.query("active cryptos for merchant ordered by sort", merchantId);
const fiats = db.query("active fiats for merchant ordered by sort", merchantId);
// 2. Resolve each per-id template, join the chunks.
const cryptoItems = cryptos
.map(c => parser.process(templates.find(`crypto_item.${c.id}`, lang)))
.join("");
const fiatItems = fiats
.map(f => parser.process(templates.find(`payment_item.${f.id}`, lang)))
.join("");
// 3. Resolve each section template with item placeholders.
const cryptoSection = cryptos.length
? parser.process(templates.find("section.crypto", lang), { CryptoItems: cryptoItems })
: "";
const fiatSection = fiats.length
? parser.process(templates.find("section.fiat", lang), { FiatItems: fiatItems })
: "";
// 4. Limits section is similar; LimitsRows are joined <tr> chunks.
const limitsSection = (cryptos.length || fiats.length)
? parser.process(templates.find("section.limits", lang), { LimitsRows: buildLimitsRows(cryptos, fiats, lang) })
: "";
// 5. Return the variables the orchestrator references.
return {
CryptoSection: cryptoSection,
FiatSection: fiatSection,
LimitsSection: limitsSection,
HasCrypto: cryptos.length ? "1" : "",
HasFiat: fiats.length ? "1" : "",
};
}
随后编排模板连同普通的站点/运行时变量一起收到这些值,做最后一遍渲染。
命名约定
这里约定胜过自由,因为拼装器按 id 找模板。
| 模式 | 示例 |
|---|---|
spintax.<entity>_item.<id> | spintax.crypto_item.btc |
spintax.<entity>_row.<id> | spintax.crypto_row.btc(表格行) |
spintax.section.<key> | spintax.section.crypto |
spintax.<page-name> | spintax.payment_options |
变量遵循同样的形状:
%CryptoItems%、%FiatItems%、%LimitsRows%——拼接好的按 id 块%CryptoSection%、%FiatSection%、%LimitsSection%——解析完的小节%HasCrypto%、%HasFiat%——标志('1'或'')
变量用 PascalCase,ID 用 snake_case,两者都只用 ASCII。
存储是你的事,不是 spintax 的
子模板放哪儿,这套模式都一样好使:
- 数据库表(
templates:id + body + lang) - JSON 文件:
{ "crypto_item.btc": "<li>…</li>", … } - 文件系统:
templates/crypto_item/btc.txt - CMS 里按语言的字段
引擎不需要数据库。它只是把解析好的 HTML 代进变量引用。拼装器是你的代码,用驱动渲染的任何运行时来写:WordPress 插件、Cloudflare Worker、Node 脚本、Postgres 函数——同一个模式。
按 id 的编辑备注
这是杀手级特性。编辑备注跟着数据住,而不是复制进每一页。
/# spintax.payment_item.visa — 3DS warning baked in #/
<li>Visa — {3DS-protected|with 3D Secure} debit and credit cards, {instant deposit|immediate confirmation}.</li>
/# spintax.crypto_item.xrp — destination-tag reminder per coin #/
<li>XRP — fast and {cheap|low-fee}, {do not forget the destination tag|destination tag is required}.</li>
/# spintax.payment_item.qiwi — legacy status per method #/
<li>QIWI — {legacy support|now legacy}, {accepted but discouraged|not recommended for new accounts}.</li>
每个按 id 的模板把细节记录一次。三页、十页、一千页——全都继承正确的提醒。把 QIWI 挪到 “deprecated” 只需改一个模板;所有渲染同时翻转。
不组合的话,这些备注就是散落各页的内联字符串——审计的噩梦,在受监管的行业还是慢性法律风险。
条件兜底
你会看到有作者试图用枚举语法表达条件:
{%HasCrypto%|%HasFiat%||<p>Payment methods coming soon.</p>}
他们指望“两个标志都空时显示兜底”。普通 {a|b|c|d} 枚举的现实:引擎在四个分支里等概率随机挑一个。输出是非确定的,页面上还可能出现 "1" 这个变体。
枚举分支是均匀随机抽取——它们从不看变量。按值选择要用条件前置工序:
{?!HasCrypto?{?!HasFiat?<p>Payment methods coming soon.</p>}}
读作:如果没有加密也没有法币,渲染兜底。条件在随机分支选择器之前解析,所以输出完全由变量决定。
条件 spintax 不支持的复合逻辑留在拼装器里——比较、&&/||、计算值。先算出一个守卫变量,再用 {?Guard?…} 把门。三种形式、真值表、两遍流水线和反模式的细节在专门的条件 spintax 指南里。
什么时候不要组合
组合有开销——要维护三种模板、要接一个拼装器、要组织一层存储。流水线在这些情况下回本:
- 五个以上结构相似的条目;
- 存在按 id 的编辑备注或排序要求;
- 多个页面复用同一批条目;
- 编辑们需要各自独立修改条目。
这些情况跳过组合:
- 整页一共一到三个条目;
- 条目不跨页面重复;
- 未来一年结构不会变;
- 除你之外没人会编辑它。
一次性的关于页或单篇文章,一个自包含模板更快、更干净、更好调试。
常见错误
| 别这样 | 为什么 | 改成这样 |
|---|---|---|
| 给小页面做组合(≤3 条、无编辑差异) | 流水线开销大于收益。 | 保持一个自包含模板。 |
| 用枚举编码条件 | 引擎按随机而非按值挑选;输出非确定。 | 单变量判断用 {?VAR?then|else};复合逻辑在拼装器算好,用 {?Guard?…} 把门。 |
| 把按 id 的细节内联进编排或小节 | 失去“改一处、处处生效”的好处。 | 细节留在按 id 的 _item 模板里。 |
| 条目级和小节级关注点混进一个模板 | 页面一长,重构就痛苦。 | 三层干净分开:条目、小节、编排。 |
| 把排序硬编码进编排模板 | 排序一变,整个目录都要改页面。 | 拼装器按每个条目的排序字段排。 |
| 忘了给空小节短路 | 一个空 <h3> 下面没有 <ul>,就这么上了生产。 | 条目列表为空时拼装器返回 ""。 |
放过渲染页里未解析的 %XxxItems% 残留 | 变量缺失时占位符原样存活。 | QA 工序标记生产 HTML 里任何残留的 %…%。 |
组合检查清单
- 每组重复条目都有自己的按 id 模板。
- 每个小节都有单一的
_section模板,引用条目占位符。 - 编排模板只引用小节变量,不引用条目变量。
- 排序来自数据,不来自模板内容。
- 单变量条件用
{?VAR?…};复合逻辑留在拼装器。绝不用枚举分支。 - 空小节产出
"",不产出零散标记。 - 按 id 的编辑备注不在小节或编排里重复。
- 五个样本在“全部类别为空”“只有加密”“只有法币”“全部类别都有”“单个遗留条目”下都渲染干净。
- 任何渲染里都没有残留的
%…%、{…}或[…]。
本系列到这里告一段落。思维模型、变量、排列、语法,现在再加组合。开始下一篇文章时回到思维模型——这套流程一次比一次快。