本文约 4,613 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:删掉插件,删不掉它的痕迹
后台点“停用”再点“删除”,大多数人以为这件事就结束了。实际上 WordPress 的两个动作分工不同:停用(deactivate)只是把插件从 active_plugins 里摘出去;删除(delete)才会调用该插件的卸载钩子。而很多插件压根没写卸载钩子,或者只在特定条件下清理部分数据(作者通常保留数据以防用户误删)。
结果是:一个用了两年的插件可能在 wp_options 里留下几百条记录、几张自定义数据表、wp_cron 里几个仍然每五分钟执行一次的任务、以及存入 wp_usermeta 的每个用户的面板设置。这些东西不直接让网站挂掉,但会持续消耗:autoload 数据拉长每个请求,cron 任务制造无意义的负载,陈旧 option 让 wp_options 表膨胀到几百 MB。
本文要解决的,是当你确定不再需要某个插件时,如何把它的痕迹清理干净,以及作为插件开发者,如何写一个负责任的 uninstall.php。这也是插件安全审计自查清单里“更新机製面收敛”的延续动作。
原理分析:WordPress 的两套卸载机制,以及它们各自的限制
WordPress 给插件提供了两个钩子:
| 钩子 | 触发时机 | 局限 |
|---|---|---|
register_deactivation_hook() |
用户点击“停用” | 不适合删数据,用户可能只是临时停用 |
register_uninstall_hook() |
用户在插件列表点“删除” | 必须指向文件中的全局函数 |
uninstall.php 文件 |
同上,优先于上述钩子 | 需自行处理常量判断与权限校验 |
三者的关键差别在于:register_deactivation_hook 会在用户临时停用、切换主题、排查冲突时频繁触发,绝不能在这里删用户数据;而 uninstall.php 是唯一适合做彻底清理的地方,它只在管理员明确执行删除操作时运行一次。
开发者还有一个常被忽视的细节:uninstall.php 运行在一个非常早期的上下文里,此时还没有完整加载 WP 环境。因此文件开头必须自己 defined('WP_UNINSTALL_PLUGIN') 保护,防止被直接访问。
而作为站点管理者,你得接受的现实是:大部分插件懒得写卸载逻辑。所以清理往往要你自己动手。
保姆级实操
第一步:卸载前先全面体检,列出该插件的所有痕迹。
# 1) 找出插件创建的数据表(表名通常带插件前缀)
wp db query "SHOW TABLES LIKE '%wpcf7%';"
# 2) 统计该插件在 options 里留下的记录数
wp db query "SELECT COUNT(*) FROM wp_options WHERE option_name LIKE '%wpcf7%';"
# 3) 查看仍然挂着但属于该插件的定时任务
wp cron event list --fields=hook,next_run_gmt,recurrence | grep -i "wpcf7\|相关插件名"
第二步:确认插件是否提供了官方卸载入口。
# 卸载钩子通常会ὸ放在主文件或单独文件里
grep -rn "register_uninstall_hook\|uninstall.php" wp-content/plugins/目标插件/ --include="*.php" | head
ls wp-content/plugins/目标插件/uninstall.php 2>/dev/null && echo "存在 uninstall.php"
有官方卸载逻辑的插件,请务必通过后台的“删除”按钮触发(而不是 FTP 直接删目录),否则 uninstall.php 不会执行。
第三步:先停用,观察一到两周,再删除。
wp plugin deactivate 目标插件
# 观察期内检查站点是否被依赖
wp eval "echo class_exists('目标主类名') ? '仍有依赖' : '无依赖'; echo PHP_EOL;"
第四步:正式删除并触发卸载钩子。
通过后台插件列表点删除是最稳的做法;命令行下则手动确认:
wp plugin uninstall 目标插件
注意 wp plugin uninstall 要求插件先处于停用状态,否则会提示 Warning: The plugin is active.
第五步:清理残留 option 与 transient。
# 逐条确认后再删:先看内容
wp option get 相关option名
# 删除已确认无用的 option(注意 group 与 autoload 字段)
wp option delete _transient_相关缓存名
wp option delete widget_相关控件名
批量删除要格外谨慎,建议先把表名与 option 列表导出备份:
wp db export before-cleanup.sql
第六步:清理 cron 定时任务残留。
# 精确清除某个钩子上的所有定时任务
wp cron event delete 目标插件_hook名
# 确认已清除
wp cron event list | grep -c "目标插件_hook名"
第七步:处理正文里残留的短代码。
# 统计受影响的内容数量
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%[目标短代码%';"
数量少就手工清理;数量大可以用 wp search-replace 做批量替换,但必须先导出 wp_posts 表。
作为插件开发者,正确写法应当是这样:
<?php
/**
* uninstall.php —— 仅在管理员执行删除时运行一次
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit; // 防止直接访问
}
// 多站点:按站点清理
if ( is_multisite() ) {
$site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
myplugin_clean_all();
restore_current_blog();
}
} else {
myplugin_clean_all();
}
function myplugin_clean_all() {
global $wpdb;
// 1) 删除 option
delete_option( 'myplugin_settings' );
// 2) 删除全部相关 transient
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '\_transient\_myplugin\_%'
OR option_name LIKE '\_transient\_timeout\_myplugin\_%'"
);
// 3) 清除定时任务
wp_clear_scheduled_hook( 'myplugin_daily_sync' );
// 4) 删除自定义数据表
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}myplugin_logs" );
// 5) 清理用户级元数据
delete_metadata( 'user', 0, 'myplugin_panel_status', '', true );
}
常见踩坑
- FTP 直接删目录。这样跳过了
uninstall.php,数据永远留在库里。正确顺序永远是后台删除或wp plugin uninstall。 - 在
register_deactivation_hook里删数据。用户排查冲突时停用一次插件,所有配置就没了,属于严重的插件设计缺陷。 - 忘记清理 transient 的 timeout 兄弟记录。transient 有两条 option(值与过期时间),删了一条另一条会变成永久孤儿。
wp cron event delete删不掉其它任务的同名钩子。删除前用wp cron event list完整看一遍 recurrence,避免误删由其它插件注册的常见钩子名。- 多站点未逐站点清理。只清了主站,子站的表和 option 全留下了。用
get_sites()遍历是必须的。 - 直接 DROP TABLE 不看表名共享情况。有些表会被多插件共用,务必先
SHOW CREATE TABLE确认。 - 案例:某站点卸装一款老的社会化分享插件后没清 cron,该插件的目录文件已被删除,但 wp-cron 调用仍然保留着每小时触发的
social_share_sync。结果是每小时一次的 PHP fatal error + 一封管理员邮件,三个月累积了 2000 多封报错邮件,邮箱爆满导致真正重要的备份失败告警反而被挤掉了。清理方式:wp cron event delete social_share_sync。教训:停用/删除顺序反了,cron 就变成了定时炸弹。
总结
判断标准很简单:停用是开关,删除才是终结(这一层思考与用代码替代插件的 10 个常见场景中关于“谁来维护”的判断一致)。作为使用者,清理顺序固定为“导出备份 → 停用观察 → 后台删除 → 清 option/transient/cron → 处理短代码”;作为开发者,uninstall.php 必须包含 WP_UNINSTALL_PLUGIN 保护、多站点遍历、transient 双记录清理、wp_clear_scheduled_hook 与自定义表 DROP 这五件事。
最后一个实用习惯:每次删除插件后,用 wp db query "SELECT SUM(LENGTH(option_value))/1024 FROM wp_options WHERE autoload='yes'" 对比删除前后的数字。这个数值应当在持续下降——如果它没有变化,说明还有残留没清干净。
