1.1 Discuz! 模板系统是怎么工作的

> 弄清一条请求怎么变成 HTML,改模板才不用瞎猜。

骨架与血肉:先建立一个比喻

Discuz! 的模板文件既不是纯 HTML,也不是 PHP 文件,它是一份带标记的 HTML 骨架,标记在编译阶段会被替换成真正的数据。可以把它看成一次装修改造:骨架决定这副身体长什么样,血肉是长在骨头上的肌肉,决定它呈现什么内容。

改骨架——换掉 div 层级、换掉整页布局——那叫换模板套系;只换血肉——把 logo 换掉、主色从蓝改成红——那叫换模板风格。这个比方 1.2 节还会再用一次。

从 URL 到 HTML 的完整链路

阶段 谁在干活 产出 观察证据
1. 请求入口 forum.php / home.php / portal.php 确定 CURSCRIPT,加载框架 地址栏文件名变了,行为随之变化
2. 框架初始化 class_core.php 引导 $_G 建数据库连接、判定登录态与权限 $_G['uid']、$_G['adminid'] 有值
3. 业务逻辑 source/app/xxx/module/ 下的类 查数据、组装数组 模板里出现了只有逻辑层才该有的数据
4. 调用渲染 业务代码调用 template() 指定渲染哪个模板文件 页面开始出现完整 HTML
5. 模板编译 source/class/class_template.php 编译成 PHP 缓存到 data/cache/ 缓存目录多出可读 PHP 文件
6. 输出 PHP 执行编译产物 浏览器收到 HTML 页面正常显示

第 3 步和第 4 步的边界,是初学者最该记住的一条:查数据、写判断,都是业务逻辑的活;模板只负责把备好的数据摆到页面上。一旦你在模板里写复杂查询,八成是放错了地方。

第 1 步:入口脚本与 $_G

Discuz! 没有路由分发,所有入口都是站点根目录下的实体文件。用户访问 forum.php 就进 forum.php,访问 portal.php 就进 portal.php。入口脚本做的事只有三件:引入核心、初始化、把控制权交给对应模块。

最常见的渲染调用只有一个参数:

<?php
// 放在 source/app/xxx/module/ 下对应的业务文件里
// 渲染当前套系下的 forum/forumdisplay.htm
template('forum/forumdisplay');

需要指定非默认目录时会用上后面几个参数。X5.0 里 app 自带的模板走 app 内部模板目录,且只支持 .php 格式模板:

<?php
require './source/class/class_core.php';
$discuz = C::app();
$discuz->cachelist = [];
$discuz->init();

require_once appfile('yyy/zzz');   // 调用 /source/app/xxx/yyy/zzz.php
template('tpl', 0, 'source/app/xxx/template');  // 渲染 app 的模板

这段就是加载辅助函数的标准写法:初始化框架、引入某个 app 的文件、再渲染模板。appfile() 和第三个参数指定的模板目录,是 X5.0 相对 X3.5 变化最明显的地方。老代码里找不到对应写法时,先确认自己是不是在照抄 X3 的模板。

第 5 步:模板是怎么变成 PHP 的

模板文件用 XML/HTML 语法,模板标签写在 HTML 注释里。下面是一段真实结构的片段:

<!--{if $thread['subject']}-->
<li class="thread-item">
  <a href="forum.php?mod=viewthread&tid={$thread['tid']}" class="thread-title">
  <!--{$thread['subject']}--></a>
</li>
<!--{/if}-->

class_template.php 逐条扫描,把注释形式的标签翻译成 PHP 控制语句,拼成一个完整的 PHP 文件写进缓存:

模板里写的 编译后大致等价于 说明
{$var} echo $var; 输出变量
<!--{if $a}--> if ($a): 条件分支开始
<!--{loop $arr $k $v}--> foreach ($arr as $k => $v): 循环,可嵌套
<!--{echo $x}--> echo $x; 显式输出
<!--{subtemplate common/header}--> 编译期把另一模板内容合并进来 不是运行时 include
<!--{template common/footer}--> 运行时 include 另一模板 与上一行区别很大
<!--{hook/xxx}--> 调用插件钩子 插件与模板的连接点
<!--{eval echo 1;}--> echo 1; 直接执行 PHP

这张表解释了两个常被搞混的标签:{subtemplate xxx} 与 {template xxx}。前者发生在编译期,被引用的模板内容会拼进当前缓存文件;后者发生在运行时,是一次真正的 include。引用 common/header 这类公共片段,习惯上用 {subtemplate common/header}。

为什么模板里能写 PHP

因为模板的最终产物就是 PHP 文件。{eval ...} 之所以能用,原理不是「模板引擎支持嵌入 PHP」,而是这一段会被原样写进编译产物,等于你亲手往生成的 PHP 里插了一行。

这也顺带解释了两件事:一是模板里能访问 $_G、能调用全局函数,因为它跑在 PHP 全局作用域里;二是模板里能写 PHP 语法,却不该写 PHP 逻辑。业务规则一旦写进模板,就没法被其他入口复用,也没法做成后台配置项——记住这条分界线,后面讲细胞模板封装时会明白它的价值。

为什么不改完缓存看不到效果

模板不是每次访问都重新解析。缓存命中时,系统直接 include 已生成的 PHP 文件,跳过整个解析过程。好处是性能,代价是你手改的模板文件,在缓存更新之前根本没人读。

你看到的现象 真正的原因 该做什么
改了 HTML,页面毫无变化 仍在 include 旧缓存 PHP 更新缓存
只改了一部分文件,另一部分却变了 两批文件对应的缓存新旧不一 更新缓存,让它们重新编译
改了 {lang} 调用,看不到新文案 语言包与模板是两套缓存 一起更新
更新缓存后短暂变慢 下一次访问要现场编译模板 正常现象,多访问几次即恢复

具体怎么更新缓存、开发者模式开关怎么加、怎么用设计者模式查钩子,都在 1.4 节讲。开发阶段建议养成习惯:改完模板先更新缓存,再刷浏览器。

小结与动手清单

压缩成三句话:模板是 HTML 骨架加模板标记,业务逻辑在模板之外准备数据,class_template.php 把它编译成 PHP 缓存到 data/cache/ 执行。你要建立的核心认知是缓存。

打开你的站点,按这个顺序确认一遍:

  1. 进 data/cache/ 目录,随便打开一个模板缓存文件看看——里面是 PHP 代码,不是 HTML。
  2. 访问任意一个 Discuz! 页面,右键查看源代码,对照 template/default/ 下的同名文件,找出对应的 header 片段。
  3. 记住上面那张标签翻译表,1.3 节开始就要动手改文件了。