搜索全站
插件 12 分钟阅读

插件停用和卸载不是一回事:WordPress 残留数据与定时任务的清理清单

停用插件后发现数据库里还躺着它的十几张表和几百条 option?本文讲清停用与卸载的区别、uninstall.php 的正确写法,以及清理残存数据表、定时任务与短代码的完整步骤。

U
UFO_PF 2020-11-03 · 阅读 12 分钟
插件 插件
阅读提示
本文约 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 );
}

常见踩坑

  1. FTP 直接删目录。这样跳过了 uninstall.php,数据永远留在库里。正确顺序永远是后台删除或 wp plugin uninstall。
  2. 在 register_deactivation_hook 里删数据。用户排查冲突时停用一次插件,所有配置就没了,属于严重的插件设计缺陷。
  3. 忘记清理 transient 的 timeout 兄弟记录。transient 有两条 option(值与过期时间),删了一条另一条会变成永久孤儿。
  4. wp cron event delete 删不掉其它任务的同名钩子。删除前用 wp cron event list 完整看一遍 recurrence,避免误删由其它插件注册的常见钩子名。
  5. 多站点未逐站点清理。只清了主站,子站的表和 option 全留下了。用 get_sites() 遍历是必须的。
  6. 直接 DROP TABLE 不看表名共享情况。有些表会被多插件共用,务必先 SHOW CREATE TABLE 确认。
  7. 案例:某站点卸装一款老的社会化分享插件后没清 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'" 对比删除前后的数字。这个数值应当在持续下降——如果它没有变化,说明还有残留没清干净。

U
UFO_PF作者

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

留下你的想法

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