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,作用域独立,适合公共头尾。