本文约 4,348 字,预计阅读 11 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:这段代码必须一直跑,不能被任何人关掉
做站点维护时经常遇到一类需求:某个配置和站点功能绑定,它不该出现在后台可停用的列表里。比如客户站上的安全加固规则、把后台默认关闭的 XML-RPC、强制所有用户使用双因素、或者给 WooCommerce 补的一个临时性逻辑补丁。
把这些写成普通插件,问题在于它会出现在后台插件列表里,任何人——包括不懂技术的客户、其他管理员、甚至误操作——都可以一键停用。而一旦被停用,你依赖的行为就消失了,且不会有任何报错提示。
WordPress 为此提供了一个专门的机制:mu-plugins(must-use plugins,必用插件)。放在 wp-content/mu-plugins/ 目录下的 PHP 文件会被 WordPress 自动加载,不出现在常规插件列表里,也无法在后台停用。它不是给普通功能用的,而是给「站点基础设施级」的代码用的。
原理分析:mu-plugins 在加载链上的位置
先看 WordPress 的启动顺序,mu-plugins 的位置非常靠前:
wp-config.php加载,定义数据库等常量;- 核心加载
wp-settings.php; - 加载
wp-content/mu-plugins/下的 PHP 文件(按文件名字母序,仅根目录,不递归子目录); - 触发
muplugins_loaded钩子; - 加载普通插件(读取
active_plugins); - 触发
plugins_loaded钩子; - 加载主题、触发
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 结尾的文件。删除文件则彻底移除。
常见踩坑
-
以为子目录会被自动加载:不会。必须自己写加载器,否则代码永远不执行且不报错。
-
在 mu-plugin 里直接用第三方插件的函数:加载顺序在前,此时普通插件还没加载,会触发
Fatal error: Uncaught Error: Call to undefined function。一律挂到plugins_loaded之后。 -
激活钩子不会触发:mu-plugins 没有「激活」这个动作,
register_activation_hook()在 mu-plugins 里不会执行。需要一次性初始化逻辑时,要用版本号 +update_option()自己做迁移判断。 -
mu-plugin 里出现错误会直接白屏:它加载太早,WordPress 的错误处理还没完全就绪。写错一个字符可能整个站点打不开,只能上服务器改文件。
-
忘记它不显示在
wp plugin list:排查时容易漏掉。 -
案例: 某团队把「禁止非管理员访问后台」的规则写成 mu-plugin,上线后客户反馈部分编辑进不去后台。由于后台无法停用,远程排查花了很长时间,最后只能让客户临时用 FTP 重命名文件。事后改进:所有 mu-plugin 都加了能力判断与一个紧急开关常量——在
wp-config.php里定义MMF_MU_DISABLED即可整体跳过。教训是:越是不能轻易关闭的东西,越要预留紧急出口。 -
用它塞业务逻辑:mu-plugins 只适合基础设施级代码。业务功能放这里,后续没法按站点开关,维护会失控。
总结
适用场景记住三条:站点级强制规则(安全、合规)、需要早于普通插件生效的拦截逻辑、不允许被后台停用的修补代码。不适合放业务功能、可配置的功能、以及需要按站点开关的东西。
写法上三个要点:每个文件开头 defined( 'ABSPATH' ) || exit;;依赖第三方插件的代码一律挂在 plugins_loaded 之后;子目录必须靠加载器引入。
紧急性建议:给所有 mu-plugin 加一个可以在 wp-config.php 里关闭的总开关。关于更深一层的系统级扩展点(比如用 db.php、object-cache.php 替换核心组件),可以看 drop-ins 机制说明;而如果你想给站点加一个可量化的性能日志,mu-plugins 也是最合适的落点,具体见 插件数量与性能。
