各位在座的全栈开发者、WordPress 架构师,还有那些正在 WordPress 里受苦受难的 CMS 维护人员,大家好。
我是你们今天的讲师,一个曾经把 HTML 直接塞进 PHP 里的老油条,现在转型成了试图把 WordPress 变成“纯粹数据仓库”的激进派。今天我们不聊怎么写漂亮的面包屑导航,也不聊怎么优化 wp_enqueue_scripts 的加载顺序,我们聊点硬核的,聊点能救命——或者说,能让你后端 PHP 进程不那么快崩掉的——东西。
题目很扎心:如何通过剥离原生动态逻辑实现 WordPress 向 Headless 架构的平滑过渡。
听着像天书?别急。我打个比方:WordPress 以前是个“全功能五星级酒店”,前台是个大堂,后厨是个乱糟糟的作坊,服务员(PHP)既端菜又唱歌。你想把它改成“分时租赁公寓”?那你得先把那个只会唱歌的服务员(主题逻辑)赶走,把后厨(数据库查询)和前台(用户界面)切开。
但这手术风险很大,搞不好 WordPress 就原地去世。怎么才能“平滑”?就是今天我们要讲的——无痛剥离。
第一阶段:从“胖子”到“肌肉男”的脱水手术
首先,我们要认清现状。为什么说 WordPress 原生逻辑重?因为它太“胖”了。
在这个架构里,PHP 不光负责算数,还负责展示。一个 wp_query 循环下去,不仅查数据库,还把你整个页面的 HTML 结构、样式表、JavaScript 甚至还有一些乱七八糟的插件钩子输出给浏览器。这就叫耦合。
平滑过渡的第一步,是“切除视觉神经”。
我们要把“渲染逻辑”从“PHP 执行流”里剥离出来。怎么剥离?最粗暴也最有效的方法:让 PHP 变成哑巴。
想象一下,你不想让 PHP 说话,只想让它扔出数据。在 functions.php 里的 wp_head 钩子里,你可以进行一次“暴动”:
// 1. 立即停止输出任何 HTML 到前端
add_action('init', function() {
ob_start(function($buffer) {
// 只要一有输出,直接吞掉,扔进缓存或者直接返回空
// 这里的 $buffer 包含了所有的 HTML、CSS 甚至是 PHP 报错信息
return '';
});
}, 1); // 极高的优先级,防止被任何垃圾插件覆盖
但这还不够。你的主题文件 header.php 和 footer.php 还在苦苦支撑。我们需要修改 template_include。这就像是告诉浏览器:“别去找 header.php 了,前面那个路口左转,去 404.php 溜达一圈。”
// 2. 劫持模板加载器,强制所有请求走 404(或者你指定的空模板)
add_filter('template_include', function($template) {
// 检查当前请求是不是 API 请求
$is_api = strpos($_SERVER['REQUEST_URI'], '/wp-json/') !== false;
if (!$is_api) {
// 非 API 请求,直接返回一个空壳或者 404 模板
// 这样 WP 就不会去渲染那个臃肿的 header.php 和 footer.php 了
return get_template_directory() . '/empty-page.php';
}
return $template;
});
平滑的关键点: 此时此刻,你的 WordPress 依然能跑,WordPress 后台依然能登录,插件依然能工作。但是,前台的用户访问它,只会看到一个白板。这叫什么?这叫“休克疗法”。你在休克中准备新的心脏(API)。
第二阶段:搭建 API 主动脉——REST API 的精细化改造
现在 PHP 不渲染了,它是个空壳。接下来,我们要给这个空壳装上心脏,泵出数据。
WordPress 5.0 以后,REST API 已经成了标配。但原生的 REST API 有个毛病:它太“公家”了。它给你一堆文章、评论、分类,你得自己在 JS 里写逻辑去筛选、去关联。这就像你点外卖,餐厅直接把后厨的垃圾(未清洗的食材)和成品一起端上来了。
我们要做的是:定制化端点。
比如,我们要做一个电商网站。我们不需要“所有文章”,我们需要的是“正在售出的商品”。别在客户端用 SQL 去 Join 表,太慢了,而且不安全。我们要在后端做这件事。
// 3. 注册一个精准打击的 REST 端点
add_action('rest_api_init', function () {
register_rest_route('my/v1', '/products', array(
'methods' => 'GET',
'callback' => 'get_active_products_callback',
'permission_callback' => '__return_true', // 简单起见,开放权限
));
});
function get_active_products_callback($request) {
// 在这里,PHP 只负责干脏活累活:查数据库、组装 JSON
// 不需要输出任何 HTML,因为上面第一步已经把输出流截断了
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'is_active',
'value' => '1',
'compare' => '='
)
),
'orderby' => 'date',
'order' => 'DESC'
);
$query = new WP_Query($args);
$data = array();
while ($query->have_posts()) {
$query->the_post();
// 模拟一下数据映射,甚至可以在这里动态计算价格
$data[] = array(
'id' => get_the_ID(),
'title' => get_the_title(),
'price' => get_field('price'), // 假设你用了 ACF
'slug' => get_post_field('post_name', get_the_ID())
);
}
wp_reset_postdata();
return new WP_REST_Response($data, 200);
}
看到没有?这就是“平滑”的体现。你不需要重写所有代码,你只需要在 functions.php 里加上这几行代码,你的前端就能通过 fetch('/wp-json/my/v1/products') 获取干净、脱敏的数据。
这就是剥离原生逻辑的核心:把“展示”留给前端,把“数据计算”留在后端。
第三阶段:Gutenberg —— 模块化的砖块,不再是 HTML 片段
很多老派开发者在谈 Headless 时,会喊“我要扔掉 Gutenberg!”。大错特错。在剥离原生逻辑的过程中,Gutenberg 是你唯一的盟友。
以前写主题,你需要在 single.php 里硬编码 <div class="entry-content">。如果你的产品有个新的布局,你就得去改 PHP。这叫什么?这叫低级重复劳动。
Gutenberg 的核心价值在于:区块即数据。它把页面拆解成了一个个微小的、可复用的 JSON 数据包。
当你把 WP 变成 Headless 时,你的前端(比如 React/Next.js)拿到的是什么?拿到的不是 HTML 字符串,是一堆包含布局指令、样式、内容数据的 JSON 对象。
// 前端代码示例:接收来自 WP 的数据
async function fetchPageContent() {
const response = await fetch('/wp-json/wp/v2/pages?slug=my-super-page');
const page = await response.json();
// page.content.rendered 现在不是 HTML,而是一个 JSON 结构
// 类似于:
// [
// {"blockName": "core/heading", "attrs": {"level": 1}, "innerHTML": "Hello World"},
// {"blockName": "core/paragraph", "innerHTML": "This is a paragraph."},
// {"blockName": "my-plugin/product-card", "attrs": {"productId": 123}}
// ]
return page;
}
这时候,你的前端开发就变成了搭积木。原本复杂的 PHP 模板逻辑,变成了前端组件库里的 <BlockRenderer />。这种转换是极其平滑的,因为数据结构没有变,变的是获取和渲染的方式。
第四阶段:处理“脏数据”——清理原生插件的输出
剥离原生逻辑后,你会遇到一个巨大的坑:插件的残留物。
有些插件虽然你不用了,但它还在 wp_head 里输出 CSS 和 JS,或者直接往页面里塞广告代码。当你把 PHP 渲染关掉后,这些代码就像幽灵一样出现在 API 响应里,或者直接显示在页面上。
这时候,我们需要一个“清道夫”。
// 4. 启动暴力清理模式
add_action('init', function() {
// 4.1 抓住每一个试图输出的家伙
ob_start(function($buffer) {
// 过滤掉那些不需要的 HTML 标签
$buffer = preg_replace('/<script[^>]*wp-emoji-release.min.js[^>]*></script>/si', '', $buffer);
$buffer = preg_replace('/<link rel=["']https?://fonts.googleapis.com[^>]*>/si', '', $buffer);
// 4.2 如果是 API 请求,保留 JSON,丢弃 HTML
if (strpos($_SERVER['REQUEST_URI'], '/wp-json/') !== false) {
return $buffer;
}
// 4.3 如果是前台页面(虽然上面 template_include 把前台重定向了,但为了防万一)
// 我们直接返回空
return '';
});
}, 1);
这种写法有点“野”,但非常有效。它强迫你在“清洗”数据上动脑筋。这也是 Headless 迁移中非常痛苦但必须的一步——数据污染治理。
第五阶段:GraphQL —— 当你厌倦了 REST 的拼接
如果你觉得 REST API 的路由太长,或者你不想在前端写一堆 fetch('/posts') 然后处理嵌套循环,那么 GraphQL 是下一个台阶。
但是,很多初学者想用 Headless 时,会选择“偷懒方案”:在 PHP 服务器端跑一个 GraphQL 的中间件,或者直接用 WPGraphQL 插件。这其实是倒退!
真正的平滑过渡,应该是把 GraphQL 的解析逻辑放在前端,而不是后端。
怎么实现?利用 WordPress 的 REST API 的 Field Map(字段映射) 功能。你可以在注册端点时,把原本隐藏的数据“挖掘”出来,暴露给前端。
// 5. 把 ACF 的数据“变魔术”一样暴露给 REST API
add_action('rest_api_init', function () {
register_rest_route('my/v1', '/products', array(
'methods' => 'GET',
'callback' => 'get_product_callback',
'schema' => array(
'type' => 'object',
'properties' => array(
'id' => array('type' => 'integer'),
'price' => array('type' => 'number'),
'stock' => array('type' => 'integer'),
// 这里你可以把 meta 数据映射成 JSON
'meta_data' => array(
'type' => 'object',
'properties' => array(
'tags' => array('type' => 'string')
)
)
)
)
));
});
function get_product_callback($request) {
$args = array('post_type' => 'product', 'numberposts' => 1);
$query = new WP_Query($args);
// ... (省略查询逻辑)
$post = $query->posts[0];
// 手动组装数据,确保返回的 JSON 是干净、精确的
// 而不是依赖插件自带的 JSON 序列化
return new WP_REST_Response([
'id' => $post->ID,
'title' => $post->post_title,
'price' => get_field('price', $post->ID),
'attributes' => get_field('attributes', $post->ID) // 拿来就用
], 200);
}
这时候,你的前端代码可以写得非常优雅,像喝白开水一样简单:
// 前端
const res = await fetch('/wp-json/my/v1/products');
const { title, price, attributes } = (await res.json())[0];
第六阶段:媒体与附件的噩梦——图片处理的解耦
说到 Headless,没人敢回避媒体文件。WordPress 的媒体库很方便,但把它和前端解耦是个噩梦。
当你删除主题,前台没了,后台还在。图片 URL 还是指向 http://yoursite.com/wp-content/uploads/...。前端去请求这些图片,没问题,但是……
问题来了:WordPress 的附件元数据是绑定在主题上的。 比如 img_dimensions、image_meta 等信息。一旦你换了前端,这些信息可能就丢了,或者因为主题没有加载 wp_enqueue_media 而丢失。
平滑过渡策略:
- 依赖关系重构: 不要在主题里处理图片逻辑。创建一个名为
wp-media的独立插件(或功能模块),它只负责:- 上传时调用外部 API(如 AWS S3 或阿里云 OSS)进行压缩和存储,而不是存在 WP 的数据库里。
- 在上传时同步更新一个自定义的 JSON 文件到文件系统,或者存储到自定义数据库表。
- 端点伪装: 告诉前端,图片 URL 还是用
yoursite.com/wp-content/uploads没问题,但是尺寸参数?w=800&h=600最好改成直接访问你的对象存储 CDN。
// 6.1 强制上传后不生成缩略图(如果你用的是对象存储)
add_filter('intermediate_image_sizes_advanced', function($sizes) {
// 如果你要用对象存储,这里可以返回空数组,让外部对象存储自己处理
return array();
});
虽然这在初期看起来多此一举(你本来就在 WP 里存了啊),但这才是真正的“解耦”。当有一天你决定把 WP 搬迁到另一个服务器,或者干脆把 WP 换成 Ghost(如果你的内容都存了对象存储),你的迁移成本会降低 90%。
第七阶段:缓存层的介入——让 PHP 彻底躺平
原生 WordPress 最怕大流量。因为它每次请求都要经历:路由 -> 数据库连接 -> 查询 -> 模板渲染 -> 发送 HTML。这一套下来,哪怕只有一个 var_dump,也能把服务器干趴下。
剥离了逻辑后,WP 变成了纯粹的数据库查询器。这时候,缓存才是真正的“平滑过渡”的润滑剂。
我们可以引入 Redis 或者 Memcached。
// 7. Redis 缓存示例
add_action('rest_api_init', function () {
register_rest_route('my/v1', '/hot-news', array(
'methods' => 'GET',
'callback' => 'get_hot_news_callback',
));
});
function get_hot_news_callback() {
$cache_key = 'hot_news_' . get_locale();
// 尝试从 Redis 获取
$data = wp_cache_get($cache_key);
if (false === $data) {
// 缓存未命中,查数据库
$args = array('posts_per_page' => 5, 'orderby' => 'meta_value_num', 'meta_key' => 'views');
$query = new WP_Query($args);
$data = $query->posts;
// 写入缓存,有效期 1 小时
wp_cache_set($cache_key, $data, '', 3600);
}
return new WP_REST_Response($data, 200);
}
你看,当流量冲进来时,WP 根本不用查数据库,直接把 JSON 扔出去。此时,你的 WP 架构已经变成了一个“无状态的 API 服务”。这才是 Headless 的精髓:伸缩性。
第八阶段:工作流的变革——WordPress 现在只是一个 CMS
最后,我们要谈心法。
当你剥离了所有逻辑,你的 WordPress 实例里,还剩下什么?
- 用户管理。
- 内容分类。
- 数据存储。
这很可怕,但也很有趣。这意味着,你的前端开发团队和后端开发团队可以完全并行工作。
前端可以写 React 组件,通过 API 拿数据渲染。
后端可以专心维护 WP,更新文章,调整分类。
他们互不干扰,通过 JSON 格式对话。
这需要什么?
你需要一个“数据模型”文档。前端必须知道“这个端点返回的数据长什么样,有什么字段”。这通常需要编写 OpenAPI (Swagger) 文档。
{
"openapi": "3.0.0",
"info": {
"title": "My Headless API",
"version": "1.0.0"
},
"paths": {
"/products": {
"get": {
"summary": "获取产品列表",
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Product"
}
}
}
}
}
}
}
}
}
}
没有这个文档,过渡就是混乱的。有了这个文档,你的 WordPress 就成了 API 文档背后的真实数据源。
尾声:别回头,向前看
好了,这就是今天的讲座内容。我们回顾一下:
- 休克疗法:用
ob_start和template_include把 PHP 的渲染逻辑切断,把页面变成白板。 - API 纯粹化:用
register_rest_route自定义端点,拒绝原生的“大杂烩”数据。 - Gutenberg 序列化:利用 JSON 结构化数据代替 HTML 片段。
- 数据清洗:清理插件残留的 HTML 脏数据。
- 缓存为王:引入 Redis,让 PHP 变成无状态服务。
- 文档驱动:用 Swagger 定义接口,实现前后端解耦。
很多人觉得 Headless 是颠覆,其实它只是重装系统。你把旧的系统界面卸载了,把内核(数据库和查询逻辑)保留了下来,然后挂载了一个全新的 UI。
这就是平滑过渡。不是一次性的大爆炸,而是一次一次的“外科手术”。
好了,下课。记得把你的 functions.php 备份好,别在测试环境直接干 ob_start 把自己卡死了。
谢谢大家。