2.2 循环、嵌套与模板复用
把数组画成列表,把重复片段收成公共件。
{loop} 的两种写法
{loop} 对应 PHP 的 foreach,是模板里唯一的数据遍历方式,区别只在要不要拿键名:
| 写法 | 参数个数 | 循环体内可用 | 典型场景 |
|---|---|---|---|
{loop $arr $v} |
2 个 | $v |
只要值,比如链接列表 |
{loop $arr $k $v} |
3 个 | $k、$v |
键是标识,比如版块、导航 |
<!-- 只要值 -->
<!--{loop $navlinks $link}-->
<li><a href="{$link[url]}">{$link[name]}</a></li>
<!--{/loop}-->
<!-- 键和值都要 -->
<!--{loop $subnav $k $v}-->
<li id="{$k}">{$v[name]}</li>
<!--{/loop}-->
变量名自己起,但别和外层重名。真实 .htm 文件里每个标签都被 HTML 注释包着,本文中注释符号一律写成 < >。
为什么循环里常要拿键
Discuz! 的数据大量以「键是标识、值是数据」的形式传来——版块列表里键是 fid,导航里键是导航 ID。这类结构循环时把键取出来最省事,最常见的用途是拿键名当 class 或 id,让 CSS 能精确命中某一项:
<ul class="nav">
<!--{loop $nav $k $v}-->
<li{if $k == 'portal'} class="current nav-{$k}"{/if}>
<a href="{$v[url]}">{$v[name]}</a>
</li>
<!--{/loop}-->
</ul>
编译后 {$k} 就是数组键(比如 forum),CSS 里写 .nav-forum 就能命中;{if $k == 'portal'} 则是直接用键判断,避免为每个键写一条 CSS。
嵌套循环:内层变量会覆盖外层
内层 {loop} 可以自由嵌套,但有一条必须记住:内层用的是同一套变量作用域。内层循环如果也叫 $v,跑完后 $v 指向内层最后一项,外层后续使用就全乱了。正确做法是每层用不同的变量名:
<!--{loop $forums $fid $forum}-->
<h3>{$forum[name]}</h3>
<ul>
<!--{loop $forum[subforums] $sfid $subforum}-->
<li>{$subforum[name]}</li>
<!--{/loop}-->
</ul>
<!--{/loop}-->
变量名带上层级语义($forum / $subforum),比 $v1 $v2 好维护得多,这也是官方编码规范里说「宁可写 $threaddata 也不要写 $data1」的原因。要跨层使用外层变量时,先存进一个不会被覆盖的变量:
<!--{loop $threads $tid $thread}-->
{eval $fidname = $thread[forumname];}
<!--{loop $thread[posts] $pid $post}-->
<li>{$fidname} - {$post[message]}</li>
<!--{/loop}-->
<!--{/loop}-->
循环在表格里的写法
表格是循环最典型的场景:表头写一次,<tbody> 里用循环铺行。
<table class="tb">
<thead><tr><th>版块</th><th>主题数</th></tr></thead>
<tbody>
<!--{loop $subforums $fid $forum}-->
<tr{if $fid % 2} class="alt"{/if}>
<td><a href="forum.php?fid={$fid}">{$forum[name]}</a></td>
<td>{$forum[threads]}</td>
</tr>
<!--{/loop}-->
</tbody>
</table>
空态同样要处理——数组为空时给出提示,而不是留一个光秃秃的表格:
<!--{if empty($subforums)}-->
<tr><td colspan="2" class="empty">暂无版块</td></tr>
<!--{else}-->
<!--{loop $subforums $fid $forum}-->...<!--{/loop}-->
<!--{/if}-->
三个细节值得抄下来:循环体只包 <tr>,不包 <table> 和 <tbody>;奇偶行变色用 {if $fid % 2} 加 alt class,比 CSS 里写 :nth-child 稳;循环变量直接拼进 URL,省掉一次 {eval}。
另外 {loop} 遇到空数组、不存在的变量、标量值都不会报错,直接跳过,不必写 is_array() 之类的防御判断。代价是变量名拼错时页面安静地什么都不输出,跟「确实没数据」一样;排查时临时插一行 {eval print_r($varname);} 看结构,看完立刻删掉。
{subtemplate}:编译期合并
{subtemplate xxx/yyy} 把另一个模板文件的内容在编译期合并进当前模板,合并后两份内容变成同一个编译产物,运行时只有一份 HTML。
{subtemplate common/header}
{subtemplate portal/block/list}
路径写法:common/header 指的是 template/{当前套系标识}/common/header.htm,不带 template/ 前缀,也不带扩展名。要引入另一个套系就写全路径,如 {subtemplate mystyle/common/header}。
它的最大好处是运行时零开销,所以 Discuz! 官方明确提示,页面里 {loop} 标签较多时尽量用 {subtemplate} 而不是 {template}。打个比方:{subtemplate} 是把两张纸订成一张,读者只翻一张;{template} 是把两张纸摞起来,翻到第二张时才知道有。
{template}:运行时 include
{template xxx/yyy} 走 PHP 的 include,在运行时把目标模板拉进来执行,公共头尾就这么用:
{template common/footer}
两者的差别集中在四点:
| 对比项 | {subtemplate} |
{template} |
|---|---|---|
| 合并时机 | 编译期 | 运行时 |
| 运行时开销 | 无 | 一次 include |
| 作用域 | 与主模板共享变量 | 独立,同名变量不冲突 |
| 官方建议 | loop 多、复用频繁时优先 | 用于边界清晰的公共片段 |
实践分工:{subtemplate} 用于页面内部的小片段(列表项、单个区块),它们往往要用到外层的循环变量;{template} 用于结构完整的公共文件(header、footer),本就不该和外层变量纠缠。「作用域独立」是 {template} 唯一无可替代的价值。
公共文件该放哪
Discuz! 约定公共模板放在 template/{标识符}/common/ 下,头尾就是 header 和 footer。自定义套系时在自己的 common/ 目录里放同名文件,如 template/你的标识/common/header.htm。
继承时不要复制粘贴默认套系的 header/footer,内核一更新你就得全部重做。正确做法是只写自己真正改动的那几行,用 extend_common.css 做覆盖式扩展——template/{标识}/common/extend_common.css 会在默认 common.css 之后加载。
小结
{loop} 两个参数只要值、三个参数键值都要;嵌套时每层用不同变量名,跨层取值先存进中间变量。表格里循环只包 <tr>,奇偶行用 {if $k % 2} 判。复用上,{subtemplate} 编译期合并、零开销、loop 多时优先;{template} 运行时 include,作用域独立,适合公共头尾。
