1. 主题模板里的“函数”到底是什么
不少刚接触 Typecho 主题开发的朋友,打开模板文件后第一反应是:里面全是$this->xxx(),长得像函数又不像普通函数,不知道从哪来的,也不知道能调什么。其实 Typecho 的模板调用体系,本质上是一个“对象方法输出”的封装。你在 index.php 里写的$this->title(),并不是独立存在的全局函数,而是当前页面所对应的Widget_Archive(文章归档组件)实例上的方法。这个实例根据你访问的页面类型不同,可能是文章页、分类页、搜索页,或是独立页面,但对外暴露的调用方式是一致的。
这套设计的最大好处就是“模板层不需要关心数据从哪来”。你写$this->content(),它自动输出当前文章内容,写$this->permalink()自动输出当前页面的 URL。不需要自己写 SQL 去查数据库,也不需要手动拼接链接,框架已经帮你把当前上下文的数据准备好了。理解这层逻辑,再去看模板里的任意一个调用,心里就有底了。
另一个值得说清楚的概念是“函数”和“方法”的区别。严格来说,$this->title()是对象方法,但 Typecho 官方文档和社区里都习惯叫“模板函数”或“调用函数”。而我们这要讨论的“常用调用函数”,既包括这些模板方法,也包括 Typecho 全局的辅助函数(比如_t()、Typecho_Widget::widget()),二者共同组成了主题开发时最常用的工具集。
下面我按实际开发场景,把这套体系里的高频调用逐个拆开讲,附上写法、用途和踩坑经验。如果你正在做主题、改主题,或者想把 Typecho 用得更顺手,这份清单应该够你用一阵子。
2. 文章和页面:最核心的一组调用
2.1 标题、链接、日期这三个“基本款”
只要是文章列表页或者文章内容页,这三样几乎是必用的。标题的调用是$this->title(),直接输出当前文章的标题文本。链接是$this->permalink(),输出当前文章的完整地址,通常配合<a href="...">使用。日期稍微有点讲究,默认写法$this->date()会输出2019-05-05这种格式,如果你想控制格式,可以像这样传参:
<time class="meta__date"><?php $this->date('Y年m月d日'); ?></time>日期格式参数用的是 PHP 的date()函数格式规则,Y是四位数年份,m是带前导零的月份,d是带前导零的日期。实际开发里我比较推荐给<time>标签的datetime属性输出机器可读格式,文本内容再单独控制:
<time datetime="<?php $this->date('c'); ?>"><?php $this->date('F j, Y'); ?></time>c会输出完整的 ISO 8601 格式日期,比如2024-12-08T21:15:30+08:00,对搜索引擎和浏览器都友好。
这里有一个新手经常踩的坑:在文章列表页,$this->date()输出的是列表循环里当前文章的时间,而不是全站某个固定时间。如果循环外使用,可能拿到的是初始化时的值,行为不一定符合预期。所以日期、标题这类调用,一定要放在foreach或者 Typecho 的循环结构内部,才能保证输出当前条目的数据。
2.2 正文、摘要与“”
文章正文的调用最直接,模板里写<?php $this->content(); ?>就行。但这里有个细节:content()默认输出的是数据库里 post 表的完整内容字段,如果文章里插入了“精美分页”的<!--more-->标记,content()在列表页会自动截断,并且在截断处显示一个指向全文的链接。
这个“截断”行为让很多开发者困惑。实际上,content()内部根据当前的上下文(是列表还是单篇)决定是否截断。如果你在列表页想要强行输出全文,可以把参数传进去,让截断失效:
<?php $this->content('', true); ?>第二个参数true表示强制输出完整内容。第一个参数是“”的链接文本,默认是一个指向“继续阅读 »”的链接。
如果只是想要摘要,也就是文章开头一段话,Typecho 里通常配合excerpt字段使用。常见的写法是:有自定义摘要字段就输出它,没有就用content()截断。不过要注意,excerpt是 Markdown 编辑器可能不会自动生成的字段,很多主题作者会自己定义“摘要”输入框,实现思路是在后台文章编辑页注册一个自定义字段summary,然后在主题里读取,作为摘要展示。
<?php if ($this->fields->summary): ?> <p class="post__summary"><?php $this->fields->summary(); ?></p> <?php else: ?> <p class="post__summary"><?php $this->excerpt(80, '...'); ?></p> <?php endif; ?>excerpt(80)表示截取 80 个字符,'...'是截断后的后缀。这套组合拳在日常主题开发里出镜率极高。
2.3 分类、标签与作者信息
分类的调用写法是$this->category(),不带参数时输出当前文章所属分类的链接,多个分类会用逗号隔开。如果想让分类不带链接,只需要传入参数:
<?php $this->category(',', false); ?>第一个参数是多个分类之间的分隔符,第二个参数false表示不输出链接,只输出分类名称。这个技巧在文章页头部显示“发布于某分类”时很实用。
标签调用稍微复杂一点,$this->tags()默认输出全部标签的链接形式,每个标签是<a href="...">标签名</a>的结构。如果文章没有标签,会输出空内容。想要控制标签输出,比如设置分隔符、是否加链接,可以这样做:
<?php $this->tags(', ', false); ?>跟分类同理,第一个参数是分隔符,第二个参数控制是否输出链接。个别主题想给每个标签套上自定义 HTML 结构,那就不能用tags()的默认输出,得自己循环tags数组。Typecho 本身支持在模板中这样遍历:
<?php if (count($this->tags)): ?> <?php foreach ($this->tags as $tag): ?> <a href="<?php echo $tag['permalink']; ?>"><?php echo $tag['name']; ?></a> <?php endforeach; ?> <?php endif; ?>$this->tags是当前文章标签的数组,每个元素包含name、permalink等字段。这个自由度更高,也是我在主题里处理标签的首选方式。
作者信息同样好取:$this->author()默认输出作者昵称的链接(指向作者归档页),老版本里$this->author->mail可以拿到作者邮箱,用来拼接 Gravatar 头像:
<img src="<?php echo $this->author->gravatar(64); ?>" alt="作者头像">有一个大家容易忽略的点:$this->author是当前文章的作者对象,不是登录用户对象。在文章页里要显示博主信息,务必用$this->author。而如果你写的是侧边栏想要“当前登录用户”的信息,那得另想办法,用全局$_SESSION['uid']或者用户组件去查询,别混用。
3. 站点信息与侧边栏:被问得最多的调用
3.1 站点地址、主题地址和资源路径
在主题里,最绕不开的就是路径问题。引入 CSS、JavaScript、图片时,如果你写死了/usr/themes/xxx/style.css,将来网站搬目录就会全盘崩坏。正确做法是用 Typecho 提供的方式:
<link rel="stylesheet" href="<?php $this->options->themeUrl('style.css'); ?>"> <script src="<?php $this->options->themeUrl('js/main.js'); ?>"></script>themeUrl()可以理解为“拼接当前主题目录下的路径”,传的参数会拼接在主题基础路径后面。这个函数在 Typecho 1.0 及之后的版本都能用。站点首页地址对应$this->options->siteUrl(),一般用来做页脚链接、站点 logo 的跳转地址。
<a href="<?php $this->options->siteUrl(); ?>">回到首页</a>如果只是想要“当前页面的 URL”,而不是站点地址,不要用siteUrl(),应该用前面提到的$this->permalink()或者在模板里用 PHP 自带的$_SERVER['REQUEST_URI']。很多半吊子教程混用这两个,导致博客首页向别的页面分享时,URL 拼接出错。
3.2 站点标题、描述和 RSS 地址
站点名称调用是$this->options->title,站点描述是$this->options->description。注意这两个没有括号,属于“属性”而不是“方法”。写的时候别手滑加括号:
<title><?php $this->options->title(); ?></title> <!-- 错误 --> <title><?php $this->options->title; ?></title> <!-- 正确 -->很多朋友问,为什么我用$this->options->title()报错或者输出为空?原因就是 Options 组件里,title和description是以属性形式存在的,而themeUrl()、adminUrl()这类才是带括号的方法。判断的标准很简单:返回的是一个字符串配置项,还是需要经过计算生成的路径。配置项就不加括号,计算类的方法才加括号。
RSS 订阅地址的调用是$this->options->feedUrl(),通常写在侧边栏“订阅”按钮的链接里。它输出的是站点的主 RSS 地址,可以自行拼接参数来区分分类订阅,不过一般主题用不到那么复杂,直接输出即可。
<a href="<?php $this->options->feedUrl(); ?>">RSS 订阅</a>3.3 侧边栏的“三板斧”:最新文章、分类列表和标签云
侧边栏几乎所有博客系统都会做,Typecho 的官方默认主题里有一整套现成的调用,但很多新手不知道这些怎么来的。在 Typecho 里,侧边栏常用的列表其实是通过Typecho_Widget::widget()调用别的组件实现的。
最新文章列表的标准写法:
<?php $this->widget('Widget_Contents_Post_Recent') ->parse('<li><a href="{permalink}">{title}</a></li>'); ?>这里用了一个 Typecho 的特色方法parse(),它接收一个格式化字符串,里面用花括号包裹字段名,比如{permalink}、{title}、{date}。parse()会循环数据源并输出。这个写法非常紧凑,适合侧边栏这种简单列表。但要注意字段名的准确性,写错字段它不会报错,只是输出空字符串,排查起来容易懵。
分类列表的调用方式:
<?php $this->widget('Widget_Metas_Category_List') ->parse('<li><a href="{permalink}">{name}</a></li>'); ?>标签云的调用方式:
<?php $this->widget('Widget_Metas_Tag_Cloud') ->parse('<li><a href="{permalink}">{name}</a></li>'); ?>也许你会发现,这三个调用的结构都一样,只是中间的组件名不同。这其实是 Typecho 的组件化设计:Widget_Contents_Post_Recent代表“最近文章”,Widget_Metas_Category_List代表“分类列表”,Widget_Metas_Tag_Cloud代表“标签云”。理解了这个规律,你甚至可以在侧边栏调用“最近评论”:
<?php $this->widget('Widget_Comments_Recent') ->parse('<li><a href="{permalink}">{author}</a>: {text}</li>'); ?>需要注意,评论区组件返回的字段跟文章组件不一样,有author、text、permalink等,具体可以打印出来看。我自己的习惯是,先写一个临时页面用var_dump()打出组件对象,看清楚了再写模板,这样能少走不少弯路。
3.4 登录、注册、退出与后台链接
主题页脚经常要放“登录”“退出”或者“管理后台”的入口。Typecho 提供了几个便捷方法:
<a href="<?php $this->options->adminUrl(); ?>">管理</a> <a href="<?php $this->options->logoutUrl(); ?>">退出</a>adminUrl()输出后台地址,logoutUrl()输出带退出参数的登录页地址。注册地址用registerUrl(),不过在仅允许邀请注册时会跳转到登录页面,属于正常行为。
这里有个体验上的细节:如果你是做主题给用户用,登录、退出这些链接最好用if判断脚本身份,而不是无脑输出。比如:
<?php if ($this->user->hasLogin()): ?> <a href="<?php $this->options->adminUrl(); ?>">管理</a> <a href="<?php $this->options->logoutUrl(); ?>">退出</a> <?php else: ?> <a href="<?php $this->options->loginUrl(); ?>">登录</a> <?php endif; ?>$this->user->hasLogin()判断当前是否有用户登录,这是主题里控制前后端权限展示的常用手段。很多付费主题的“用户中心”就是这么搭起来的。
4. 进阶工具函数与开发必备技巧
4.1 多语言辅助函数_t()与_e()
Typecho 的主题和插件里经常能看到_t('字符串')这种调用。_t()是“翻译函数”,开发主题时只要把所有文案都包在_t()里,后续再出一套语言文件,就能实现前端界面多语言切换。
<p><?php _e('没有找到相关内容,换个关键词试试?'); ?></p>_e()和_t()的区别是,_t()只返回翻译结果,_e()直接输出。所以_t()通常用于拼接变量,_e()用于模板中直接打印文本。新手容易混淆,我建议记住一句话:需要赋值或者拼接用_t(),直接在模板里出文本用_e()。
4.2 全局组件调用:Typecho_Widget::widget()
刚才侧边栏介绍的$this->widget(),本质上就是Typecho_Widget::widget()的快捷包装。当你在模板任何地方想临时拉一份数据时,都可以用这个静态调用:
<?php $posts = Typecho_Widget::widget('Widget_Contents_Post_Recent') ->to($list); ?> <?php while ($list->next()): ?> <a href="<?php $list->permalink(); ?>"><?php $list->title(); ?></a> <?php endwhile; ?>->to($list)的意思是,把查询结果赋值给变量$list,然后就能像$list自身一样循环调用next()和各个字段方法。这个写法比parse()更灵活,适合在自定义区块里做复杂布局。如果你需要查询多个不同条件的数据,可以这样做:
$filtered = Typecho_Widget::widget('Widget_Contents_Post_Recent') ->to($recent);再配合$recent->next()循环,就能逐条拿到文章对象。
4.3 条件判断与无数据兜底
模板开发中,判断“有没有内容”和“做了什么类型页面”是整个逻辑的地基。常用判断包括:
<?php if ($this->is('index')): ?>首页<?php endif; ?> <?php if ($this->is('post')): ?>文章页<?php endif; ?> <?php if ($this->is('category')): ?>分类页<?php endif; ?> <?php if ($this->is('search')): ?>搜索页<?php endif; ?>$this->is()是 Typecho 通过Widget_Archive暴露出来的“当前页面类型判断”方法。写index、post、page、category、tag、author、search、date、404等字符串,就能精准匹配当前访问模式。这个判断逻辑在定义模板结构、设置不同页面样式时极有用,比如你想让首页不显示侧边栏,直接写:
<?php if (!$this->is('index')): ?> <aside>侧边栏内容</aside> <?php endif; ?>另外,任何列表都可能为空。Typecho 没有单独的“空数据判断函数”,而是在对应组件上摸$this->has()或直接检查$this->stack的长度。模板里常用的写法是:
<?php if ($this->have()): ?> 循环输出内容 <?php else: ?> <p>暂无内容</p> <?php endif; ?>have()方法返回当前上下文是否还有数据,这个在列表页最常用,配合while ($this->next())构成标准的循环模板结构。
4.4 自定义字段读取与设置
Typecho 的自定义字段是很多进阶玩家的“法宝”。在后台写文章时,右侧“自定义字段”区域可以添加任意键值对,模板里用$this->fields->字段名就能读取。
<?php if ($this->fields->subtitle): ?> <h2><?php $this->fields->subtitle(); ?></h2> <?php endif; ?>需要注意,自定义字段也分为属性和方法两种访问方式:$this->fields->subtitle取到字段的值,$this->fields->subtitle()直接输出。二者在常规场景下是等价的,都有输出效果,但如果你要在 PHP 层做字符串比较,必须用不带括号的属性形式来赋值:
<?php $subtitle = $this->fields->subtitle; ?>有很多人用自定义字段实现文章浏览量计数。思路很简单:在模板里读取views字段,加一,再写回。不过这个操作需要小心,因为每次访问都触发数据库写入,在高并发场景下可能造成压力。个人博客还好,如果流量大,建议引入缓存插件或采用更稳妥的计数方案。
4.5 其它高频杂项调用
有几类调用虽然不那么常用,但关键时刻能救命。一个是“当前页面是否处于评论提交成功状态”的判断,可以在评论表单里这样用:
<?php if ($this->need('comments.php')): ?>其实 Typecho 的主题里,$this->need()是用来引入模板文件的。写法$this->need('comments.php')等同于include 'comments.php',但会带上当前的组件上下文,模板内部就能直接用$this了。如果你自己拆分模板文件,强烈推荐用$this->need()而不是 PHP 的include,这样能保证变量环境不混乱。
再一个是“面包屑导航”相关的调用。Typecho 官方默认没有现成的面包屑函数,需要通过“当前分类”的对象去逐级往上找:
<?php $category = $this->category; if ($category) { $catObj = $this->widget('Widget_Metas_Category_List')->to($catList); } ?>严格来说,Typecho 每个分类对象都有parent属性,配合循环就能向上追踪父分类。我在做企业站主题时经常写这样一小段递归逻辑,输出“首页 > 父分类 > 子分类 > 当前文章”的结构。这段代码网上各种写法都有,核心是拿到$this->category这个当前分类数组。
此外,所有页面都需要输出“页面标题”,Typecho 没有像某些系统那样提供get_the_title_in_html()之类的复杂函数,常规做法是在header.php里面自己组合。比如文章页想要 SEO 友好的标题:
<?php if ($this->is('post')): ?> <title><?php $this->title(); ?> - <?php $this->options->title; ?></title> <?php else: ?> <title><?php $this->options->title; ?> - <?php $this->options->description; ?></title> <?php endif; ?>这里同样体现了is()+title()+options的配合。整套组合拳打完,一个标准 Typecho 页面所需的头部信息就都能动态输出了。
5. 常见问题与排查技巧实录
5.1 调用输出为空:先确认上下文
我做 Typecho 主题的第一年,遇到最多的诡异问题就是“明明写了$this->title(),浏览器里啥都没有”。这种问题九成出在模板上下文不对。比如你在自己写的sidebar.php里面,用include方式引进来,但 sidebar 文件里并没有循环结构,也就没有“当前文章”这个概念,此时调用$this->title()拿到的只能是空。
排查思路很简单:先在模板里打印一下当前上下文类型:
<?php var_dump($this->getArchiveType()); ?>或者干脆输出整个对象结构,但页面会变得巨大,所以更稳妥的方法是格式化输出几个核心属性:
<?php var_dump($this->options); var_dump($this->category); ?>看到输出之后,你就会立刻明白当前处于什么页面、有没有数据。如果连$this->options都是空的,那问题多半出在模板引入方式上,检查是不是用了include而不是$this->need(),或者模板路径不对导致根本没进入 Typecho 的执行流程。
5.2parse()输出乱码或字段为空
用parse()的时候,如果输出的列表项全为空,或者出现一条多余的“Array”字符串,通常是因为字段名写错了。parse()模板支持的字段以组件的数据列为准,不是所有方法都能用。最直接的排查方式:先不要写parse(),直接用while ($list->next())循环,把每个可用字段都打出来:
<?php $this->widget('Widget_Contents_Post_Recent') ->to($recentList); ?> <?php while ($recentList->next()): ?> <?php var_dump($recentList->fields); ?> <?php endwhile; ?>看到字段名之后再回去修改parse()模板字符串。这个“先打印后写模板”的习惯,能帮你省掉一半的开发时间。
5.3 日期时区和格式不对
Typecho 后台“设置 – 评论”以及“设置 – 基本”里的时区选项,直接决定前端$this->date()输出的是否正确。如果你发现输出的日期时间跟实际相差 8 小时,赶紧去后台看看时区设置是否为“UTC+8”。因为 Typecho 默认可能是GMT+8,但服务器时间可能是 UTC,造成偏移。这是部署环境问题,靠date_default_timezone_set()去模板里硬切不推荐,因为改完后台设置又会被覆盖,维护成本高。
5.4 函数列表速查表
为了方便你日常查阅,我把常用的调用整理成下面这个速查表。记住这些,主题开发的基本盘就稳了:
| 功能 | 调用写法 | 备注 |
|---|---|---|
| 文章标题 | <?php $this->title(); ?> | 输出文本 |
| 文章链接 | <?php $this->permalink(); ?> | 完整 URL |
| 文章日期 | <?php $this->date('Y-m-d'); ?> | 支持日期格式参数 |
| 文章正文 | <?php $this->content(); ?> | 自动截断识别<!--more--> |
| 摘要 | <?php $this->excerpt(100, '...'); ?> | 截取长度、后缀 |
| 分类 | <?php $this->category(',', false); ?> | 第二个参数控制链接 |
| 标签 | <?php $this->tags(',', false); ?> | 第二个参数控制链接 |
| 作者 | <?php $this->author(); ?> | 输出作者链接 |
| 站点名称 | <?php $this->options->title; ?> | 注意无括号 |
| 站点描述 | <?php $this->options->description; ?> | 注意无括号 |
| 主题资源路径 | <?php $this->options->themeUrl('style.css'); ?> | 自动拼接主题目录 |
| 站点首页 | <?php $this->options->siteUrl(); ?> | 网站根地址 |
| RSS 地址 | <?php $this->options->feedUrl(); ?> | 订阅源 |
| 最近文章 | $this->widget('Widget_Contents_Post_Recent')->parse(...) | 组件调用 |
| 分类列表 | $this->widget('Widget_Metas_Category_List')->parse(...) | 组件调用 |
| 标签云 | $this->widget('Widget_Metas_Tag_Cloud')->parse(...) | 组件调用 |
| 最近评论 | $this->widget('Widget_Comments_Recent')->parse(...) | 组件调用 |
| 页面类型判断 | $this->is('post') | 支持 index/post/page/category 等 |
| 是否有数据 | $this->have() | 循环前判断 |
| 引入模板 | $this->need('comments.php') | 带组件上下文 |
| 登录判断 | $this->user->hasLogin() | 返回布尔值 |
| 多语言文本 | _e('文本')/_t('文本') | 前者输出,后者返回 |
5.5 性能与安全的两个提醒
最后一个想说的是性能问题。反复调用Typecho_Widget::widget()去查“最近文章”“标签云”这类数据,缓存的压力比想象中大。虽然 Typecho 自带一套对象缓存机制,但在高并发下,还是建议给首页、侧边栏做页面静态化,或者至少加一层 Memcached / Redis 缓存插件。不要在一个列表页里写五六个widget()查询,那会把你数据库的连接瞬间打爆。
安全性方面,用$this->content()和$this->excerpt()输出时不必过度担心 XSS,因为 Typecho 在存储和输出阶段默认做了一层过滤。但如果你把$this->author之类的属性直接拼进<script>标签或者 HTML 属性里,那风险就自己扛了。我的原则是:所有来自用户的内容,一律只输出在 HTML 正文区域里,不要拼接进src、href、onclick这类属性中,除非你手动做htmlspecialchars()转义。
6. 我踩过几次坑之后的一点体会
Typecho 的模板调用函数说来说去也就这些,数量不多,但每个背后都有一套设计逻辑。我做主题做到后期,其实很少去背某个函数名,而是先想明白“当前模板处于什么页面”、“这个组件能给我什么数据”,然后打开官方文档菜单或者直接打印对象看一眼,答案自然就出来了。
这里分享一个自己的土方法:在header.php的顶部放一段调试代码,用var_dump()输出$this对象的类名。当你打开不同页面时,看这个类名就知道当前页面绑定的是哪个 Widget,然后顺藤摸瓜去查它的方法列表。比如访问文章页时,$this其实是Widget_Archive的实例,它的方法里就躺着title()、content()、category()这一大堆;访问独立页面时,虽然也是Widget_Archive,但内部数据的类型标记会有所不同。用这个方式,能让你从“背函数”进阶到“理解机制”,遇到没见过的需求也不会抓瞎。
另外,如果你改了主题文件但页面没有任何变化,先去后台清一下缓存。Typecho 的主题模板在开启“缓存”功能时会保存编译结果,很多改动不会立即生效。我在本地开发时习惯直接关闭缓存,上线前再打开,这样既能即时调试又不牺牲速度。
最后再补一句:网上那些“Typecho 函数大全”的帖子,很多是从旧版本复制过来的,个别函数在新版 Typecho 里已经废弃或不推荐使用。遇到模棱两可的调用,优先翻阅 Typecho 官方文档的“主题制作”章节,或者直接看你当前版本的var/Widget目录下对应组件源码,实在搞不懂,就做个最简单的/tmp/test.php页面去打印对象,什么谜底都揭开了。
希望这份调用清单能帮你少走点弯路。如果你刚做完自己的主题,回头看看文章里提到的is()判断和widget()动态查询这两个大招有没有用上——它们基本决定了你的主题是只能“静态展示”,还是能“根据场景动态变化”,这两者的差距,就是普通主题和靠谱主题的分界线。