前阵子帮朋友把博客从老平台迁到 Typecho,他原来的主题是找别人定制的,站点名、Logo、统计代码全部硬编码在 header.php 里。迁移的时候我翻了一下午模板,才把所有散落的配置找齐,改完还得反复确认有没有漏掉的入口。那一刻我就在想,如果当初写主题的人肯多用一下 Typecho 的配置机制,后面接手的人能省多少事。其实 Typecho 早就留好了两个口子:themeConfig和themeFields。前者在后台上生成主题设置面板,后者在文章编辑页添加自定义字段。两个函数配合起来,就能把一个“一次性主题”升级成“可配置主题”,这也是标题里 themeFileds(正确拼写是 themeFields)的核心用法。
这篇文章不绕弯子,直接讲清楚这两个函数怎么用、存到哪里、模板里怎么读取,再把我实际开发中踩过的坑一并列出来。适合所有自己折腾 Typecho 主题、或者准备把主题交付给别人使用的朋友。
1. 先理清思路:这俩函数到底帮我们省了什么事
1.1 写死配置的坑到底有多痛
主题开发里有一类需求非常高频:站点 Logo 要能换、侧边栏要能开关、页脚要能自定义文案、每篇文章可能要单独设置头图和摘要。这些信息有一个共同特点,就是“会变”,而且变化的频率还不低。
如果你把这些参数写死在模板文件里,每次改动都要经历一次完整的“找文件、改代码、上传、刷新”流程。自己用还好,顶多是麻烦点;一旦主题要交付给别人,那就成了灾难。对方不懂代码,改一个 Logo 都要来问你,你就要一遍遍远程指导,或者干脆帮对方改。我身边不少写主题的朋友最后都不愿意做定制,就是被这种重复劳动消磨了热情。
Typecho 的解决方案其实很朴素:把“可变信息”抽象成后台配置项和文章字段,让用户在后台直接填,模板只负责读取和渲染。做到这一层,主题的维护成本会直线下降,交付体验也完全不一样。
1.2 themeConfig 与 themeFields 的分工
Typecho 主题函数文件 functions.php 里,有两个官方预留的函数钩子:
themeConfig($form):定义全局主题设置项。后台“控制台-外观-设置外观”页面会加载这个函数,把表单渲染出来。配置项保存后全站任意位置都能读取,适合放 Logo、侧边栏开关、页脚信息、统计代码这些站点级参数。themeFields($layout):定义文章和页面编辑页的自定义字段。后台写文章时会加载这个函数,在编辑器下方“自定义字段”区域显示输入框。每个字段的值与单篇文章绑定,适合放摘要、头图、阅读时长、置顶标记这类文章级参数。
两者的适用场景完全不同,但在实际开发中经常搭配使用。一个管全局,一个管单篇,各司其职。
有人可能会问:Typecho 后台文章编辑页本身不是有个“自定义字段”面板吗?为什么还要用 themeFields?区别在于,系统自带的面板是一次性的,你手动添加的字段只对当前这篇文章生效,换一篇又要重新加。而 themeFields 定义的是“模板级字段”,对所有文章都生效,相当于把字段固化到了编辑界面里,写文章时只需填值,不需要关心字段名是什么。
1.3 开发环境准备
在动手之前,建议先把本地环境搭好。我通常用 PHP 7.4+ 跑 Typecho 1.2 版本,配合 SQLite 数据库,这样在本地改代码、调试非常轻量。重点提醒:functions.php 文件一定要用 UTF-8 无 BOM 格式保存。有 BOM 的话,页面输出前会产生不可见字符,导致“headers already sent”之类的报错。这种问题排查起来极其恶心,因为你看到的报错信息往往和真实原因隔了好几层。
另外,Typecho 官方文档比较精简,themeFields 这类函数几乎没有系统性的文档。我的建议是把 Typecho 源码下载一份放在手边,直接看var/Widget/Themes/Edit.php和var/Widget/Contents/Post/Edit.php这两个文件,比任何教程都管用。后面我会提到具体要关注哪些代码段。
2. 主题设置实战:让后台也能改 Logo、侧边栏、页脚
2.1 表单元素家族一览
Typecho 后台表单采用的是自研的 Widget Helper 体系,所有元素类都继承自Typecho_Widget_Helper_Form_Element。常用的有:
| 元素类 | 对应 HTML | 使用场景 |
|---|---|---|
Typecho_Widget_Helper_Form_Element_Text | input 单行文本 | Logo 地址、统计代码、备案号 |
Typecho_Widget_Helper_Form_Element_Textarea | textarea 多行文本 | 页脚信息、自定义 CSS |
Typecho_Widget_Helper_Form_Element_Select | select 下拉框 | 侧边栏位置、列表风格 |
Typecho_Widget_Helper_Form_Element_Radio | radio 单选 | 开关类配置 |
Typecho_Widget_Helper_Form_Element_Checkbox | checkbox 多选 | 模块启停、功能开关组 |
这几个类的构造函数签名基本一致:
new Typecho_Widget_Helper_Form_Element_Text( $name, // 配置项名称,字符串 $options, // 选项数组,Text/Textarea 传 NULL $value, // 默认值 $label, // 显示在输入框前面的标签文字 $description // 输入框下方的说明文字 )有两个容易被忽略的细节:
$label和$description建议用_t()包一层。这个函数是 Typecho 的翻译辅助函数,虽然很多主题用不到多语言,但习惯性写上没坏处。- Text、Textarea 的
$options传 NULL 即可,只有 Select、Radio、Checkbox 需要传选项数组。
2.2 从空模板到可配置主题
以一个最简单的 functions.php 为例,我要做三件事:站点 Logo、侧边栏开关、页脚自定义文字。完整代码如下:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; function themeConfig($form) { // 站点 Logo $logoUrl = new Typecho_Widget_Helper_Form_Element_Text( 'logoUrl', NULL, '', _t('站点 Logo 地址'), _t('填写完整 URL,留空则使用默认文字标题') ); $form->addInput($logoUrl); // 侧边栏开关 $sidebarSwitch = new Typecho_Widget_Helper_Form_Element_Radio( 'sidebarSwitch', array('0' => '隐藏', '1' => '显示'), '1', _t('侧边栏开关'), _t('选择是否在页面右侧显示侧边栏') ); $form->addInput($sidebarSwitch); // 页脚自定义文字 $footerText = new Typecho_Widget_Helper_Form_Element_Textarea( 'footerText', NULL, 'Powered by Typecho', _t('页脚信息'), _t('支持 HTML,用于填充版权信息等') ); $form->addInput($footerText); }保存后到后台“控制台-外观-设置外观”,就能看到这些配置项。输入内容后点击保存,数据就写入了数据库。整个过程不需要改动任何模板文件。
有一点要提前说明:themeConfig只负责渲染表单,提交和保存都是 Typecho 框架自动处理的。你不需要写任何存储逻辑,这是它比手搓配置页省心的地方。
2.3 模板里如何优雅地读取配置
配置保存后,主题任意模板文件都能通过$this->options读取。$this->options是全局配置对象,在主题任意模板中都可访问,包括 header.php、footer.php、sidebar.php 等被动态加载的模块。
<!-- header.php --> <?php if ($this->options->logoUrl): ?> <a href="<?php $this->options->siteUrl(); ?>"> <img src="<?php echo $this->options->logoUrl; ?>" alt="logo"> </a> <?php else: ?> <a href="<?php $this->options->siteUrl(); ?>"><?php $this->options->title(); ?></a> <?php endif; ?>这里要注意两种写法的区别:
$this->options->logoUrl:获取值,需要配合echo输出。$this->options->logoUrl():直接输出值,相当于echo $this->options->logoUrl;。
Typecho 的 Options 对象重载了魔术方法__call,所以这两种写法都成立。我用得最多的是第二种,少写几个字符。但有个经验:如果配置项不存在,方法调用虽然不会报错,但有些场景下返回的 NULL 会影响后续逻辑,建议在关键位置先用if判断一次。
侧边栏开关的读取同样简单:
<?php if ($this->options->sidebarSwitch == '1'): ?> <?php $this->need('sidebar.php'); ?> <?php endif; ?>这里有个很隐蔽的坑:Radio 和 Select 存储的值是字符串'0'和'1',不是布尔值。所以判断时要用== '1',不能写成=== true。虽然 PHP 的宽松比较能容忍,但写习惯了严格比较的朋友在这里很容易栽跟头。
2.4 配置项的验证与过滤
后台配置项如果直接拼进 HTML 输出,脏数据可能会破坏页面布局,甚至引入 XSS 风险。Typecho 表单元素提供了两个方法用于安全处理:
addRule($rule, $message):添加验证规则。内置规则有required、url、email、integer、xssCheck等。filter($filters):添加过滤回调,提交后按顺序处理值。可传trim、strip_tags等函数名,也可以传回调数组。
我通常在处理 Logo 地址和统计代码时做 URL 校验:
$logoUrl->addRule('url', _t('Logo 地址必须是合法 URL')); $logoUrl->filter('trim', 'strip_tags');这样用户在后台填了一个带引号的非法地址,保存时系统会给出友好提示,而不是跑到前端页面才发现问题。统计代码这类内容我会更谨慎,只用trim过滤,保留 HTML 原样,然后在模板输出时用自定义函数做白名单清洗。
3. 文章自定义字段实战:给每篇内容加上专属信息
3.1 themeFields 的挂载位置与使用姿势
themeFields 和 themeConfig 的写法很像,但挂载对象不同,这是新手最容易踩的坑。themeConfig 接收的是$form对象,用$form->addInput()添加元素;themeFields 接收的是$layout对象,要用$layout->addItem()。如果照着 themeConfig 的套路在 themeFields 里用 addInput,直接报错。
正确的打开方式:
function themeFields($layout) { $summary = new Typecho_Widget_Helper_Form_Element_Text( 'summary', NULL, '', _t('文章摘要'), _t('自定义摘要,留空则自动截取正文前 120 字') ); $layout->addItem($summary); }保存后打开后台“写文章”页面,在编辑器下方找到“自定义字段”区域,展开就能看到“文章摘要”输入框。这个字段对所有文章生效,属于“全局定义、逐篇赋值”的模型。
从代码层面看,Typecho 在加载文章编辑页时,会先检查当前主题的 functions.php 里有没有 themeFields 函数。有的话就创建一个 Layout 对象传入,把所有定义好的字段渲染到编辑页面。这篇文章提交时,Typecho 会把这些字段的值连同文章内容一起保存。
3.2 实用案例:摘要、头图、阅读时长
我自己维护的主题里定义了三个自定义字段:summary(摘要)、cover(头图)、readTime(预计阅读时长)。完整代码如下:
function themeFields($layout) { $summary = new Typecho_Widget_Helper_Form_Element_Text( 'summary', NULL, '', _t('文章摘要'), _t('用于列表页显示,留空则自动截取正文前 120 字') ); $layout->addItem($summary); $cover = new Typecho_Widget_Helper_Form_Element_Text( 'cover', NULL, '', _t('文章头图'), _t('填写图片 URL,列表页会优先展示该图') ); $layout->addItem($cover); $readTime = new Typecho_Widget_Helper_Form_Element_Text( 'readTime', NULL, '', _t('阅读时长'), _t('单位:分钟,例如填 5 表示 5 分钟') ); $layout->addItem($readTime); }如果还想做“置顶”功能,建议用 Select 而不是 Checkbox:
$isTop = new Typecho_Widget_Helper_Form_Element_Select( 'top', array('0' => '不置顶', '1' => '置顶'), '0', _t('置顶设置'), _t('置顶文章将在列表页优先展示') ); $layout->addItem($isTop);为什么不用 Checkbox?因为 Checkbox 未勾选时不会写入字段值,导致读取端要额外做 isset 判断,逻辑绕。Select 和 Radio 永远有值,模板判断简单直接,默认值还能在函数里就定好。
3.3 列表页和详情页怎么输出这些字段
文章自定义字段在模板里的读取方式非常统一,通过$this->fields对象:
<!-- 列表页 index.php 或 archive.php --> <article> <?php if ($this->fields->cover): ?> <img src="<?php echo $this->fields->cover; ?>" alt="<?php $this->title(); ?>"> <?php endif; ?> <h2><a href="<?php $this->permalink(); ?>"><?php $this->title(); ?></a></h2> <p><?php echo $this->fields->summary ? $this->fields->summary : mb_substr(strip_tags($this->content), 0, 120); ?></p> </article>注意:列表页和详情页的用法完全一样。Typecho 在循环输出文章时,会把每篇文章的字段包装成 Fields 对象注入到$this->fields。读取不存在的字段时返回 NULL,所以像封面图这种“可有可无”的字段,输出前务必用 if 判断。
详情页单独输出阅读时长也比较常见:
<?php if ($this->fields->readTime): ?> <span class="read-time">预计阅读 <?php echo intval($this->fields->readTime); ?> 分钟</span> <?php endif; ?>这里我用intval()把值统一转成整数,避免有人在后台填了"5分钟"这种带单位的内容导致前端出现冗余文字。
3.4 区分内容类型的处理思路
themeFields 是全局生效的,这意味着“独立页面”的编辑页也会显示同样的字段。如果我只想让文章有摘要字段,而独立页面不显示,怎么办?
很遗憾,themeFields 本身不能直接按内容类型区分字段。Typecho 的源码里,文章和页面共用了一套编辑控制器,主题字段会被同时注入到两种编辑页面。想通过函数层面区分做不到,只能在模板层做处理:独立页面模板page.php里不读取这些字段就行。
如果你确实需要“文章显示字段 A,页面显示字段 B”,可以换一种思路:分别在post.php和page.php两个模板里写死对应的展示逻辑,后台字段统一由 themeFields 定义,只是前端读取时只挑自己需要的字段。这种做法在业务上完全够用,也避免了给其他内容类型增加多余的输入框。
4. 字段存储原理与常见坑
4.1 主题配置到底存哪张表
后台点“保存设置”之后,主题配置写入 Typecho 的 options 表。表里会有一条记录,name 字段是theme:{主题目录名},value 字段是一个 PHP 序列化后的数组。我实际在数据库里看到的大概是这个样子:
name: theme:mytheme value: a:3:{s:7:"logoUrl";s:21:"https://example.com/a.png";s:14:"sidebarSwitch";s:1:"1";s:10:"footerText";s:18:"Powered by Typecho";}这也是为什么“切换主题再切回来,配置不会丢”——配置是跟着主题目录名走的,只要目录名不变,配置就一直在。
但这里有个隐蔽的坑:如果你用相同的主题目录名,但改了函数里字段的 name,旧配置里已经存了旧字段名,新代码读新字段名时会读到 NULL。改字段名之前,最好先到后台“设置外观”里重新保存一次,或者手动清掉旧配置记录。我处理过好几个“改完配置没生效”的案例,最终原因都是这个。
4.2 文章自定义字段存在哪张表
文章字段存在独立的 fields 表里,表名通常是typecho_fields。每条记录包含:
- cid:文章 ID
- name:字段名
- type:字段类型(str / int / float)
- str_value / int_value / float_value:按类型存放的值
Typecho 写入时会自动判断值的类型:整数存 int_value,浮点数存 float_value,其他都存 str_value。读取时根据 type 自动返回对应字段。所以在模板里 echo$this->fields->readTime时,存进去的是字符串'5',取出来就是字符串'5';如果用 API 直接写入整数 5,取出来就是整数 5。类型不一致可能导致前端拼接时出小问题,建议在输出处用intval或strval统一处理。
4.3 常见问题排查实录
我整理了几个高频问题,基本覆盖了大部分人踩过的坑:
问题1:后台“设置外观”里不显示配置项
优先检查 functions.php 是否有语法错误。Typecho 加载主题函数失败时通常不会报错,而是直接跳到默认界面。可以临时在 functions.php 顶部加两行代码排查:
error_reporting(E_ALL); ini_set('display_errors', '1');另外确认函数名是themeConfig,大小写虽然不影响 PHP 函数调用,但如果你在函数里用了命名空间或者类静态方法,处理起来就会复杂很多。
问题2:themeFields 添加的字段在编辑页看不到
首先检查是不是用了$layout->addItem(),而不是$form->addInput()。这是最常见的误用。其次确认当前启用的主题就是你正在编辑的主题,Typecho 只会加载当前启用主题的 functions.php,其他主题里定义的 themeFields 不会生效。
问题3:字段值取出来总是 NULL
先看数据库里typecho_fields表有没有记录。没有记录说明表单没保存成功,重点检查表单元素 name 是否拼写一致。有记录但前端取不到,检查模板里字段名是否带错了大小写。Typecho 对字段名大小写敏感,summary 和 Summary 是两个完全不同的字段。
问题4:输出字段时页面报错
直接 echo NULL 不会报错,但如果你在模板里对字段值用了mb_substr、strpos等函数,PHP 8+ 会抛出 TypeError。所以处理字段前先判断:
$summary = $this->fields->summary ?: mb_substr(strip_tags($this->content), 0, 120);问题5:配置保存后前台没变化
先清 Typecho 缓存。后台“控制台-设置-常规”里有清空缓存按钮,或者直接删掉var/cache/目录下的缓存文件。还有一个隐蔽原因:你的主题可能开了 CDN 或者浏览器缓存,页面看不到变化并不代表后端没生效,按 Ctrl+F5 强制刷新再试。
5. 从零到一:一个可交付主题的完整示例
5.1 完整的 functions.php 骨架
把前面讲的整合到一起,一个可以交付的主题 functions.php 长这样:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; function themeConfig($form) { $logoUrl = new Typecho_Widget_Helper_Form_Element_Text( 'logoUrl', NULL, '', _t('站点 Logo 地址'), _t('填写完整 URL,留空则使用默认文字标题') ); $logoUrl->addRule('url', _t('Logo 地址必须是合法 URL')); $logoUrl->filter('trim', 'strip_tags'); $form->addInput($logoUrl); $sidebarSwitch = new Typecho_Widget_Helper_Form_Element_Radio( 'sidebarSwitch', array('0' => '隐藏', '1' => '显示'), '1', _t('侧边栏开关'), _t('选择是否在页面右侧显示侧边栏') ); $form->addInput($sidebarSwitch); $footerText = new Typecho_Widget_Helper_Form_Element_Textarea( 'footerText', NULL, 'Powered by Typecho', _t('页脚信息'), _t('支持 HTML,用于填充版权信息等') ); $form->addInput($footerText); } function themeFields($layout) { $summary = new Typecho_Widget_Helper_Form_Element_Text( 'summary', NULL, '', _t('文章摘要'), _t('用于列表页显示,留空则自动截取正文前 120 字') ); $layout->addItem($summary); $cover = new Typecho_Widget_Helper_Form_Element_Text( 'cover', NULL, '', _t('文章头图'), _t('填写图片 URL,列表页会优先展示该图') ); $layout->addItem($cover); $readTime = new Typecho_Widget_Helper_Form_Element_Text( 'readTime', NULL, '', _t('阅读时长'), _t('单位:分钟,例如填 5 表示 5 分钟') ); $layout->addItem($readTime); }5.2 模板文件的调用对照
functions.php 定义了字段之后,模板里的调用位置也给你标出来:
header.php 里输出 Logo 和侧边栏状态:
<header> <?php if ($this->options->logoUrl): ?> <a href="<?php $this->options->siteUrl(); ?>"> <img src="<?php echo $this->options->logoUrl; ?>" alt="logo"> </a> <?php else: ?> <a href="<?php $this->options->siteUrl(); ?>"><?php $this->options->title(); ?></a> <?php endif; ?> </header>index.php / archive.php 列表页输出文章头图和摘要:
<?php while ($this->next()): ?> <article> <?php if ($this->fields->cover): ?> <img src="<?php echo $this->fields->cover; ?>" alt="<?php $this->title(); ?>"> <?php endif; ?> <h2><a href="<?php $this->permalink(); ?>"><?php $this->title(); ?></a></h2> <p> <?php echo $this->fields->summary ? $this->fields->summary : mb_substr(strip_tags($this->content), 0, 120); ?> </p> </article> <?php endwhile; ?>post.php 详情页输出阅读时长和置顶标识:
<?php if ($this->fields->readTime): ?> <span class="read-time">预计阅读 <?php echo intval($this->fields->readTime); ?> 分钟</span> <?php endif; ?> <?php if ($this->fields->top == '1'): ?> <span class="top-tag">置顶</span> <?php endif; ?>footer.php 输出页脚配置文字:
<footer> <?php echo $this->options->footerText; ?> </footer>5.3 主题交付前的自检清单
写完了 functions.php 和模板调用,交付前我一般会过一遍自检清单:
- 后台“设置外观”页面能正常渲染,所有主题配置项都有合理的默认值。
- 写一篇新文章,确认 themeFields 定义的字段出现在“自定义字段”区域,且保存后能正常读取。
- 在 PHP 8+ 环境下跑一遍页面,确认没有使用已废弃的语法。
- 用浏览器开发者工具检查页面源码,避免 Logo 等配置项因转义不当破坏 HTML 结构。
- 关掉 PHP 错误显示,确保前端的体验不会因为调试信息而受影响。
走完这五步,主题基本就能安心交付了。你唯一需要叮嘱使用者的是:配置都藏在后台,字段都在写文章页面,不用碰代码。
我个人在实际使用中还有一个体会:凡是能在后台配置的,尽量让使用者自己填,哪怕你暂时用不到。等主题公开出去,别人能自己在后台改设置,你会感谢当初愿意多写这几百行 functions.php。有朋友跟我抱怨说,他的主题代码量翻了一倍,但其实那多出来的代码,恰恰是主题真正“可交付”的部分。