搜索全站
插件 11 分钟阅读

mu-plugins(必用插件)目录的使用场景与正确写法

想让某段代码永远生效、还不被人误停?mu-plugins 目录自动加载、无法在后台停用、优先级高于普通插件。本文讲清它的加载机制、典型场景、写法与四个必须知道的坑。

U
UFO_PF 2017-03-18 · 阅读 11 分钟
mu 插件
阅读提示
本文约 4,348 字,预计阅读 11 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:这段代码必须一直跑,不能被任何人关掉

做站点维护时经常遇到一类需求:某个配置和站点功能绑定,它不该出现在后台可停用的列表里。比如客户站上的安全加固规则、把后台默认关闭的 XML-RPC、强制所有用户使用双因素、或者给 WooCommerce 补的一个临时性逻辑补丁。

把这些写成普通插件,问题在于它会出现在后台插件列表里,任何人——包括不懂技术的客户、其他管理员、甚至误操作——都可以一键停用。而一旦被停用,你依赖的行为就消失了,且不会有任何报错提示。

WordPress 为此提供了一个专门的机制:mu-plugins(must-use plugins,必用插件)。放在 wp-content/mu-plugins/ 目录下的 PHP 文件会被 WordPress 自动加载,不出现在常规插件列表里,也无法在后台停用。它不是给普通功能用的,而是给「站点基础设施级」的代码用的。

原理分析:mu-plugins 在加载链上的位置

先看 WordPress 的启动顺序,mu-plugins 的位置非常靠前:

  1. wp-config.php 加载,定义数据库等常量;
  2. 核心加载 wp-settings.php;
  3. 加载 wp-content/mu-plugins/ 下的 PHP 文件(按文件名字母序,仅根目录,不递归子目录);
  4. 触发 muplugins_loaded 钩子;
  5. 加载普通插件(读取 active_plugins);
  6. 触发 plugins_loaded 钩子;
  7. 加载主题、触发 init 等。

三个由此产生的重要特性:

特性一:加载早于所有普通插件。 这意味着 mu-plugins 里的代码可以对普通插件做拦截——比如在普通插件加载前用 pre_http_request 过滤掉某些请求,或者注册一个普通插件无法覆盖的过滤器。反过来,mu-plugins 里不能假定普通插件的函数已经可用,任何依赖第三方插件的代码都必须挂在 plugins_loaded 之后执行。

特性二:后台不可停用。 WordPress 后台插件页有一个独立的「必用插件」视图,只列出不提供管理。要停用它只能改文件名或删除文件,也就是必须有服务器文件权限。这既是它的价值也是它的风险。

特性三:不递归子目录。 这是最常见的误解。WordPress 只加载 wp-content/mu-plugins/*.php,子目录里的文件不会被自动加载。如果插件放在子目录,必须自己写一个加载器。

相关常量与函数:

名称 类型 作用
WPMU_PLUGIN_DIR 常量 mu-plugins 目录的绝对路径
WPMU_PLUGIN_URL 常量 对应的 URL
muplugins_loaded 动作钩子 mu-plugins 全部加载完成后触发
get_mu_plugins() 函数 读取 mu-plugins 列表(位于 wp-admin/includes/plugin.php)

保姆级实操

第一步:创建目录

wp-content/mu-plugins/ 默认不存在,需要手动创建:

cd wp-content
mkdir -p mu-plugins

第二步:写第一个 mu-plugin

文件名建议用小写字母加连字符,因为加载顺序按字母序,用前缀数字可以控制先后:

<?php
/**
 * Plugin Name: 站点基础规则
 * Description: 强制生效的站点级规则,后台不可停用。
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

// 关闭 REST API 的用户枚举端点(需要登录才能列出用户)
add_filter( 'rest_endpoints', function ( $endpoints ) {
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
} );

// 禁止在文章编辑界面加载表情符号脚本,减少一个前端请求
add_action( 'init', function () {
    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
    remove_action( 'wp_print_styles', 'print_emoji_styles' );
} );

第三步:写一个子目录加载器(可选但推荐)

如果你想把每个功能拆成独立子目录,就用一个加载器统一引入:

<?php
// wp-content/mu-plugins/000-loader.php
defined( 'ABSPATH' ) || exit;

foreach ( glob( WPMU_PLUGIN_DIR . '/*/index.php' ) as $mmf_file ) {
    require_once $mmf_file;
}

这样 mu-plugins/security/index.php、mu-plugins/tweaks/index.php 就会被依次加载。文件名前缀 000- 保证加载器本身最先执行。

第四步:确认它已被 WordPress 识别

# 后台「插件 → 必用插件」查看,或用命令行确认目录存在
ls -1 wp-content/mu-plugins/

# 用 WP-CLI 执行代码,打印 WordPress 实际识别到的 mu-plugins
wp eval 'require_once ABSPATH . "wp-admin/includes/plugin.php"; print_r( array_keys( get_mu_plugins() ) );'

注意 wp plugin list 不会列出 mu-plugins,这是正常的,别误以为没生效。

第五步:处理依赖第三方插件的场景

如果需要判断某个插件是否激活,必须等到 plugins_loaded:

<?php
// wp-content/mu-plugins/100-woo-tweak.php
defined( 'ABSPATH' ) || exit;

add_action( 'plugins_loaded', function () {
    if ( ! class_exists( 'WooCommerce' ) ) {
        return; // 插件不存在就安静退出,绝不报错
    }
    // 此处才安全调用 WooCommerce 的函数与类
    add_filter( 'woocommerce_product_tabs', function ( $tabs ) {
        unset( $tabs['reviews'] );
        return $tabs;
    } );
} );

第六步:部署与版本管理

把整个 mu-plugins/ 目录纳入 Git 管理,通过部署脚本同步到服务器。因为后台无法停用,任何改动都相当于直接上线,必须先在本地或预发布站验证再推生产。

第七步:停用方式

要临时禁用某个 mu-plugin,把文件改名加后缀即可(如 security.php.disabled),WordPress 只加载 .php 结尾的文件。删除文件则彻底移除。

常见踩坑

  1. 以为子目录会被自动加载:不会。必须自己写加载器,否则代码永远不执行且不报错。

  2. 在 mu-plugin 里直接用第三方插件的函数:加载顺序在前,此时普通插件还没加载,会触发 Fatal error: Uncaught Error: Call to undefined function。一律挂到 plugins_loaded 之后。

  3. 激活钩子不会触发:mu-plugins 没有「激活」这个动作,register_activation_hook() 在 mu-plugins 里不会执行。需要一次性初始化逻辑时,要用版本号 + update_option() 自己做迁移判断。

  4. mu-plugin 里出现错误会直接白屏:它加载太早,WordPress 的错误处理还没完全就绪。写错一个字符可能整个站点打不开,只能上服务器改文件。

  5. 忘记它不显示在 wp plugin list:排查时容易漏掉。

  6. 案例: 某团队把「禁止非管理员访问后台」的规则写成 mu-plugin,上线后客户反馈部分编辑进不去后台。由于后台无法停用,远程排查花了很长时间,最后只能让客户临时用 FTP 重命名文件。事后改进:所有 mu-plugin 都加了能力判断与一个紧急开关常量——在 wp-config.php 里定义 MMF_MU_DISABLED 即可整体跳过。教训是:越是不能轻易关闭的东西,越要预留紧急出口。

  7. 用它塞业务逻辑:mu-plugins 只适合基础设施级代码。业务功能放这里,后续没法按站点开关,维护会失控。

总结

适用场景记住三条:站点级强制规则(安全、合规)、需要早于普通插件生效的拦截逻辑、不允许被后台停用的修补代码。不适合放业务功能、可配置的功能、以及需要按站点开关的东西。

写法上三个要点:每个文件开头 defined( 'ABSPATH' ) || exit;;依赖第三方插件的代码一律挂在 plugins_loaded 之后;子目录必须靠加载器引入。

紧急性建议:给所有 mu-plugin 加一个可以在 wp-config.php 里关闭的总开关。关于更深一层的系统级扩展点(比如用 db.php、object-cache.php 替换核心组件),可以看 drop-ins 机制说明;而如果你想给站点加一个可量化的性能日志,mu-plugins 也是最合适的落点,具体见 插件数量与性能。

U
UFO_PF作者

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

留下你的想法

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