WP DEVELOP

EP219. “迁移 Banner:attributes 搬进 block.json 与 wp_localize_script 给回退图”

首页 WordPress 开发课程 现代化 BLOCK 开发标准做法 · EP219
约 23 分钟· #EP219#现代化 BLOCK 开发标准做法
🔒 登录后可标记已读

迁移这一章第一个「真正复杂」的 Block——Banner(背景图选择器 + 嵌套标题/按钮)。跟迁移占位 Block 的流程大体一致,但多了几个新问题:Block 的 attributes 要从 JS 对象搬进 block.json(JSON 里不能写 JS 表达式/变量,只能写字面量,原本用 JS 变量算出来的默认值需要改用别的机制处理);edit/save 分开成独立文件时要注意依赖导入不能漏(比如 InnerBlocks 需要在用到它的每个文件里各自导入一次);旧的 JSXBlock 类里通过构造函数第三参数注入的「主题目录路径」,换一种方式实现——直接在批量注册 Block 的函数里用 wp_localize_script() 挂在官方的 wp-editor 脚本上,不再依赖自定义类。


涉及文件

  • wp-content/themes/fictional-clean-blocks/functions.php (修改,注释旧的 JSXBlock 调用、新增 wp_localize_script + register_block_type_from_metadata;新增/迁移全部 PlaceholderBlock 占位 Block;同时清理 search 板块 render.php 里过时的 while 循环)
  • wp-content/themes/fictional-clean-blocks/src/banner/block.json (新建)
  • wp-content/themes/fictional-clean-blocks/src/banner/edit.js (新建,从旧的 our-blocks/banner.js 搬运)
  • wp-content/themes/fictional-clean-blocks/src/banner/index.js (新建)
  • wp-content/themes/fictional-clean-blocks/src/banner/render.php (新建,从旧的 our-blocks/banner.php 搬运)

代码实现

src/banner/block.json(新建)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "ourblocktheme/banner",
  "title": "Fictional University Banner",
  "supports": {
    "align": ["full"]
  },
  "attributes": {
    "align": { "type": "string", "default": "full" },
    "imgID": { "type": "number" },
    "imgURL": { "type": "string" }
  },
  "editorScript": "file:./index.js",
  "render": "file:./render.php"
}

src/banner/edit.js(完整文件,从旧的 our-blocks/banner.js 搬运,imgURL 的默认回退逻辑改用 useEffect 实现)

import apiFetch from "@wordpress/api-fetch"
import { Button, PanelBody, PanelRow } from "@wordpress/components"
import {
  useBlockProps,
  InnerBlocks,
  InspectorControls,
  MediaUpload,
  MediaUploadCheck
} from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
import { useEffect } from "@wordpress/element"

export default function Edit(props) {
  const blockProps = useBlockProps()

  useEffect(function () {
    if (!props.attributes.imgURL) {
      props.setAttributes({ imgURL: ourThemeData.themePath + "/images/library-hero.jpg" })
    }
  }, [])

  useEffect(
    function () {
      if (props.attributes.imgID) {
        async function go() {
          const response = await apiFetch({
            path: `/wp/v2/media/${props.attributes.imgID}`,
            method: "GET"
          })
          props.setAttributes({ imgURL: response.media_details.sizes.pageBanner.source_url })
        }
        go()
      }
    },
    [props.attributes.imgID]
  )

  function onFileSelect(x) {
    props.setAttributes({ imgID: x.id })
  }

  return (
    <div {...blockProps}>
      <InspectorControls>
        <PanelBody title="Background" initialOpen={true}>
          <PanelRow>
            <MediaUploadCheck>
              <MediaUpload
                onSelect={onFileSelect}
                value={props.attributes.imgID}
                render={({ open }) => {
                  return <Button onClick={open}>Choose Image</Button>
                }}
              />
            </MediaUploadCheck>
          </PanelRow>
        </PanelBody>
      </InspectorControls>
      <div className="page-banner">
        <div
          className="page-banner__bg-image"
          style={{ backgroundImage: `url('${props.attributes.imgURL}')` }}
        ></div>
        <div className="page-banner__content container t-center c-white">
          <InnerBlocks
            allowedBlocks={["ourblocktheme/genericheading", "ourblocktheme/genericbutton"]}
          />
        </div>
      </div>
    </div>
  )
}

src/banner/index.js(完整文件,save 因为只有一行直接内联,不单独拆文件)

import { InnerBlocks } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
import metadata from "./block.json"
import Edit from "./edit"

registerBlockType(metadata.name, {
  edit: Edit,
  save: function () {
    return <InnerBlocks.Content />
  }
})

functions.php:批量注册函数里用 wp_localize_script 挂在官方 wp-editor 脚本上,注入主题目录路径

function our_new_blocks() {
  wp_localize_script('wp-editor', 'ourThemeData', array('themePath' => get_stylesheet_directory_uri()));
  
  register_block_type_from_metadata(__DIR__ . '/build/banner');
  register_block_type_from_metadata(__DIR__ . '/build/footer');
  register_block_type_from_metadata(__DIR__ . '/build/header');
  register_block_type_from_metadata(__DIR__ . '/build/eventsandblogs');
  // ...后续讲座逐个迁移完成的其余 Block,同样一行一个...
}

add_action('init', 'our_new_blocks');

关键改动点:

  • attributes 要从 JS 对象搬进 block.json:跟标题一样,register_block_type_from_metadata() 依赖 block.json 里声明好的 attributes 字段,不再从 JS 里的 registerBlockType() 第二个参数读取——把原本 JS 里 attributes: {...} 这一整段对象搬到 block.json,用双引号包住每个属性名(JSON 语法要求)
  • supports 同样搬进 block.json{"align": ["full"]}),道理一致
  • JSON 不能写 JS 表达式,imgURL 的默认值处理方式要变:原本旧版本 JS 里 imgURL: {type: "string", default: banner.fallbackimage} 这种写法,default 的值是一个动态变量(依赖 wp_localize_script() 注入的全局对象)——但 block.json 是纯 JSON,只能写死字面量,没法在这里引用运行时才存在的变量。这一讲索性去掉 imgURLdefault,改用一个新的 useEffect(依赖数组为空,只在组件首次挂载时运行一次)在 JS 里判断「如果还没有 imgURL,就手动 setAttributes 一个默认值」——效果等价,只是实现方式从「声明式默认值」改成了「运行时判断补齐」
  • edit.js/index.js 分拆时,两个文件各自需要的 import 不能漏InnerBlocksedit.js(编辑器里显示可插入子 Block 的区域)和 index.jssave 函数里的 InnerBlocks.Content两处都要用到,必须各自独立 import一次——这是这一讲踩到的一个真实 bug(一开始只在 edit.js 顶部导入过一次,忘记在 index.js 也导入,导致 save 阶段报「InnerBlocks 未定义」)
  • Edit 函数必须声明 props 参数:这一讲另一个真实踩到的 bug——占位 Block 因为没有任何动态数据,Edit() 不需要参数;但但凡涉及 attributes/setAttributes 的复杂 Block,忘记在 export default function Edit(props) 里声明 props 参数,会直接报「props 未定义」——排查思路是善用浏览器控制台报错信息,作者提到「与其一次性追求完美,不如先跑起来看报错信息,通常是最快的排错方式」
  • save 只有一行代码时,不单独拆 save.js 文件:直接在 index.jsregisterBlockType() 调用里内联一个匿名函数 save: function() { return <InnerBlocks.Content /> }——这是跟「edit 逻辑复杂就拆文件」对称的判断标准:代码量决定要不要拆分,不是一刀切
  • wp_localize_script() 换了一种「挂靠对象」:上一章的 JSXBlock 类是把数据挂在自己那个 Block 专属注册的脚本 handle 上(比如 banner);这一讲改成统一挂在 WordPress 官方内置的 wp-editor 这个脚本 handle 上——因为不管哪个 Block 的编辑器脚本,都必然会依赖 wp-editor 这个基础包,所以把这份「主题目录路径」数据挂在这个通用脚本上,任何 Block 的 JS 都能读到全局变量 ourThemeData.themePath,不需要每个 Block 各自单独注入一份
  • our_new_blocks() 函数(所有 Block 的批量注册入口)里、任何 register_block_type_from_metadata() 调用之前调用一次 wp_localize_script() 即可,不需要为每个 Block 分别调用
  • 顺手清理了「占位 Block」阶段遗留的一个 while 循环问题search 相关的 render.php(无 JS 环境下的搜索结果兜底页面)之前照抄传统主题代码时保留了主查询的 while 循环,导致这个页面完全空白——按 EP208 的规则删掉这个循环后问题解决,这提醒了「迁移到新系统」和「WP 6.4 单篇内容不需要 while 循环」是两件独立的事,即使是最简单的占位 Block 也可能踩到这个坑
  • 调试思路:先让代码跑起来,根据浏览器控制台报错逐条修:这一讲连续出现「InnerBlocks 未定义」「props 未定义」两个错误,作者没有试图一次性把所有东西都想清楚再写代码,而是先保存看报错、照着报错信息定位问题——这是处理这类「从旧代码搬运到新结构」场景时更高效的实际工作方式

Hook / Function 速查

名称类型用途
wp_localize_script('wp-editor', $变量名, $数据数组)WP 内建 function挂靠在官方 wp-editor 脚本上注入全局数据,任何 Block 编辑器 JS 都能读到,不需要各自单独注入
useEffect(callback, [])(首次挂载时判断并补齐默认值)React 用法替代 block.json 里无法用动态值声明的 default,运行时判断补齐

常见坑

  • 试图在 block.jsonattributes.default 里写一个 JS 变量/表达式——JSON 是纯数据格式,不支持这种写法,必须写字面量或者干脆不写默认值、改用 JS 运行时逻辑补齐
  • edit.js/index.js 拆成两个文件后,某个共用的依赖(比如 InnerBlocks)只在其中一个文件里 import——另一个文件用到时会报「未定义」
  • 复杂 Block 的 Edit/Save 函数忘记声明 props 参数——只要用到 attributes/setAttributes 就必须要这个参数,简单占位 Block 不需要,但一旦涉及动态数据就必须补上
  • 只把「已迁移到 block.json」当作唯一要检查的事,忽略了「WP 6.4 单篇内容不需要 while 循环」这条独立规则同样要应用到迁移后的 render.php 文件里

延伸 / 后续讲座会用到

下一讲继续迁移标题(genericheading)和按钮(genericbutton)两个 Block。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 29, EP219