模板开发文档

ZhiCms 发布于 阅读:178 开发动态

ZhiCms 模板开发文档

ZhiCms 系统内置 双模板引擎,二者对外接口一致,可在后台「系统设置 → 模板引擎」随时切换:

引擎 标识 说明
ZhiCms 自研引擎(默认) legacy ZhiCms\base\Template,ZhiCms 自研原生标签语法
ThinkTemplate think ZhiCms\base\ThinkTemplate,基于真·ThinkPHP 模板引擎(think-template),语法与 ThinkPHP 一致

警告:当前主题模板按默认引擎(ZhiCms 自研引擎)语法编写。请勿随意切换引擎,否则前端很可能崩溃(空白页/语法错误/样式错乱)。仅当已用新引擎语法重写全部模板时才可切换。

两种引擎都通过统一的 assign() / display() 接口工作,因此同一套控制器代码无需改动,只需保证模板语法与当前引擎兼容。

切换引擎后请务必到「系统 → 缓存管理」清除模板编译缓存(或删除 data/cache/tpldata/cache/tpl_compile 目录),否则可能命中旧编译结果。


1. 模板目录结构

前台模板位于 app/{模块}/view/{控制器}/ 下,默认模块为 index

app/index/view/
├── public/                 # 公共片段(可被任意页面 include)
│   ├── header.html         # 页头:<head> + 顶部导航 + 头部公共变量
│   ├── footer.html         # 页脚:友情链接 + 版权
│   ├── sidebar.html        # 侧边栏:用户信息 / 登录 / 排行榜等
│   └── pagebar.html        # 分页条
├── index/
│   ├── index.html          # 首页
│   ├── list.html           # 列表页
│   └── view.html           # 文章详情
├── brand/  cheaps/  rank/  search/  m/  ucenter/  ...   # 各功能页面

1.1 一个标准页面的骨架

几乎所有前台页面都以 header / footer 公共片段开始与结束:

{include file="app/index/view/public/header"}

<!-- 页面主体内容 -->
<div class="container">
    ...
</div>

{include file="app/index/view/public/footer"}

header.html 负责输出 <!DOCTYPE html><head>(含 SEO 标题/关键词/描述)、顶部导航与全局 CSS/JS;footer.html 负责输出页脚、友情链接、版权与公共 JS。


2. 控制器与模板的交互

2.1 变量赋值 assign()

public function index(){
    $this->assign('newsList', $list);   // 数组
    $this->assign('title', '示例标题');  // 标量
    $this->display();                   // 渲染默认模板
}

assign($name, $value) 把数据注入模板,模板中直接以 $name 使用。

2.2 渲染模板 display()

$this->display();                                    // 默认:app/index/view/{controller}/{action}.html
$this->display('app/index/view/index/index');        // 指定模板(推荐绝对路径,避免 TPL_PATH 为空时报错)
$this->display('', false, false);                    // $isTpl=false 时 $tpl 为模板字符串

display() 会自动把控制器所有 public 属性$this->xxx)一并注入模板。因此前台控制器最常见的写法是直接给属性赋值,无需逐个 assign()

public function view(){
    $this->view = $data;        // 详情数据
    $this->pageTitle = '标题';   // SEO 标题
    $this->canonicalUrl = url(...);
    $this->display();
}

2.3 布局(layout)

控制器设置 $this->layout 后,页面内容会套用到该布局中:

public $layout = 'app/index/view/public/layout';     // 所有输出都进入该布局

ThinkTemplate 引擎还额外支持 {extend name="base"}...{block}...{/block} 继承语法。

2.4 控制器输出数据到模板(完整流程)

一个「从数据库取数 → 赋值 → 模板渲染」的典型控制器方法:

public function list(){
    $where = array("1");                       // 查询条件(数组元素会以 AND 拼接)
    $where[] = "`status` = '1'";               // 追加条件

    // 分页:第 1 参每页条数,返回 ['list'=>..,'count'=>..,'page'=>html]
    $page = obj("api/ApiData")->page("10", "yun_article", $where, "`id` DESC", $baseUrl);
    $this->page = $page;                       // 模板里用 $page['list'] 遍历,$page['page'] 输出分页

    $this->pageTitle   = '列表页 - ' . obj('base/Base')->SiteConfig('sitename');
    $this->pageKeywords = '关键词';
    $this->display();
}

模板端对应输出:

{foreach $page['list'] as $item}
  <a href="{$item['url']}">{$item['title']}</a>
  <span>{$item['addtime']|date_format}</span>
{/foreach}
{$page['page']}   <!-- 分页条 -->

3. 双引擎通用能力

以下语法在 ZhiCms 自研引擎 与 ThinkTemplate 两种引擎下都可用,是编写「可同时兼容两套引擎」模板的基础。

3.1 变量输出

{$name}              <!-- 输出 $name -->
{$user.name}         <!-- 输出 $user['name'] -->
{$user['name']}      <!-- 数组下标写法,两引擎都支持 -->
{$list[0].title}     <!-- 多维访问 -->

3.2 判断(if / elseif / else)

{if $status == 1}
  启用
{elseif $status == 2}
  暂停
{else}
  未知
{/if}

常用判断写法:{if $x == 1}{if $x != ''}{if $x > 3}{if !empty($arr)}{if isset($a) && $a == 1}

3.3 循环 foreach

{foreach $list as $item}
  <a href="{$item.url}">{$item.title}</a>
{/foreach}

{foreach $map as $key => $val}
  {$key}: {$val}
{/foreach}

ZhiCms 自研引擎会在循环内自动维护 $n 计数器($n 从 1 开始);ThinkTemplate 同样兼容 {foreach}。空数组时 foreach 不会执行,不会报错。

3.4 引入公共片段 include

{include file="app/index/view/public/header"}
{include file="app/index/view/public/footer"}
{include file="app/index/view/index/list_item"}   <!-- 可复用列表项 -->

3.5 函数调用输出

{obj('base/Base')->SiteConfig('sitename')}   <!-- 站点名 -->
{obj('base/Base')->SEO('index_title')}        <!-- SEO 标题 -->
{date('Y-m-d H:i:s', $item['addtime'])}       <!-- 时间格式化 -->
{count($list)}                                 <!-- 统计数量 -->
{url('index/index/index')}                     <!-- 生成路由 URL -->

说明:自研引擎把 {函数(...)} 编译为 <?php echo 函数(...);?>;ThinkTemplate 中这类「函数直出」建议使用 {:func()} 写法(见 5.2)。

3.6 isset / empty 防空

为避免变量未定义告警,判断前先用 isset / empty 防护:

{if isset($pageTitle) && $pageTitle != ''}{$pageTitle}{else}默认标题{/if}
{if !empty($banners)} ...轮播... {/if}

4. ZhiCms 自研引擎(默认)

直接通过正则把 {...} 编译成 PHP,输出默认原样、不转义

4.1 支持的标签

语法 编译结果 说明
{$var} <?php echo $var;?> 变量输出
{$a.b.c} <?php echo $a['b']['c'];?> 点语法访问多维数组
{CONSTANT} <?php echo CONSTANT;?> 常量输出(如 {CONTROLLER_NAME}{ACTION_NAME}{APP_NAME}{__PUBLIC__}
{if ...}{elseif ...}{else}{/if} <?php if... ?>... 条件判断
{foreach $arr as $v} / {foreach $arr as $k => $v} <?php foreach... ?> 循环
{for $i=0;$i<10;$i++} / {/for} <?php for... ?> for 循环
{php ...} <?php ... ?> 原生 PHP 块
{函数(...)} <?php echo 函数(...);?> 函数输出
{include file="..."} <?php $__Template->display("...");?> 引入片段

4.2 常见坑


5. ThinkTemplate(真·ThinkPHP 引擎)

语法与 ThinkPHP 模板引擎一致,额外支持更丰富的写法。

5.1 变量与函数

5.2 模板继承

{extend name="app/index/view/public/base"}
{block name="content"}页面内容{/block}

5.3 更多标签

支持 {include file="..."}{volist}{switch}{compare} 等 ThinkPHP 标签。

编写可同时兼容两套引擎的模板:尽量只用第 3 章的通用语法;如需函数直出,用 {:func()}(自研引擎也能正确输出),避免在文案中出现裸大括号。


6. 常用框架对象与数据

模板中常用 obj() 全局助手获取业务数据:

表达式 说明
{obj('base/Base')->SiteConfig('sitename')} 站点名称
{obj('base/Base')->SiteConfig('hosturl')} 站点域名
{obj('base/Base')->SiteConfig('sitekeywords')} 关键词
{obj('base/Base')->SiteConfig('sitedescription')} 站点描述
{obj('base/Base')->SEO('index_title')} SEO 标题
{obj('base/Base')->SEO('index_dec')} SEO 描述
{url('index/index/index')} 生成路由 URL
{obj('api/ApiData')->...} 数据查询(一般由控制器封装后赋值)

控制器 assign() 注入的变量在模板中直接以 $变量名 使用。


7. 常用模板变量(控制器注入)

以下变量由各控制器在渲染前注入,模板可直接使用:

具体变量名以各控制器 assign() 注入为准;开发时可先在同页面控制器源码中查看注入了哪些变量。

7.1 分页输出

分页条已由框架生成 HTML,直接输出即可:

{if isset($page) && isset($page['page'])}
<div class="pagination" style="margin-top:24px;">{$page['page']}</div>
{/if}

7.2 URL 生成

{url(...)} 生成带路由规则的地址,避免硬编码:

<a href="{url('index/index/view', ['id' => $item['id']])}">{$item['title']}</a>

8. 数据库操作(控制器取数 / 增改删)

控制器通过 obj("api/ApiData")(即 app\api\model\ApiDataModel)访问数据库。表名可带前缀 {pre}(自动替换为 yun_)或直接用真实表名 yun_xxx

所有表名前缀统一为 yun_(如文章表 yun_article、用户表 yun_user、评论表 yun_comment、轮播表 yun_huan、友链表 yun_link)。插件自定义表通常也用 yun_ 前缀建表。

8.1 查询(查)

单条记录(无 $order 时内部走 find(),返回一维数组):

$row = obj("api/ApiData")->dataSelect("yun_article", array("`id` = 123"));
// $row = ['id'=>123, 'title'=>'...', 'content'=>'...']

多条记录(传 $orderselect(),返回二维数组):

$list = obj("api/ApiData")->dataSelect("yun_article", array("`status` = '1'"), "`id` DESC LIMIT 0, 10");
// $list = [ ['id'=>..,'title'=>..], ... ]

原生 SQL 查询(可带占位参数防注入):

$rows = obj("api/ApiData")->thisQuery(
    "SELECT c.*, u.username FROM `{pre}comment` c LEFT JOIN `{pre}user` u ON u.id = c.uid WHERE c.mid = ? ORDER BY c.id DESC LIMIT 6",
    array($id)
);

计数

$count = obj("api/ApiData")->dataCount("yun_comment", array("`mid` = 1 AND `hide` = 'n'"));

流式查询构造器table()->where()->order()->limit()->select()):

$rows = obj("api/ApiData")->table("yun_article", true)
    ->where(array("`status` = '1'"))
    ->order("`id` DESC")
    ->limit("0, 20")
    ->select();

8.2 新增(增)

$newId = obj("api/ApiData")->insertData("yun_my_table", array(
    'title'   => $title,
    'content' => $content,
    'addtime' => time(),
));
// $newId 为自增主键 id

批量插入(性能更高,一条多值 INSERT):

$dataList = array(
    array('title' => 'a', 'content' => '...'),
    array('title' => 'b', 'content' => '...'),
);
$affected = obj("api/ApiData")->insertAllData("yun_my_table", $dataList);

8.3 更新(改)

obj("api/ApiData")->dataUpdate("yun_my_table", array(
    'title' => $newTitle,
    'hits'  => $hits + 1,
), array("`id` = $id"));

原生更新(支持参数化):

obj("api/ApiData")->executeQuery(
    "UPDATE `{pre}article` SET `hits` = `hits` + 1 WHERE `id` = ?",
    array($id)
);

8.4 删除(删)

// 简单按 id 删除(自动参数化)
obj("api/ApiData")->deleteThis("yun_my_table", "`id` = 123");

// 复杂条件删除(推荐参数化)
obj("api/ApiData")->deleteThis("yun_my_table", "`uid` = ? AND `model` = ?", array($uid, 'article'));

// 原生删除
obj("api/ApiData")->executeQuery("DELETE FROM `{pre}my_table` WHERE `id` = ?", array($id));

8.5 控制器中「取数 → 加工 → 赋值给模板」完整示例

public function detail(){
    $id = (int)$this->arg('id');            // 接收 GET/POST 参数并转整型

    // 1) 查文章
    $view = obj("api/ApiData")->dataSelect("yun_article", array("`id` = $id"));
    if (empty($view)) { $this->alert('文章不存在'); }

    // 2) 加工:补字段 / 触发钩子让插件改写
    $view['cateName'] = \app\base\controller\BaseController::getNavName($view['navid'] ?? 0);
    \ZhiCms\base\Hook::listen('article_view', array(&$view));

    // 3) 赋值(public 属性会被 display() 自动注入)
    $this->view = $view;
    $this->pageTitle = $view['title'] . ' - ' . obj('base/Base')->SiteConfig('sitename');
    $this->canonicalUrl = url('index/index/view/id=<id>', array('id' => $id));
    $this->display();
}

8.6 前端提交数据的写入(增删改示例)

配合前端表单,控制器接收 $_POST(用 $this->arg() / $this->Postarg() 安全取值)后写入数据库:

public function add(){
    if (!$this->isPost()) { $this->alert('非法请求'); }

    $title = trim($this->Postarg('title'));       // 安全取 POST,已做 htmlspecialchars 过滤
    $content = $_POST['content'];                  // 富文本内容一般用原始值

    obj("api/ApiData")->insertData("yun_article", array(
        'title'   => $title,
        'content' => $content,
        'addtime' => time(),
    ));
    echo json_encode(array('info' => '添加成功', 'status' => 'y'));
}

arg():合并 $_GET+$_POST,会剥离 <script> 等并做 htmlspecialcharsPostarg() 只取 POST。需要原始值(如富文本/JSON)时直接用 $_POST['xxx']


9. 静态资源引入

建议在 public/web/ 下组织 CSS/JS,模板中通过 {__PUBLIC__} 常量引用:

<link rel="stylesheet" href="{__PUBLIC__}web/css/blog.css?v=5.0.2">
<script src="{__PUBLIC__}web/js/common.js?v=5.0.2"></script>

10. 明暗主题适配

系统支持 light/dark 双主题(<html data-theme="light|dark">),主题变量定义在 public/web/css/blog.css / common.css


11. 常见问题排查

现象 原因 / 处理
Template file ... not found 模板路径不对;display() 建议用绝对路径 app/index/view/xxx,且 data/config/global.phpTPL_PATH 为空时不能依赖相对路径
Undefined constant "xxx" 自研引擎把 {中文} / {标签} 当常量解析;将文案中的裸大括号转义为实体
页面空白 / 无输出 模板编译缓存旧版,删除 data/cache/tpl* 后重试
{:func()} 不生效 当前为自研引擎;自研引擎用 {func()} 写法
切换引擎后样式错乱 引擎不同导致标签渲染差异,核对模板中使用了哪些语法,统一为通用语法
变量未定义告警 isset / empty 防空
页面显示 HTML 实体 &#123; 这是转义后的模板标签示例,属正常;如需真的 {,用 &#123; 即可
背景 / 文字在 dark 下看不清 模板内联写死了深色文字;改用 var(--fontColor) 等主题变量

本文档基于 ZhiCms 双模板引擎实现编写,覆盖模板目录、页面骨架、变量注入、双引擎语法差异、公共片段、分页/URL、静态资源、明暗主题与常见问题等完整开发要点。


12. 插件模板化(模板与插件协作)

当插件是「模板化插件」(plugin.jsontype=template + rewrite,见 PLUGIN_DEV.do 第 7 章)时,它的前台页由插件自己的 view/ 目录下的模板渲染,而不是 app/index/view/。模板编写的异同如下。

12.1 模板位置与引擎

前台主站模板 模板化插件模板
目录 app/index/view/ plugins/{alias}/view/
渲染者 前台控制器 display() 插件控制器基类 display()ZhiCms\base\ThinkTemplate
默认引擎 后台「模板引擎」可切 legacy/think 默认 think-template(与 guangdiu/kiees 一致,建议保持)
变量来源 控制器 assign() / public 属性 插件控制器 assign() + display() 里统一注入的站点变量

插件模板语法遵循 think-template 引擎(见第 3 章通用语法 + 第 5 章),可用 {foreach} / {$x.y} / {include} / {:func()}

12.2 插件模板里可用的变量

插件控制器 display() 通常会统一注入以下变量(以 guangdiu 为参考,具体以你的控制器为准):

变量 说明
{$site_name} / {$site_logo} / {$copyright} / {$beian} 站点名称 / Logo / 版权 / 备案(来自 SiteConfig
{$host_url} 站点域名
{$plug_static} 插件静态资源根目录(/plugins/{alias}/static
{$plug_url} 插件首页链接(伪静态或动态)
{$plug_base} 插件链接前缀(如 plug-guangdiu
{$is_mobile} 是否移动端(0/1)
{$show_sidebar} 是否显示侧栏(移动端常 0)
{$nav_index} / {$nav_cheaps} / {$nav_brand} / {$nav_rank} 各频道链接
{$hot} / {$list} / {$article} / {$page} 业务数据(控制器 assign 注入)

12.3 在插件模板里引用站点配置

<title>{$site_name}</title>
<footer>© {$copyright} {$beian}</footer>
<link rel="stylesheet" href="{$plug_static}/css/style.css?v=1.0.0">

12.4 插件模板 include 自己的片段

{include file="plugins/guangdiu/view/header"}
{include file="plugins/guangdiu/view/sidebar"}
...页面主体...
{include file="plugins/guangdiu/view/footer"}

路径要写成插件根目录绝对路径plugins/guangdiu/view/header),不要用主站 app/index/view/public/header,否则会混入主站布局(主站 header 含 BaseController 注入的变量,插件页没有)。

12.5 插件模板里的链接生成

模板已注入 {$plug_url},拼详情/频道链接:

<a href="{$plug_url}-{$item.id}.html">{$item.title}</a>      <!-- 详情 -->
<a href="{$nav_cheaps}">优惠券</a>                          <!-- 频道 -->

若需动态生成(如翻页),可在模板里用:

<a href="{:plugins\guangdiu\controller\SiteController::pageUrlHref(['page'=>$page['next']])}">下一页</a>

更推荐:控制器把分页 HTML 算好(如 $this->assign('pagebar', $html))直接输出 {$pagebar}

12.6 插件模板的缓存清理

改了插件 view/ 下模板后,若前台有模板编译缓存或静态缓存命中旧版,请清理:

否则可能看到旧版模板。

12.7 把插件设为整站首页

后台「系统设置 → 站点设置 → 主页展示插件」选择该模板化插件后,访问 / 会渲染插件首页(URL 不变)。插件无需任何改动,框架 App::applyHomePlug() 自动把首页路由转给 PlugController::view()Plugin::displayPage()。前提:插件已启用、type=templaterewriteplug-<alias>.html。*

请先 登录 再评论