EP193. “Banner 改用 PHP render_callback 与 content 参数”
🔒 登录后可标记已读📌 并入 EP192 的提醒:EP189 加的链接选择器弹窗(Popover),目前点击 Block 外的其他地方并不会自动关闭。想让它「失焦即关闭」,给 Popover 开始标签加一个 onFocusOutside 属性:
<Popover position="middle center" onFocusOutside={() => setIsLinkPickerVisible(false)}>
把 Banner Block 从「JS 端 save 函数返回写死 HTML」改造成「PHP 端 render_callback 动态渲染」——这是作者反复预告、这一章最推崇的做法。核心动机:JS save 函数的输出会被原样字符串存进数据库,以后想统一调整这个 Block 的 HTML 结构,哪怕只改一个字,也得让编辑过这个 Block 的每一篇文章/模板都重新手动点一次「更新」才能生效;换成 PHP 渲染后,HTML 结构只活在服务器端的 PHP 文件里,数据库只需要存「用了哪个 Block、嵌套了哪些子 Block」这类最精简的信息,以后改 PHP 文件、全站所有用到这个 Block 的地方立刻生效,不需要挨个重新保存。这一讲比之前插件章节的 render_callback 多一层难度:Banner 内部允许嵌套其他 Block(InnerBlocks),PHP 端不仅要拿到 $attributes,还要拿到已经渲染好的嵌套内容 $content。
涉及文件
wp-content/themes/fictional-university-block-theme/functions.php(修改,JSXBlock类支持可选的render_callback)wp-content/themes/fictional-university-block-theme/our-blocks/banner.js(修改,save只保留InnerBlocks.Content)wp-content/themes/fictional-university-block-theme/our-blocks/banner.php(新建)
代码实现
functions.php:JSXBlock 类新增可选的第二参数 $renderCallback,为真时才注册 PHP 回调:
class JSXBlock {
function __construct($name, $renderCallback = null) {
$this->name = $name;
$this->renderCallback = $renderCallback;
add_action('init', [$this, 'onInit']);
}
function ourRenderCallback($attributes, $content) {
ob_start();
require get_theme_file_path("/our-blocks/{$this->name}.php");
return ob_get_clean();
}
function onInit() {
wp_register_script($this->name, get_stylesheet_directory_uri() . "/build/{$this->name}.js", array('wp-blocks', 'wp-editor'));
$ourArgs = array(
'editor_script' => $this->name
);
if ($this->renderCallback) {
$ourArgs['render_callback'] = [$this, 'ourRenderCallback'];
}
register_block_type("ourblocktheme/{$this->name}", $ourArgs);
}
}
new JSXBlock('banner', true);
new JSXBlock('genericheading');
new JSXBlock('genericbutton');
our-blocks/banner.js:save 只返回嵌套内容本身,不再输出任何外层 HTML:
function SaveComponent() {
return <InnerBlocks.Content />
}
our-blocks/banner.php(新建,真正的 HTML 结构从这里动态输出):
<div class="page-banner">
<div class="page-banner__bg-image" style="background-image: url('<?php echo get_theme_file_uri('/images/library-hero.jpg') ?>')"></div>
<div class="page-banner__content container t-center c-white">
<?php echo $content; ?>
</div>
</div>
关键改动点:
- 为什么要走 PHP render_callback:JS
save函数计算出的 HTML 字符串会原封不动存进数据库;以后想调整这个 Block 的结构(哪怕只是加一个 class),已经用过这个 Block 的每一篇内容都不会自动使用新结构,必须手动打开、重新点保存才行——如果这个 Block 被用在几百上千个页面里,这是不可接受的维护成本。换成 PHP 渲染后,数据库里只保留「哪个 Block、嵌套了什么」这份最精简的描述,真正的 HTML 由 PHP 文件在每次请求时动态生成,改一次 PHP 文件全站立刻生效 JSXBlock类新增可选参数$renderCallback = null:给参数一个默认值null,这样不需要 PHP 渲染的 Block(比如genericheading、genericbutton)继续用new JSXBlock('genericheading')这种单参数写法完全不受影响、不用被迫多传一个false;只有 Banner 需要传new JSXBlock('banner', true)显式开启onInit()里用一个中间变量$ourArgs动态决定要不要加render_callback:先建好基础的参数数组(只有editor_script),只有$this->renderCallback为真时才往数组里追加'render_callback' => [$this, 'ourRenderCallback'],最后统一传给register_block_type()——这样同一个类既能服务「纯 JS 输出」的 Block,也能服务「PHP 动态渲染」的 BlockourRenderCallback($attributes, $content)——比插件章节多了第二个参数$content:之前插件章节写的render_callback只接收$attributes(因为那些 Block 都没有嵌套其他 Block);这一讲的 Banner 允许嵌套子 Block(标题、按钮),WordPress 在调用render_callback时,会额外把「所有嵌套子 Block 已经各自渲染好的 HTML 拼在一起」通过第二个参数$content传进来——这正是这一讲比之前难一层的地方ourRenderCallback内部依然是熟悉的ob_start()/require/ob_get_clean()套路:动态require一个跟 Block 名字对应的 PHP 文件(get_theme_file_path("/our-blocks/{$this->name}.php")),这个 PHP 文件内部可以直接使用$attributes/$content这两个变量(因为它是被require进当前函数作用域执行的,能访问到函数内的局部变量)banner.php里的$content直接echo输出:这个变量里已经是「用户在编辑器里实际排列的标题、按钮」渲染好的 HTML 字符串,PHP 端不需要(也没办法)知道具体嵌套了哪些子 Block,只需要把这坨已经处理好的内容原样插入到正确的位置banner.js的save大幅精简:从原本一整段写死的 HTML/JSX(包含page-banner/page-banner__bg-image/page-banner__content三层div)精简成一行<InnerBlocks.Content />——因为外层的 HTML 结构现在完全交给 PHP 文件负责,JS 端的save唯一职责就是把「用户在InnerBlocks里实际添加的子 Block 内容」保存下来,不需要再输出任何外层包装- 验证效果的关键点:数据库里存的内容变「干净」了——之前每次都能在
wp_posts.post_content里看到完整的page-banner/page-banner__bg-image这些外层div标签;改用render_callback之后,数据库里<!-- wp:ourblocktheme/banner -->注释内部只剩嵌套的子 Block,一个外层div都看不到——外层结构完全是 PHP 运行时现算的,不需要持久化 - 改动后旧的 Block 实例必须删除重插:因为旧实例的
save输出里带着现在已经不存在的外层div结构,如果不重新插入,前台会出现「PHP 动态生成的外层 + 数据库里存的旧外层」重复嵌套两层的问题——这跟之前几讲遇到「区块已被修改」冲突提示是类似的性质,只是这次问题更明显(视觉上会看到банner套banner),解决方式同样是删除旧实例、插入新的 - 背景图路径这一讲依然写死(
get_theme_file_uri('/images/library-hero.jpg')),只是把 JS 里原本硬编码的字符串路径换成了 PHP 函数调用——真正支持「上传/选择自定义背景图」的功能留到下一讲
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
render_callback($attributes, $content) | Block 注册选项(回调签名) | Block 允许嵌套子 Block 时,第二个参数 $content 会拿到所有已渲染的嵌套内容 |
get_theme_file_path($相对路径) | WP 内建 function | 获取主题目录下某文件的绝对文件系统路径(配合 require 使用) |
InnerBlocks.Content(无 prop) | React 组件 | 当外层 HTML 完全交给 PHP 处理时,save 只需要返回这一个组件保存嵌套内容 |
常见坑
- Block 允许嵌套子 Block(用了
InnerBlocks)时,render_callback却只接收$attributes一个参数,忘记加$content——拿不到用户实际添加的嵌套内容,PHP 端没法把它们插入正确位置 save函数依然输出完整的外层 HTML 结构,同时又启用了 PHPrender_callback——会导致前台出现「PHP 生成的外层 + 数据库存的外层」重复嵌套两层的问题- 改造成 PHP 渲染之后,没有删除并重新插入旧的 Block 实例——旧实例数据库里存的还是包含完整外层 HTML 的旧版本,会跟新的 PHP 渲染逻辑冲突
JSXBlock类的$renderCallback参数没有给默认值null——所有已有的、不需要 PHP 渲染的 Block(genericheading/genericbutton)实例化时都被迫要多传一个参数,增加不必要的代码改动
[截图:改造成 PHP render_callback 后,打开含旧版 Banner 实例的页面时编辑器弹出的"区块已被修改"冲突提示]
延伸 / 后续讲座会用到
下一讲要给 Banner 的背景图接上真正的「上传/从媒体库选择图片」功能,取代目前写死的图片路径。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 28, EP192, EP193