搜索全站
主题 13 分钟阅读

functions.php 与插件职责划分:哪些逻辑必须放插件

换主题后短代码全失效、文章类型消失?讲清 functions.php 与插件的加载时机差异、一份可执行的职责划分清单,以及 mu-plugins 的正确用法。

U
UFO_PF 2019-10-18 · 阅读 13 分钟
fu 主题
阅读提示
本文约 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 步:处理”已经在主题里写坏了”的历史遗留

  1. 用 wp post-type list 找出主题注册的文章类型;
  2. 把注册代码搬到功能插件,保持 post_type 名称与 rewrite['slug'] 完全一致;
  3. 启用插件、停用主题里的那段代码(注释掉或删掉);
  4. 后台「设置 → 固定链接」点一次保存刷新重写规则;
  5. 用 wp rewrite list 抽查几条规则是否还在,归档页模板的创建方式见自定义文章类型归档模板。
wp post-type list --fields=name,public,has_archive
wp rewrite list --match="/product" 

这套迁移对前台 URL 零影响,因为 URL 由 rewrite['slug'] 决定,与注册代码所在文件无关。

常见踩坑

  1. 在 functions.php 顶层直接执行代码。 文件在 plugins_loaded 之前加载,此时 wpdb 之外的很多 API 还没就绪,直接在顶层调 get_posts() 会出意外。所有逻辑都应挂在钩子上。
  2. 函数不判断存在性导致冲突。 主题里的函数建议包一层:
if ( ! function_exists( 'mmf_breadcrumb' ) ) {
    function mmf_breadcrumb() {
        // …
    }
}
  1. 在插件里用 after_setup_theme。 插件加载时这个钩子已经过了,挂上去不会执行,应改用 init 或 plugins_loaded。
  2. 把子主题的 functions.php 当成”父主题的补丁文件”。 子主题与父主题的 functions.php 都会被加载(子主题先,父主题后),不是覆盖关系。要改父主题行为请用 remove_action / remove_filter,而不是复制整个文件。
  3. 案例: 某企业站把 WooCommerce 的模板覆盖、商品类型注册、支付回调接口全写在一个商业主题的 functions.php 里。主题授权到期后不再更新,安全团队要求更换主题,一换就全线报错:回调接口 404、自定义商品类型归档页空白。花了两周才把代码剥离重建,期间订单回调积压。教训是:只要这段代码”换主题后站点会坏”,它就没有资格待在 functions.php 里。剥离时可参考WordPress 模板层级图解确认覆盖文件该放哪一层。
  4. 功能插件越写越大变成第二个主题。 它依然不能包含模板文件——模板永远属于主题层。
  5. mu-plugins 里写了语法错误。 因为无法从后台停用,出错后可能连后台都进不去,只能通过 FTP 改文件名禁用。上线前务必先在本地验证。

总结

一句判断标准:换主题之后,这段代码消失会让站点”变样”还是”变瘫”?变样放主题,变瘫放插件。实操上,把 add_theme_support、菜单、图片尺寸、资源加载留在 functions.php;把文章类型、分类法、短代码、REST 端点、角色权限、定时任务、第三方集成放进站点专属功能插件;把不可停用的强制约束放 mu-plugins。开始一个新站点时先建好功能插件,成本几乎为零,但能省掉未来一次伤筋动骨的迁移。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。