ThinkPHP框架内置模板引擎语法详解与最佳实践

Mason
Mason
发布时间:2026-09-20 | 3人浏览过
原创

ThinkPHP 的 ThinkTemplate 模板引擎主要解决两件事:把控制器传入的数据转换成 HTML,以及用简洁的标签完成条件判断、循环、模板包含和页面继承。实际开发时并不需要在模板中编写大量 PHP,只要掌握变量输出、if、foreach、volist、include、extend 和 block 这些核心语法,就可以覆盖大多数后台管理系统、内容网站和传统服务端渲染页面的需求。

本文重点讲模板语法本身及其组织方式。不同 ThinkPHP 主版本在视图驱动的安装方式、配置文件位置等方面可能存在差异,因此已有项目应以实际使用版本的配置为准,不要直接照搬其他版本的视图配置。

一、先理解 ThinkPHP 模板是怎样工作的

模板文件并不是独立获取数据库数据的页面。更常见的流程是:控制器或业务层准备数据,把数据传给视图,模板引擎解析模板标签,最终生成浏览器能够接收的 HTML。

例如控制器已经取得一篇文章:

<?php

namespace app\controller;

use app\BaseController;

class Article extends BaseController
{
    public function detail()
    {
        $article = [
            'id'      => 1001,
            'title'   => 'ThinkPHP 模板语法入门',
            'author'  => '张三',
            'status'  => 1,
        ];

        return view('article/detail', [
            'article' => $article,
        ]);
    }
}

模板获得 article 变量之后,只负责显示:

<h1>{$article.title}</h1>
<p>作者:{$article.author}</p>

这种分工非常重要。数据库查询、权限判断、复杂统计等工作应该尽量在模板渲染之前完成,而不是塞进模板。

SVG 配图 1
图 1:ThinkPHP 模板从数据准备到 HTML 输出的基本流程。

二、模板变量输出语法

1. 输出普通变量

控制器传入:

return view('index', [
    'title' => 'ThinkPHP 教程',
]);

模板中使用:

<h1>{$title}</h1>

{$title} 是最常见的变量标签。左花括号和美元符号之间不要随意插入空格,否则可能无法按照模板变量解析。

2. 输出数组元素

假设控制器传入:

$user = [
    'id'   => 10,
    'name' => '小明',
    'age'  => 26,
];

模板中常用点语法读取数组:

<p>用户ID:{$user.id}</p>
<p>用户名:{$user.name}</p>
<p>年龄:{$user.age}</p>

这比在页面里反复书写 PHP 数组访问表达式更简洁,也能让 HTML 结构保持清楚。

3. 设置默认值

页面经常需要处理昵称、简介、备注等可能为空的数据,可以利用过滤器设置兜底内容:

{$user.nickname|default='未设置昵称'}

这样模板的含义很明确:有昵称就输出昵称,否则显示“未设置昵称”。

4. 对变量调用函数

简单的格式化工作可以在模板中完成,例如日期格式化:

{$article.create_time|date='Y-m-d H:i'}

或者直接输出表达式或函数结果:

{:url('article/detail', ['id' => $article.id])}

模板函数适合做日期格式化、URL 生成等轻量展示处理,但不建议用它承载业务计算。如果一个表达式已经长到需要反复阅读才能理解,应当提前在控制器或服务层计算好。

5. 普通文本与 HTML 内容要区分

模板输出的安全策略可能受到项目版本和模板配置影响,因此不要凭印象判断某个变量“肯定已经转义”。项目应该明确统一自己的输出规则。

普通用户名、标题、评论等用户可控文本应该按照文本处理;只有经过可信来源或专门 HTML 白名单过滤的富文本,才适合考虑使用原样输出:

{$article.content|raw}
注意:raw 的含义不是“让内容变安全”,而是允许内容按照 HTML 输出。评论、昵称、搜索关键词以及未经处理的表单数据不能因为“需要显示标签”就直接使用 raw。

三、条件判断:if、elseif 和 else

条件标签适合根据已有状态决定页面展示内容。例如后台文章的 status 为 1 时显示“已发布”,否则显示“草稿”。

{if condition="$article.status == 1"}
    <span class="status">已发布</span>
{else /}
    <span class="status">草稿</span>
{/if}

有三个以上分支时可以加入 elseif:

{if condition="$article.status == 1"}
    已发布
{elseif condition="$article.status == 2" /}
    待审核
{else /}
    草稿
{/if}

条件标签最适合处理“显示或不显示”“显示哪一种文案”“选择哪一个 CSS 类”等展示逻辑。

如果页面里出现下面这种需求:

  • 判断当前用户拥有哪几个权限;
  • 组合订单状态、支付状态和售后状态;
  • 根据十几个条件计算最终价格;
  • 在模板中查询数据库后再决定显示内容。

问题通常不在 if 标签怎么写,而在于业务逻辑放错位置。应该把最终需要的状态提前算出来,例如传入 can_edit、status_text、status_class 等变量,让模板只负责展示。

四、循环输出:foreach 与 volist 怎么选

列表页面是模板引擎最常见的使用场景。ThinkTemplate 中经常使用 foreach 和 volist 两种循环。

1. foreach:适合普通遍历

{foreach name="articles" item="article" key="key"}
    <article>
        <h3>{$article.title}</h3>
        <p>ID:{$article.id}</p>
    </article>
{/foreach}

name 表示要遍历的模板变量,item 是当前元素,key 用来取得当前数组键。

如果只是把一个数组逐条展示出来,foreach 通常已经足够,而且代码比较直观。

2. volist:适合列表数据展示

{volist name="articles" id="article" key="i"}
    <div class="article-item">
        <span>{$i}</span>
        <a href="{:url('article/detail', ['id' => $article.id])}">
            {$article.title}
        </a>
    </div>
{/volist}

volist 是 ThinkTemplate 中很典型的列表标签,除了基本遍历外,还适合需要序号、截取列表等模板场景。

例如只显示部分数据:

{volist name="articles" id="article" offset="0" length="5"}
    <p>{$article.title}</p>
{/volist}

这里表示从列表开始位置取 5 条用于展示。需要注意,这只是模板展示范围,并不能代替数据库层的分页。如果数据库已经查出了 10 万条数据,再通过模板只显示 5 条,前面的查询开销并没有消失。

SVG 配图 2
图 2:if、foreach 和 volist 分别解决不同类型的模板展示问题。

五、空数据不要靠循环内部处理

文章列表、搜索结果和订单列表都需要考虑数据为空的情况。最清楚的写法通常是先判断列表:

{if condition="!empty($articles)"}

    {foreach name="articles" item="article"}
        <div class="article-item">
            {$article.title}
        </div>
    {/foreach}

{else /}

    <div class="empty">暂无文章</div>

{/if}

这样“有数据”和“无数据”是两个明确分支。不要为了省几行代码,把空状态、首条状态、末条状态、分页状态全部塞进循环内部。

六、switch 适合固定状态映射

当一个变量可能对应若干固定状态时,switch 比连续写很多 if 更容易阅读。例如订单状态:

{switch name="order.status"}

    {case value="0"}
        待付款
    {/case}

    {case value="1"}
        已付款
    {/case}

    {case value="2"}
        已发货
    {/case}

    {default /}
        未知状态

{/switch}

但这里仍有一个边界:如果状态不仅决定文字,还关联权限、按钮、颜色和后续操作,最好在后端把它整理成结构化数据:

$order['status_text']  = '已付款';
$order['status_class'] = 'paid';
$order['can_cancel']   = true;

模板随后只需要读取这些结果,维护起来会简单很多。

七、include:拆分重复的小块模板

一个完整页面经常包含头部、导航、侧栏、分页、页脚等重复区域。最直接的复用方式是 include:

{include file="public/header" /}

<main>
    <h1>{$article.title}</h1>
</main>

{include file="public/footer" /}

适合使用 include 的内容通常具有两个特点:

  • 多个页面重复出现;
  • 它本身是一个相对独立的 HTML 区块。

例如顶部导航单独维护后,修改菜单结构只需要更新公共模板,不必修改几十个页面。

但不要把页面拆得过碎。如果一段 HTML 只有两三行、只被一个页面使用,把它单独做成模板反而增加查找成本。

八、模板继承:extend 与 block

如果网站几十个页面都使用同一套 HTML 骨架,仅靠多个 include 仍然容易重复。此时模板继承更加合适。

1. 创建基础模板

例如建立 view/base.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{block name="title"}默认标题{/block}</title>
</head>
<body>

    {include file="public/header" /}

    <main class="container">
        {block name="content"}
        默认内容
        {/block}
    </main>

    {include file="public/footer" /}

{/block}
</body>
</html>

上面的核心不是具体 HTML,而是两个 block:title 和 content。它们相当于基础模板预留出来的可替换区域。

提示:实际编写基础模板时,只保留真正需要的 block,不要把每一个 HTML 标签都做成区块,否则模板继承会失去简化结构的意义。

2. 子模板继承基础模板

文章详情页可以写成:

{extend name="base" /}

{block name="title"}
{$article.title}
{/block}

{block name="content"}

<article>
    <h1>{$article.title}</h1>
    <p>作者:{$article.author}</p>
    <div class="content">
        {$article.content|raw}
    </div>
</article>

{/block}

子模板不用重新写 html、head、导航和页脚,只覆盖当前页面真正不同的部分。

SVG 配图 3
图 3:include 与模板继承解决的是不同层级的代码复用问题。

九、include 和 extend 不要混为一谈

方式 主要用途 典型场景
include 插入一个独立模板片段 导航、页脚、侧栏、分页、提示框
extend 继承完整页面骨架 后台统一布局、内容站统一详情页结构
block 定义基础模板中允许子模板替换的区域 页面标题、正文、页面专属 CSS 或 JS 区域

一个比较自然的组织方式是:基础模板使用 include 引入公共导航和页脚,再由具体业务页面通过 extend 继承基础模板。

十、一个完整的文章列表模板示例

把前面的知识组合起来,可以得到一个实际可读的文章列表页面。假设控制器已经传入:

return view('article/index', [
    'pageTitle' => '最新文章',
    'articles'  => $articles,
]);

模板可以这样组织:

{extend name="base" /}

{block name="title"}
{$pageTitle}
{/block}

{block name="content"}

<section class="article-list">

    <h1>{$pageTitle}</h1>

    {if condition="!empty($articles)"}

        {volist name="articles" id="article" key="i"}

            <article class="article-item">

                <span class="number">{$i}</span>

                <h2>
                    <a href="{:url('article/detail', ['id' => $article.id])}">
                        {$article.title}
                    </a>
                </h2>

                <p>
                    发布时间:
                    {$article.create_time|date='Y-m-d'}
                </p>

                {if condition="$article.is_top == 1"}
                    <span class="tag">置顶</span>
                {/if}

            </article>

        {/volist}

    {else /}

        <div class="empty">
            暂无文章
        </div>

    {/if}

</section>

{/block}

这个模板已经包含了实际项目中最常见的几个要素:模板继承、变量输出、URL 生成、循环、日期格式化、条件判断和空状态处理。

十一、模板中尽量不要直接写数据库查询

技术上能够在模板中执行某些函数,并不意味着所有代码都适合写在模板里。下面这种思路就不推荐:

<!-- 不推荐:模板自己决定怎样取得业务数据 -->

{php}
    // 查询数据库
    // 计算权限
    // 修改业务状态
{/php}

问题不只是“代码不好看”,还会产生更实际的维护成本:

  • 页面为什么显示这个结果,很难从控制器追踪;
  • 同一模板被循环包含时,容易出现重复查询;
  • 业务逻辑无法方便地复用和测试;
  • 设计人员修改模板时可能意外影响业务行为。

更好的方式是后端准备完整结果:

$articles = $service->getArticleList();

return view('article/index', [
    'articles' => $articles,
]);

模板只循环:

{foreach name="articles" item="article"}
    {$article.title}
{/foreach}

十二、不要让模板里的条件表达式越来越长

下面这样的模板虽然能表达意思,但维护起来已经比较困难:

{if condition="$user.id == $article.user_id
    || ($user.level >= 8 && $article.status == 0)
    || $user.is_admin == 1"}

    <button>编辑</button>

{/if}

模板真正关心的问题只有一个:当前用户是否可以编辑。后端应该直接提供这个结果:

return view('article/detail', [
    'article' => $article,
    'canEdit' => $articleService->canEdit($user, $article),
]);

模板变成:

{if condition="$canEdit"}
    <button>编辑</button>
{/if}

这也是判断模板设计是否合理的一个简单方法:如果模板不得不理解业务规则产生结果的全过程,说明后端传给模板的数据还不够适合展示。

十三、避免在循环内部重复执行昂贵操作

例如文章列表有 30 条记录,每一条都需要作者名称。如果模板循环时再根据作者 ID 调用查询方法,可能形成大量重复请求。

不要把逻辑写成类似:

{volist name="articles" id="article"}

    {$article.title}

    <!-- 不建议在这里再次查询作者 -->

{/volist}

更合理的方式是在查询文章时把作者信息一次准备好,最终得到:

[
    [
        'id'          => 1001,
        'title'       => '文章 A',
        'author_name' => '小明',
    ],
    [
        'id'          => 1002,
        'title'       => '文章 B',
        'author_name' => '小李',
    ],
]

模板随后只负责:

{volist name="articles" id="article"}
    <h2>{$article.title}</h2>
    <p>作者:{$article.author_name}</p>
{/volist}

页面模板因此不会因为展示 30 条记录,就额外触发 30 次作者查询。

十四、模板变量命名应该表达业务含义

模板代码是否容易阅读,很大程度上取决于变量名称。

不太推荐:

{$data.name}
{$data2.title}
{$list2}

更容易理解的是:

{$user.name}
{$article.title}

{foreach name="comments" item="comment"}
    {$comment.content}
{/foreach}

当模板文件半年以后再次打开时,不需要回到控制器才能猜出 $data2 究竟表示什么。

十五、控制模板嵌套层级

标签可以嵌套,但嵌套越深,页面越难维护。例如:

{foreach name="categories" item="category"}

    {if condition="$category.status == 1"}

        {foreach name="category.articles" item="article"}

            {if condition="$article.status == 1"}

                ...

            {/if}

        {/foreach}

    {/if}

{/foreach}

两三层还能理解,如果继续加入权限、标签、评论等逻辑,很快就会形成模板迷宫。

处理方法不是寻找更复杂的模板标签,而是提前整理数据。例如后端只把允许显示的分类和文章传给模板,模板自然可以减少一层甚至两层判断。

十六、模板调试时按照数据流排查

页面变量没有显示时,不要一开始就怀疑模板引擎。按照数据流逐层检查通常更快。

  1. 确认控制器方法是否真的被当前路由调用。
  2. 确认控制器取得的数据是否正确。
  3. 确认传给模板的变量名称与模板使用的名称完全一致。
  4. 确认模板路径是否指向当前实际渲染的文件。
  5. 检查 if 条件是否导致内容根本没有进入输出分支。
  6. 检查模板缓存、运行时缓存以及修改后的模板是否已经重新编译。

例如控制器传的是:

return view('detail', [
    'articleInfo' => $article,
]);

模板却写:

{$article.title}

这不是循环、缓存或者数据库问题,而是模板变量名称不一致。应该改为:

{$articleInfo.title}

十七、ThinkPHP 模板最佳实践整理

掌握模板语法之后,更重要的是限制模板承担的职责。一个容易维护的 ThinkPHP 页面,通常具有下面的关系:

SVG 配图 4
图 4:ThinkPHP 模板开发中最值得长期坚持的五项原则。

可以把最佳实践归纳为几条简单规则:

  • 控制器和业务层负责取得数据、计算状态和权限。
  • 模板主要负责变量输出、简单判断、循环和 HTML 组织。
  • 普通数组遍历优先考虑清晰的 foreach;需要列表特性的场景可使用 volist。
  • 公共小区域使用 include,整套页面骨架使用 extend 与 block。
  • 不要在循环内部执行数据库查询或昂贵函数。
  • 不要让模板里的 if 重新实现后端业务规则。
  • 把用户输入、普通文本和可信富文本区分开,谨慎使用 raw。
  • 变量名称表达实际业务含义,不滥用 data、list2 一类模糊名称。

十八、常见问题

模板中可以直接写 PHP 吗?

模板引擎具备执行 PHP 或表达式的能力并不代表应该大量使用。偶尔处理模板无法自然表达的简单展示逻辑没有必要绝对禁止,但如果页面中出现成片 PHP 代码,通常应该重新检查代码分层。

foreach 和 volist 哪个性能更好?

实际业务中通常没有必要为了微小的模板语法差异做选择。更应该考虑代码是否清楚,以及数据库查询的数据量是否合理。真正容易产生性能问题的通常是一次加载过多数据、循环内部查询数据库或者重复执行昂贵函数。

为什么修改模板后页面没有变化?

先确认修改的是当前真正使用的模板,再检查模板编译缓存和运行时缓存。多应用、多主题或者存在自定义模板目录时,编辑错文件比模板引擎本身出现问题更加常见。

为什么使用 raw 后存在安全风险?

因为 raw 允许字符串中的 HTML 直接参与页面渲染。如果内容中存在恶意脚本或危险属性,浏览器也可能执行这些内容。因此 raw 只解决“是否按 HTML 显示”的问题,不负责判断 HTML 是否安全。

模板里到底应该允许多少逻辑?

可以用一个简单标准判断:只要这段逻辑改变的是“页面怎样呈现”,模板通常可以处理;如果它决定的是“业务规则最终是什么”,一般应该放到模板之外。

例如“状态为 1 时显示绿色标签”属于展示逻辑,可以放在模板中;“当前订单是否允许退款”涉及订单状态、时间、支付方式和权限规则,就应该由业务代码计算完成,再把 can_refund 这样的结果传给模板。

十九、总结

ThinkPHP 的 ThinkTemplate 语法本身并不复杂。日常项目真正需要熟练掌握的核心内容就是 {$变量} 输出、函数过滤、if 条件、foreach 与 volist 循环、include 模板包含,以及 extend + block 模板继承。

真正影响项目长期维护成本的,并不是记住多少模板标签,而是能不能守住模板层的职责边界。控制器和业务层把数据整理成“页面直接可以使用”的状态,模板只负责结构和展示,代码自然会变得清楚。如果一个模板中充满数据库查询、复杂权限判断、长表达式和多层循环,继续学习更多模板语法通常解决不了问题,应该优先重新整理数据和代码分层。

对于大多数 ThinkPHP 服务端渲染项目,可以按照一个简单顺序工作:后端准备完整数据 → 使用基础模板统一页面骨架 → 使用 include 复用局部内容 → 用循环和条件完成展示 → 明确普通文本与富文本的输出边界。做到这些,模板代码通常已经足够清晰、稳定,也更容易继续扩展。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表