本文约 4,988 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:换了主题,站上的产品类型全没了
站点用主题自带的 functions.php 注册了一个”产品”文章类型和十几个短代码,运营两年后想换新主题。切换完成的那一刻,产品归档页 404,文章里几百个 [product id="12"] 短代码变成裸露的方括号文本,后台左侧菜单也少了一大块。内容其实还在数据库里,只是能显示它们的代码随主题一起被卸载了。
这就是所谓的”主题绑定”(theme lock-in)。它不会立刻报错,但会在你最不想动的时候爆发。
原理分析:加载时机决定了职责边界
两者最本质的区别在加载顺序与生命周期:
| 维度 | 主题的 functions.php |
插件 |
|---|---|---|
| 加载时机 | 在 after_setup_theme 之前,仅当前启用主题(及父主题) |
plugins_loaded 之前,所有已启用插件 |
| 生命周期 | 跟随主题,切换即卸载 | 跟随插件,换主题不受影响 |
| 典型钩子 | after_setup_theme、wp_enqueue_scripts |
init、plugins_loaded、rest_api_init |
| 可禁用性 | 只能换主题 | 后台一键停用,便于排查 |
| 自动执行 | 是 | 是(mu-plugins/ 下无法后台停用) |
因为插件加载更早,把 register_post_type() 写在 functions.php 里虽然能跑(挂在 init 上),但一旦换主题,文章类型注册消失 → 重写规则失效 → 归档页与详情页 404,内容还在 wp_posts 表里,只是”取不出来”。同理,短代码的解析函数没了,[xxx] 就原样输出。
一个实用的判断问句:这段代码属于”外观呈现”还是”站点功能”? 换个主题后如果这段代码消失,站点是”变丑了”还是”坏了”?后者必须进插件。
保姆级实操
第 1 步:按清单划分职责
| 逻辑 | 归属 | 理由 |
|---|---|---|
add_theme_support() |
主题 | 声明外观能力 |
register_nav_menus() |
主题 | 菜单位置属于呈现 |
add_image_size() |
主题 | 尺寸与布局绑定 |
wp_enqueue_style/script() |
主题 | 主题自带资源 |
| 模板标签、Walker 类 | 主题 | 输出结构 |
register_post_type() |
插件 | 内容结构,换主题不能丢 |
register_taxonomy() |
插件 | 同上 |
短代码 [xxx] |
插件 | 写进文章正文,主题管不了 |
| 自定义 REST 端点 | 插件 | 接口契约与外观无关 |
| 用户角色与权限 | 插件 | 安全边界 |
| 定时任务 Cron | 插件 | 后台任务 |
| Metabox / 自定义字段组 | 插件 | 数据层 |
| 第三方 API 对接 | 插件 | 业务集成 |
第 2 步:把已有逻辑迁到功能插件
建一个站点专属功能插件,比安装十个通用插件更好维护:
<?php
/**
* Plugin Name: 站点功能(mimangfei 核心)
* Description: 文章类型、短代码、REST 端点等与外观无关的功能
* Version: 1.0.0
* Text Domain: mmf-core
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'init', function () {
register_post_type( 'product', array(
'labels' => array(
'name' => '产品',
'singular_name' => '产品',
),
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => 'product' ),
) );
} );
add_shortcode( 'product', function ( $atts ) {
$atts = shortcode_atts( array( 'id' => 0 ), $atts, 'product' );
$id = absint( $atts['id'] );
if ( ! $id ) {
return '';
}
return sprintf(
'<a class="product-link" href="%s">%s</a>',
esc_url( get_permalink( $id ) ),
esc_html( get_the_title( $id ) )
);
} );
存到 wp-content/plugins/mmf-core/mmf-core.php,后台启用即可。文章类型与短代码从此与主题解耦,换主题后照常工作。
第 3 步:主题里只保留呈现相关代码
<?php
/** 子主题 functions.php */
add_action( 'after_setup_theme', function () {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'html5', array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' ) );
add_theme_support( 'wp-block-styles' );
add_theme_support( 'align-wide' );
add_theme_support( 'responsive-embeds' );
register_nav_menus( array(
'primary' => '主导航',
'footer' => '页脚导航',
) );
} );
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style( 'mmf-main', get_stylesheet_uri(), array(), wp_get_theme()->get( 'Version' ) );
} );
第 4 步:跨主题共用的底层代码放 mu-plugins
wp-content/mu-plugins/ 下的 php 文件会被自动加载,且后台看不到、无法停用,适合放”绝对不能被关掉”的东西,比如安全限制、企业统一的接口配置:
<?php
/** wp-content/mu-plugins/load.php —— 无插件头也能生效 */
// 禁止前台加载编辑器样式、关闭文件编辑等硬性约束
add_filter( 'map_meta_cap', function ( $caps, $cap ) {
if ( in_array( $cap, array( 'edit_themes', 'edit_plugins', 'edit_files' ), true ) ) {
$caps[] = 'do_not_allow';
}
return $caps;
}, 10, 2 );
注意 mu-plugins/ 不会递归加载子目录,需要手动 require:
// wp-content/mu-plugins/load.php
foreach ( glob( __DIR__ . '/inc/*.php' ) as $file ) {
require_once $file;
}
第 5 步:处理”已经在主题里写坏了”的历史遗留
- 用
wp post-type list找出主题注册的文章类型; - 把注册代码搬到功能插件,保持
post_type名称与rewrite['slug']完全一致; - 启用插件、停用主题里的那段代码(注释掉或删掉);
- 后台「设置 → 固定链接」点一次保存刷新重写规则;
- 用
wp rewrite list抽查几条规则是否还在,归档页模板的创建方式见自定义文章类型归档模板。
wp post-type list --fields=name,public,has_archive
wp rewrite list --match="/product"
这套迁移对前台 URL 零影响,因为 URL 由 rewrite['slug'] 决定,与注册代码所在文件无关。
常见踩坑
- 在
functions.php顶层直接执行代码。 文件在plugins_loaded之前加载,此时wpdb之外的很多 API 还没就绪,直接在顶层调get_posts()会出意外。所有逻辑都应挂在钩子上。 - 函数不判断存在性导致冲突。 主题里的函数建议包一层:
if ( ! function_exists( 'mmf_breadcrumb' ) ) {
function mmf_breadcrumb() {
// …
}
}
- 在插件里用
after_setup_theme。 插件加载时这个钩子已经过了,挂上去不会执行,应改用init或plugins_loaded。 - 把子主题的
functions.php当成”父主题的补丁文件”。 子主题与父主题的functions.php都会被加载(子主题先,父主题后),不是覆盖关系。要改父主题行为请用remove_action/remove_filter,而不是复制整个文件。 - 案例: 某企业站把 WooCommerce 的模板覆盖、商品类型注册、支付回调接口全写在一个商业主题的
functions.php里。主题授权到期后不再更新,安全团队要求更换主题,一换就全线报错:回调接口 404、自定义商品类型归档页空白。花了两周才把代码剥离重建,期间订单回调积压。教训是:只要这段代码”换主题后站点会坏”,它就没有资格待在functions.php里。剥离时可参考WordPress 模板层级图解确认覆盖文件该放哪一层。 - 功能插件越写越大变成第二个主题。 它依然不能包含模板文件——模板永远属于主题层。
mu-plugins里写了语法错误。 因为无法从后台停用,出错后可能连后台都进不去,只能通过 FTP 改文件名禁用。上线前务必先在本地验证。
总结
一句判断标准:换主题之后,这段代码消失会让站点”变样”还是”变瘫”?变样放主题,变瘫放插件。实操上,把 add_theme_support、菜单、图片尺寸、资源加载留在 functions.php;把文章类型、分类法、短代码、REST 端点、角色权限、定时任务、第三方集成放进站点专属功能插件;把不可停用的强制约束放 mu-plugins。开始一个新站点时先建好功能插件,成本几乎为零,但能省掉未来一次伤筋动骨的迁移。
