本文约 4,874 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:换了个主题,站点功能全没了
真实事故:某站在主题 A 的 functions.php 里积累了三年的自定义代码——自定义文章类型、短代码、SEO 逻辑、业务接口。某天换了主题 B,所有功能瞬间消失:CPT 后台菜单没了、文章里的短代码显示成原文、接口 404。
而数据其实都还在数据库里(CPT 的文章记录没丢),只是”定义代码”没了,WordPress 认不出它们。
这类事故的根源是把 functions.php 当成了”万能代码收纳箱”。
原理分析:functions.php 是什么,什么时候加载
functions.php 是主题的”函数文件”,在 WordPress 加载流程里的位置是:
wp-config.php → 核心加载 → 插件加载 → 主题 functions.php → init 钩子 → 渲染
关键特性:
- 它属于主题。换主题 = 换一个
functions.php,旧代码全部停用。 - 它在插件之后加载。所以
functions.php里能覆盖插件的钩子,但插件不能覆盖它。 - 子主题的
functions.php先于父主题加载,且父子主题的functions.php不会互相覆盖(不像style.css或模板文件),两个都会执行。 - 每次页面请求都执行(前台和后台都是),所以里面不应该放重量级逻辑。
- 它不能定义只在特定页面才需要的复杂流程——那些应该在模板文件或插件里。
能力边界:functions.php 适合放”和这个主题的外观、展示相关”的代码;不适合放”和站点业务功能相关”的代码。
判断标准很简单:如果换主题后你还想保留这段代码,它就不该写在 functions.php 里。
保姆级实操
一、哪些该放 functions.php,哪些该放插件
| 代码类型 | 放哪里 | 理由 |
|---|---|---|
| 注册菜单、侧边栏、主题支持 | 主题 functions.php |
换主题后由新主题自己定义 |
| 文章字数统计、阅读时间 | 主题 functions.php(或功能插件) |
展示相关 |
| 自定义文章类型(产品、案例) | 功能插件 | 换主题必须保留 |
| 短代码 | 功能插件 | 换主题必须保留 |
| 自定义 REST 端点 | 功能插件 | 业务接口 |
| 第三方 API 对接 | 功能插件 | 业务功能 |
| 自定义登录页样式 | 视情况(客户品牌相关放主题) | — |
| 站点级别的 SEO 逻辑 | 功能插件 | 与主题无关 |
二、做一个自己的功能插件(Site Plugin)
这是 WordPress 社区公认的最佳实践。做法非常简单——一个 PHP 文件就够了。
2.1 创建文件
wp-content/plugins/mmf-site/mmf-site.php:
<?php
/**
* Plugin Name: 迷忙飞站点功能
* Plugin URI: https://mimangfei.com
* Description: 本站自定义功能:文章类型、短代码、接口、业务钩子。与主题无关,换主题不受影响。
* Version: 1.0.0
* Author: mimangfei
* License: GPL-2.0+
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // 禁止直接访问
}
define( 'MMF_SITE_VERSION', '1.0.0' );
define( 'MMF_SITE_DIR', plugin_dir_path( __FILE__ ) );
define( 'MMF_SITE_URL', plugin_dir_url( __FILE__ ) );
// 按功能拆文件,别堆在一个文件里
require_once MMF_SITE_DIR . 'inc/post-types.php';
require_once MMF_SITE_DIR . 'inc/shortcodes.php';
require_once MMF_SITE_DIR . 'inc/rest-api.php';
require_once MMF_SITE_DIR . 'inc/hooks.php';
2.2 拆分模块
inc/post-types.php:
<?php
add_action( 'init', 'mmf_register_case_cpt' );
function mmf_register_case_cpt() {
register_post_type( 'case', array(
'labels' => array( 'name' => '客户案例', 'singular_name' => '案例' ),
'public' => true,
'show_in_rest' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'case' ),
'menu_icon' => 'dashicons-portfolio',
'supports' => array( 'title', 'editor', 'thumbnail' ),
) );
}
inc/shortcodes.php:
<?php
add_shortcode( 'mmf_year', function() {
return date( 'Y' );
} );
2.3 启用
wp plugin activate mmf-site
wp plugin list --field=name --status=active
或在后台插件页点启用。
三、functions.php 的常见致命写法
错误 1:文件末尾有多余的闭合标签或空白行
<?php
// ...代码...
?>
<!-- 这里有个换行 -->
?> 之后的换行会被当成输出发送,导致:
header()已经发送,重定向失效(”Cannot modify header information”);- RSS Feed 的 XML 前面多一个空行,解析失败;
- 后台报 “headers already sent”。
规范:PHP 文件末尾不写 ?>。
错误 2:直接用 $_GET / $_POST 不校验
// 危险
$id = $_GET['id'];
echo get_the_title( $id );
// 正确
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
if ( $id ) {
echo esc_html( get_the_title( $id ) );
}
还要加 nonce 校验(涉及写操作时):
if ( ! isset( $_POST['mmf_nonce'] ) || ! wp_verify_nonce( $_POST['mmf_nonce'], 'mmf_action' ) ) {
wp_die( '非法请求' );
}
错误 3:在顶层写需要 WordPress 已完成加载的逻辑
// 错误:functions.php 加载时 $post 还没准备好
global $post;
if ( $post->ID == 123 ) { ... }
// 正确:挂到钩子上
add_action( 'wp', function() {
if ( is_single( 123 ) ) { ... }
} );
错误 4:函数重名导致致命错误
// 父主题里已有一个 mmf_setup(),子主题再定义一次 → Fatal error: Cannot redeclare
function mmf_setup() { ... }
解决:所有函数加前缀,或者用 function_exists() 包裹:
if ( ! function_exists( 'mmf_setup' ) ) {
function mmf_setup() { ... }
}
四、子主题 vs 功能插件:什么时候用哪个
| 场景 | 方案 |
|---|---|
| 改主题的模板结构(single.php、header.php) | 子主题 |
| 改主题 CSS | 子主题 style.css 或自定义器的”额外 CSS” |
| 覆盖主题的某个函数(可插拔函数) | 子主题 |
| 加自定义文章类型、短代码、接口 | 功能插件 |
| 加主题不涉及的业务逻辑 | 功能插件 |
两者可以并存,而且最佳实践就是并存:子主题管外观,功能插件管功能。
五、代码版本管理建议
把 wp-content/plugins/mmf-site/ 和子主题目录纳入 Git:
cd wp-content/plugins/mmf-site
git init
git add .
git commit -m "init site plugin"
.gitignore 参考:
/node_modules
*.log
.DS_Store
六、临时调试代码的正确位置
调试用的 error_log() 建议只在本地/预发布环境加,生产环境用完立刻删除。或者用条件包裹:
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'debug: ' . print_r( $data, true ) );
}
常见踩坑与失败案例
- 案例一:换主题后 CPT 后台菜单消失、前台短代码变原文。所有”业务功能”代码必须放功能插件。
- 案例二:
functions.php末尾有?>加换行,导致 RSS Feed 报 “XML declaration allowed only at the start of the document”。删除闭合标签解决。 - 主题更新覆盖了改动:直接改父主题的
functions.php,更新时全部丢失。必须建子主题(不涉及模板改动的功能代码则应放插件)。 - 案例三:在
functions.php顶层写wp_redirect(),导致后台都进不去(重定向发生在任何页面)。重定向必须挂在template_redirect钩子上并加条件判断。 - 功能插件停用后内容”消失”:和 CPT 注册代码一样,停用功能插件会让 CPT 定义消失。所以功能插件一旦上线就不要随意停用,或者把 CPT 注册迁到子主题。
- 插件里定义了和主题同名的函数:同样会 Fatal error。所有函数都加
mmf_前缀。 - 把大量代码堆在一个 5000 行的
functions.php:难以维护。按功能拆成多个require文件。
总结
核心判断标准一句话:换主题后还想保留的代码,就不要写在 functions.php 里。主题外观相关(菜单、侧边栏、模板覆盖)放子主题;业务功能相关(CPT、短代码、REST 接口、API 对接、自定义钩子)放自建的”功能插件”——一个带标准插件头的 PHP 文件加几个 require 就够了。写法上三条铁律:PHP 文件末尾不写 ?>;所有输入 absint/sanitize_* 并校验 nonce;所有函数加前缀防止重名。这样换主题、升级、迁移都不会丢东西。
