WP DEVELOP

EP193. “Banner 改用 PHP render_callback 与 content 参数”

首页 WordPress 开发课程 BLOCK THEME(2024 最佳实践) · EP193
约 17 分钟· #EP193#BLOCK THEME(2024 最佳实践)
🔒 登录后可标记已读

📌 并入 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.phpJSXBlock 类新增可选的第二参数 $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.jssave 只返回嵌套内容本身,不再输出任何外层 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(比如 genericheadinggenericbutton)继续用 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 动态渲染」的 Block
  • ourRenderCallback($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.jssave 大幅精简:从原本一整段写死的 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 结构,同时又启用了 PHP render_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