WordPress 活了 22 年的底气,藏在 wp-includes 的 180 个文件里

发布于 2026年07月09日 14:39 #DevOps#Github 解读 原文链接

WordPress 活了 22 年的底气,藏在 wp-includes 的 180 个文件里 封面图
  • 文件夹约定分离核心与用户代码,升级不覆盖用户数据
  • Hooks 系统基于全局变量实现 22 年 API 稳定,支撑 6 万插件生态
  • drop-in 机制通过文件存在检查替换核心组件,不改核心代码
  • 极致向下兼容支持 PHP 7.4 和 MySQL 5.5,保留 PHP4 兼容代码
  • WP-Cron 伪定时任务依赖访问触发,零配置但低流量延迟

大家好,我是若风。

2026 年 7 月,我打开 GitHub 搜 WordPress,它的主仓库 WordPress/WordPress 还在更新,最新版本号到了 7.1-alpha-62672,21,246 个 Star,12,950 个 Fork。这个项目 2003 年立项,到今天活了 22 年。

22 年是什么概念。2003 年 iPhone 还没发布,React 还有 10 年才出生,PHP 当时是 4.x 版本。同年诞生的开源项目,Mambo 改名 Drupal 后还在挣扎,MT(Movable Type) 基本退出了大众视野,只有 WordPress 不但活着,还占据了全球 43% 的网站。

我好奇的不是它的市场份额,是一个更实在的问题。一个 22 年前的 PHP 项目,凭什么还能往前跑,而不是像那些 contemporaries 一样变成博物馆展品。

答案不在营销文案里,藏在源码的文件结构里。我把 wp-includes/ 目录翻了一遍,找到了几条值得拆开讲的架构哲学。

先搞清一个误会,这个仓库是个镜像

很多人在 GitHub 上搜到 WordPress/WordPress,以为这就是 WordPress 的源码大本营。其实不是。

README 第一句就写得很诚实,这个仓库是 SVN 仓库的 Git 镜像,明确说「请勿发送 Pull Request」。真正的开发在 WordPress/wordpress-develop(3,365 Star) 和 core.trac.wordpress.org 上进行,Pull Request 必须附 Trac ticket 链接才会被受理。

这本身就是一个有意思的选择。2026 年了,绝大多数开源项目早把 GitHub 当唯一老家,WordPress 却坚持 SVN + Trac 的老工作流,GitHub 只做镜像。不是他们不懂 Git,是 22 年的工单历史、贡献者习惯、文档体系都长在 Trac 上,迁移成本远大于收益。这种「能用就不动」的克制,后面会反复出现。

架构写在文件夹名上,不是写在框架里

大部分现代 Web 框架跟你讲架构,会甩给你一套依赖注入容器、一套路由配置文件、一套中间件管道。WordPress 完全不这样,它的架构是「文件夹约定」。

你下载 WordPress 解压,根目录长这样。

wp-load.php          ← 引导入口
wp-settings.php      ← 环境装配
wp-blog-header.php   ← 请求总入口
wp-config-sample.php ← 配置模板
wp-login.php         ← 登录
wp-cron.php          ← 伪定时任务
wp-content/          ← 所有用户代码(插件/主题/上传)
wp-includes/         ← 核心代码
wp-admin/            ← 后台

注意 wp-content/wp-includes/ 的分界。这是整个 WordPress 架构哲学的基石。核心代码在 wp-includes,用户可以碰的东西在 wp-content,升级核心不会覆盖 wp-content,反过来也一样。

这个分界线的威力,得看 wp-load.php 才能体会。它做了一件特别聪明的事,找 wp-config.php 的时候会往上一级目录找。

if ( file_exists( ABSPATH . 'wp-config.php' ) ) {
	require_once ABSPATH . 'wp-config.php';
} elseif ( @file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! @file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
	require_once dirname( ABSPATH ) . '/wp-config.php';
}

看明白了吗。它允许你把配置文件放在 WordPress 根目录的上一层。这意味着你可以把整个 WordPress 核心当成一个可替换的「引擎包」,配置和内容都放在它够不着的地方,升级的时候直接删掉整个 WordPress 目录重传,你的站点数据毫发无伤。

这种设计没有任何框架抽象,就是一个 dirname() 调用加 file_exists 检查。但它解决的问题,是真实世界里几十万站长每次升级都怕把网站搞挂的痛点。

Hooks 系统,一个 22 年前的「事件总线」

如果说文件夹约定是骨架,Hooks 系统就是 WordPress 的神经系统。所有插件、所有主题、所有第三方扩展,都是靠它跟核心对话的。

核心实现在 wp-includes/plugin.php。我读了它的实现,apply_filters 长这样。

function apply_filters( $hook_name, $value, ...$args ) {
	global $wp_filter, $wp_filters, $wp_current_filter;

	if ( ! isset( $wp_filters[ $hook_name ] ) ) {
		$wp_filters[ $hook_name ] = 1;
	} else {
		++$wp_filters[ $hook_name ];
	}

	if ( ! isset( $wp_filter[ $hook_name ] ) ) {
		return $value;
	}

	$wp_current_filter[] = $hook_name;
	array_unshift( $args, $value );
	$filtered = $wp_filter[ $hook_name ]->apply_filters( $value, $args );
	array_pop( $wp_current_filter );

	return $filtered;
}

坦白讲,第一次读完我有点震惊。2026 年大家写事件总线,动不动上 Redis Pub/Sub、Kafka、Event Sourcing。WordPress 的 Hooks 系统,本质就是三个 PHP 全局变量,$wp_filter 存所有注册的回调,$wp_filters 做计数,$wp_current_filter 维护当前调用栈。

$wp_filter 是个关联数组,key 是 hook 名(比如 the_content),value 是 WP_Hook 对象,每个 WP_Hook 内部按 priority(默认 10)排序存回调列表。do_action 的实现几乎一模一样,区别只是 action 不返回值,filter 要把值往后传。

简单到几乎粗暴。但你要想,这套东西 2003 年设计出来,22 年间没有发生过破坏性改动,今天你写的一个 add_action('init', ...),在 WordPress 3.0 上能跑,在 7.1 上也能跑。这种 API 稳定性,在整个软件工业里都极其罕见。

它怎么做到的。答案藏在设计取舍里。Hooks 用字符串做 key,用全局变量做存储,这意味着没有类型约束、没有命名空间隔离、没有编译期检查。放在今天写新框架,这套设计会被 Code Review 骂死。但它换来了一个更珍贵的东西,插件生态的零成本兼容。任何一个 PHP 开发者,不用学任何框架概念,会写函数就能 hook 进 WordPress 的任何执行环节。

这种「牺牲工程整洁度换生态规模」的选择,是 WordPress 能长出 6 万多个插件的根本原因。

db.php drop-in,把可替换性做到数据库层

WordPress 有个更极端的设计,叫 drop-in 机制。我读 wp-includes/load.phprequire_wp_db() 函数时发现的。

function require_wp_db() {
	global $wpdb;

	require_once ABSPATH . WPINC . '/class-wpdb.php';

	if ( file_exists( WP_CONTENT_DIR . '/db.php' ) ) {
		require_once WP_CONTENT_DIR . '/db.php';
	}

	if ( isset( $wpdb ) ) {
		return;
	}

	$wpdb = new wpdb( $dbuser, $dbpassword, $dbname, $dbhost );
}

注意中间那个 if。WordPress 加载数据库类之后,会检查 wp-content/db.php 存不存在。如果存在,就 require 它,然后判断 $wpdb 这个全局变量有没有被赋值,如果有,直接跳过默认的 new wpdb()

这意味着什么。你可以在 wp-content/db.php 里写一个自己的数据库类,赋值给 $wpdb,WordPress 就会用你的实现替代默认的。著名的 HyperDB、LudicrousDB 都是这么干的,它们把 WordPress 的单机 MySQL 连接替换成读写分离、分库分表的分布式方案,核心代码一行都不用改。

这就是 drop-in。不是通过接口抽象、不是通过依赖注入、不是通过配置文件,就是检查一个文件在不在。简单到像一个 hack,但它让 WordPress 在不碰核心的前提下,完成了数据库层的整个替换。

这个思路其实很有迁移价值。我在文末会讲。

21 年的代码还在 PHP 7.4 上跑

现在说点不那么光鲜的。

wp-includes/version.php 里写着当前的版本要求。

$wp_version           = '7.1-alpha-62672';
$required_php_version = '7.4';
$required_mysql_version = '5.5.5';

PHP 7.4 的官方支持 2022 年 11 月就结束了,到今天快 4 年。MySQL 5.5 更是 2015 年就 EOL 了。WordPress 7.1 还在支持这些已经停止安全更新的运行时,这不是疏忽,是刻意的向下兼容策略。

WordPress 的哲学是「让任何一个共享主机上的老站都能升级」,所以它的最低版本要求永远卡在「还有一定比例用户在用」的那条线上。代价是核心代码里到处是版本兼容的妥协。

最典型的是 do_action 里这段,我读到的时候愣了一下。

} elseif ( is_array( $arg[0] ) && 1 === count( $arg[0] ) && isset( $arg[0][0] ) && is_object( $arg[0][0] ) ) {
	// Backward compatibility for PHP4-style passing of `array( &$this )` as action `$arg`.
	$arg[0] = $arg[0][0];
}

注释写得很清楚,「Backward compatibility for PHP4-style passing of array( &$this )」。PHP4 时代,对象是值类型,想把 $this 传给回调得包一层 array(&$this)。PHP5 之后对象默认按引用传递,这个写法就没必要了。但 WordPress 7.1 的代码里,这段兼容逻辑还在。

22 年了,这段代码还在为 PHP4 时代的插件擦屁股。这就是极致向下兼容的真实成本。

WP-Cron,一个把访问当心跳的设计

再看一个很有 WordPress 风格的设计,WP-Cron。

打开 wp-cron.php,文件头部注释第一段就把设计意图交代了。

A pseudo-cron daemon for scheduling WordPress tasks. WP-Cron is triggered when the site receives a visit.

伪 cron。不依赖系统的 crontab,靠网站被访问来触发定时任务。实现方式是每次有访客进来,WordPress 检查有没有到期的定时任务,有就在请求结束后异步跑一下。为了不影响访客体验,它还会调用 fastcgi_finish_request()litespeed_finish_request() 提前关闭 HTTP 连接,然后在后台继续执行。

if ( function_exists( 'fastcgi_finish_request' ) ) {
	fastcgi_finish_request();
} elseif ( function_exists( 'litespeed_finish_request' ) ) {
	litespeed_finish_request();
}

这个设计非常「共享主机友好」。2003 年那会儿,绝大多数 WordPress 跑在 $5/月 的共享主机上,用户根本没有 SSH 权限去配 crontab。WP-Cron 让这些用户也能用定时发布文章、定时备份这些功能,零配置成本。

但代价也很明显。低流量站点的定时任务会延迟,高流量站点的 WP-Cron 会被频繁触发造成性能问题。所以社区的最佳实践是 define('DISABLE_WP_CRON', true); 然后用系统 crontab 定时调用 wp-cron.php。官方文档也承认这个矛盾,文件注释里就写了「this file can be called directly or via a server cron daemon」。

一个设计同时是优点也是缺点,全看你的部署环境。这种「默认值照顾最大多数人,高级用户自己关掉」的策略,跟 Linux 内核那些 sysctl 参数是一个思路。

46 个 REST 控制器,18 个 Block 类

WordPress 不是只会做传统 CMS。我数了一下 wp-includes/rest-api/endpoints/ 目录,有 46 个 class-wp-rest-*-controller.php 文件,覆盖文章、页面、媒体、评论、用户、分类、应用密码、区块模式等几乎所有资源。这意味着 WordPress 其实有一个相当完整的 REST API,完全可以当 Headless CMS 的后端用。

区块系统(Gutenberg)的代码量更夸张,wp-includes/ 里有 18 个 class-wp-block-*.php 文件,从 class-wp-block-parser.php(解析区块语法) 到 class-wp-block-bindings-registry.php(区块数据绑定) 到 class-wp-block-processor.php(区块渲染处理器),是一套完整的、独立于经典编辑器的内容模型。

这跟上一篇写的 Strapi 那种天生 Headless 的 CMS 走的是完全不同的路。Strapi 从第一天就只为 API 而生,WordPress 是从「博客发布平台」一步步长出 REST API 和区块编辑器的。两条路都能走到 2026 年,但 WordPress 这条路的代价是,新系统和老系统在 wp-includes 里并存,class-wp-classic-to-block-menu-converter.php 这种迁移文件的存在,说明它还在消化历史包袱。

db.php 那个老文件,比我想的更有意思

写到这里,我想多花一点篇幅讲 wp-includes/wp-db.php 这个文件,因为它特别能说明 WordPress 处理技术债的方式。

打开这个文件,只有几行。

<?php
/**
 * WordPress database access abstraction class.
 *
 * This file is deprecated, use 'wp-includes/class-wpdb.php' instead.
 *
 * @deprecated 6.1.0
 */
if ( function_exists( '_deprecated_file' ) ) {
	_deprecated_file( basename( __FILE__ ), '6.1.0', 'wp-includes/class-wpdb.php' );
}

require_once __DIR__ . '/class-wpdb.php';

WordPress 6.1 把数据库类从 wp-db.php 重命名成了 class-wpdb.php(遵循 PHP 的 class-*.php 命名约定,方便 spl_autoload)。但老文件没有删,而是保留下来,调用 _deprecated_file() 记录一条废弃警告,然后 require 新文件。

22 年的项目,每一次重命名、每一次文件迁移,都得用这种方式给老插件留一条活路。_deprecated_file_deprecated_function_deprecated_argument_deprecated_hook,WordPress 有一整套废弃标记 API,让插件作者能在日志里看到「这个接口快没了」,提前迁移。

这种「绝不悄悄删东西」的纪律,是 WordPress 插件生态能积累到 6 万多个的根本保障。对比一下 npm 生态里 left-pad 那种一个包删除半个互联网瘫痪的事故,你会理解这种保守的价值。

一个可带走的东西,Convention-as-Architecture

拆完 WordPress,我最大的收获不是某个具体技巧,是一个可以命名的模式。

我管它叫 Convention-as-Architecture(约定即架构)

主流的架构思路是用抽象层解决问题。要可替换,就定义接口。要可扩展,就搞依赖注入。要解耦,就上事件总线中间件。这套思路在 Greenfield 项目里很优雅,但它有一个致命假设,假设你能控制整个代码库、假设所有开发者都懂你的框架。

WordPress 走了完全相反的路。它把架构决策固化成文件夹约定和文件存在性检查。wp-content/db.php 存在就用你的数据库类。wp-config.php 在上一级目录就用那个。Hooks 用字符串 key 注册,任何人不用 import 任何东西就能 hook。这些「约定」没有任何编译期保障,甚至显得有点土,但它们让一个 PHP 初学者都能在 10 分钟内给 WordPress 写一个能用的插件。

这种思路的适用场景其实比你想的广。你做内部工具平台,想让业务团队低成本接入,与其设计一套华丽的插件 SDK,不如定几个「约定文件」放固定位置,平台启动时扫描加载。你做 SaaS 想让用户自定义逻辑,与其上 workflow 引擎,不如给一个 webhooks + 文件约定的组合。

Convention-as-Architecture 不是万能的,它的代价是失去类型安全和编译期检查,代码会变脏,IDE 会抓瞎。但在「生态规模」和「工程整洁度」的取舍里,当你的目标是让尽可能多的人能参与建设时,WordPress 用 22 年证明了一件事。

脏但活着的生态,比干净但没人用的框架,值钱一万倍。

WordPress 源码读起来一点都不性感,满眼是 PHP 全局变量、字符串 hook 名、PHP4 兼容代码。但它提供了一个活生生的样本,让你看到一种完全不同的工程价值观在真实世界里跑了 22 年的结果。如果你正在做一个希望长期演进的产品,值得花一个下午把 wp-load.phpwp-settings.php 的启动链路读一遍,会比读十篇架构文章都有收获。

评论互动

© 2026 王若风的技术博客 · Powered by Astro