模板组合:把变量当作已渲染的 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。这些值来自在编排模板渲染之前运行的流水线。

为什么可行——引擎流水线

语法参考写明了解析顺序;与组合相关的几行是:

  1. 剔除注释;
  2. 提取 #set / #def 指令;
  3. 合并变量;
  4. 展开 %var% 引用
  5. 解析枚举 {a|b|c}
  6. 解析排列 [a|b|c]
  7. 后处理。

变量在枚举和排列解析之前展开。轮到枚举/排列时,%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 的编辑备注不在小节或编排里重复。
  • 五个样本在“全部类别为空”“只有加密”“只有法币”“全部类别都有”“单个遗留条目”下都渲染干净。
  • 任何渲染里都没有残留的 %…%{…}[…]

本系列到这里告一段落。思维模型、变量、排列、语法,现在再加组合。开始下一篇文章时回到思维模型——这套流程一次比一次快。


继续本系列