WP DEVELOP

EP160. “用 post meta 建立双向关系与 useEffect 清理函数”

首页 WordPress 开发课程 新 BLOCK 类型练习 · EP160
约 18 分钟· #EP160#新 BLOCK 类型练习
🔒 登录后可标记已读

目前 Professor 的 ID 只存在 Block 的 attribute 里,这只能让「文章 → 查询 Professor」单向查得到,没办法反过来从「Professor 详情页 → 查出所有提到过它的文章」。这一讲用 WordPress 的 post meta(文章元数据)补上反向关系:每当编辑器里任何一个 Featured Professor Block 的选择发生变化,就重新扫描当前文章里所有这个 Block 类型的实例、汇总出一份去重后的 Professor ID 列表,写入一条 featuredprofessor 元数据(single: false,所以每个 ID 各自存成一条独立记录,而不是挤成一个数组塞进一行)。同时处理两个容易被忽略的边界情况:Block 被删除时也要重新同步一次元数据(用 useEffect 的清理函数机制),以及刚插入还没选值的 Block 不该提前显示上一次选中的预览内容


涉及文件

  • wp-content/plugins/featured-professor/src/index.js (修改)
  • wp-content/plugins/featured-professor/featured-professor.php (修改,注册 meta)

代码实现

featured-professor.phponInit() 里用 register_meta() 注册自定义元数据

function onInit() {
  register_meta('post', 'featuredprofessor', array(
    'show_in_rest' => true,
    'type' => 'number',
    'single' => false
  ));

  wp_register_script('featuredProfessorScript', plugin_dir_url(__FILE__) . 'build/index.js', array('wp-blocks', 'wp-i18n', 'wp-editor'));
  wp_register_style('featuredProfessorStyle', plugin_dir_url(__FILE__) . 'build/index.css');

  register_block_type('ourplugin/featured-professor', array(
    'render_callback' => [$this, 'renderCallback'],
    'editor_script' => 'featuredProfessorScript',
    'editor_style' => 'featuredProfessorStyle'
  ));
}

src/index.js:扫描全部 Block 实例汇总去重、useEffect 清理函数处理删除、只有选过值才触发

function EditComponent(props) {
  const [thePreview, setThePreview] = useState("")

  useEffect(() => {
    if (props.attributes.profId) {
      updateTheMeta()
      async function go() {
        const response = await apiFetch({
          path: `/featuredProfessor/v1/getHTML?profId=${props.attributes.profId}`,
          method: "GET"
        })
        setThePreview(response)
      }
      go()
    }
  }, [props.attributes.profId])

  useEffect(() => {
    return () => {
      updateTheMeta()
    }
  }, [])
  
  function updateTheMeta() {
    const profsForMeta = wp.data.select("core/block-editor")
      .getBlocks()
      .filter(x => x.name == "ourplugin/featured-professor")
      .map(x => x.attributes.profId)
      .filter((x, index, arr) => {
        return arr.indexOf(x) == index
      })
    wp.data.dispatch("core/editor").editPost({meta: {featuredprofessor: profsForMeta}})
  }

  // ...allProfs / 下拉框 / 预览渲染部分不变,见 EP156/EP158-159...
}

关键改动点:

  • post meta 是什么:WordPress 专门有一张数据表(wp_postmeta)存文章的额外元数据,每一条记录是「meta_key(名字)+ post_id(属于哪篇文章)+ meta_value(值)」——只要给同一个 meta_key 存进多条不同 post_id 指向同一个值的记录,就能反过来「用这个值去查有哪些文章存过它」,从而实现反向查询
  • register_meta('post', 'featuredprofessor', array(...)):第一个参数是元数据归属的对象类型(post 表示这是文章级别的元数据,不是评论/用户级别);第二个参数是元数据的名字,必须跟 JS 里 editPost({meta: {...}}) 用的 key 完全一致;第三个参数是选项:
    • show_in_rest => true:让这个元数据能通过 REST API/编辑器数据仓库读写(Gutenberg 编辑器底层就是走 REST API)
    • type => 'number':值的数据类型
    • single => false关键选项——如果是 true,WordPress 会把值当成单个值存(多个值会被迫塞进一个序列化数组存成一行,官方文档提到这样查询性能较差);设成 false 表示「这个 key 可以对应多条独立记录」,一篇文章推荐了 3 位 Professor,数据库里就会有 3 条 meta_key = featuredprofessorpost_id 相同、meta_value 各自是其中一个 Professor ID 的独立记录
  • updateTheMeta() 扫描全部 Block 实例、汇总去重
    • wp.data.select("core/block-editor").getBlocks():拿到当前编辑器页面上所有 Block(不分类型)
    • .filter(x => x.name == "ourplugin/featured-professor"):只保留本插件的 Block 实例——因为页面上可能同时插入好几个 Featured Professor Block,也可能有大量其他类型的 Block(段落、图片等)混在一起
    • .map(x => x.attributes.profId):把过滤后的 Block 对象列表,转换成只保留 profId 属性值的简单数组
    • .filter((x, index, arr) => arr.indexOf(x) == index)去重的标准写法——Array.prototype.filter 的回调可以拿到三个参数:当前值、当前下标、数组本身;arr.indexOf(x) 会返回这个值第一次出现的下标,如果这个下标正好等于当前循环到的下标,说明当前这一项就是它第一次出现的位置(不是重复项),才保留下来;重复出现的项,indexOf 返回的还是第一次出现的下标,跟当前下标对不上,就会被过滤掉
  • wp.data.dispatch("core/editor").editPost({meta: {featuredprofessor: profsForMeta}}):把汇总去重后的 ID 数组写入编辑器的文章元数据状态——这一步只是更新 JS 内存里的数据仓库,还没真正存进数据库,要等用户点击「更新/保存」文章才会真正落库
  • 两个 useEffect 各司其职
    • 第一个:监听 [props.attributes.profId],只要「当前这个 Block 实例」选中的值变化,就调用一次 updateTheMeta()(重新汇总全部实例)加上原本 EP158-159 就有的「请求预览 HTML」逻辑,两者都包在 if (props.attributes.profId) 判断里
    • 第二个:useEffect(() => { return () => { updateTheMeta() } }, [])——依赖数组给空数组 [],意味着这个副作用函数本身只在组件第一次渲染时执行一次;但它 return 了一个函数,这个被返回的函数叫清理函数(cleanup function),React 会在组件被销毁/卸载(也就是这个 Block 实例被用户删除)时自动调用它——用这个机制在「Block 被删除」这个时间点上也重新跑一次 updateTheMeta(),把已经不存在的这个实例的 ID 从元数据列表里正确移除
  • 只有真正选过值才触发这一整套逻辑useEffect 里最外层包一层 if (props.attributes.profId)——不然「刚插入 Block、还没选任何 Professor」时,profIdundefined/空字符串,不应该发请求或者尝试展示预览内容;这一讲修复的另一个体验细节:如果不加这层判断,新插入的 Block 会在选值之前短暂地显示上一次(或者别的实例)的旧预览内容,观感很奇怪

Hook / Function 速查

名称类型用途
register_meta($对象类型, $meta名, $选项数组)WP 内建 function注册一个可以通过 REST API 读写的自定义元数据字段
single: falseregister_meta 选项)配置项允许同一个 meta key 在同一篇文章下存多条独立记录,而不是被迫序列化成一个数组存一行
wp.data.select("core/block-editor").getBlocks()WP 数据仓库方法获取当前编辑器页面上的全部 Block(不分类型),前面讲座(EP145)已经用过
wp.data.dispatch("core/editor").editPost(数据对象)WP 数据仓库方法更新当前文章的元数据/属性(只更新内存,需要用户保存才真正落库)
array.indexOf(value)JS 数组内建方法返回某个值在数组中第一次出现的下标,找不到返回 -1,常用于数组去重判断
useEffect 的清理函数(return () => {...}React 机制组件卸载(或依赖变化前)时自动执行的清理逻辑,这里用来在 Block 删除时同步元数据

常见坑

  • register_metasingle 设成 true——多个值会被迫序列化压缩存成一行,官方提到这样查询性能更差,且没法用「一个 meta 值对应一篇文章」这种简单直接的方式反查关系
  • 直接用 props.attributes.profId 当元数据的值——只考虑了「当前这一个 Block 实例」,忽略了同一篇文章可能插入多个本插件 Block 实例的情况,导致后插入的实例覆盖掉之前实例已经存的值
  • 汇总多个实例的 ID 时忘记去重——同一位 Professor 在同一篇文章里出现多次(比如开头和结尾各插入一次同一个人),数据库里会存进重复的元数据记录
  • 只在 profId 变化时更新元数据,没有处理「Block 被删除」这个事件——删除某个 Block 实例后,它对应的旧 ID 依然残留在元数据里,没有被清理掉
  • useEffect 的清理函数机制理解错误,以为清理函数是在依赖变化*之前*的每一次都触发——依赖数组给空数组 [] 时,主函数体只在挂载时跑一次,清理函数只在组件真正卸载时跑一次,不要跟「每次依赖变化都触发」的那个 useEffect 搞混
  • 没有判断 profId 是否为空就直接执行请求/更新元数据逻辑——会导致刚插入还没选值的 Block 短暂显示旧的/错误的预览内容

延伸 / 后续讲座会用到

下一讲要在 Professor 详情页反过来查询这份 featuredprofessor 元数据,列出所有提到过这位 Professor 的文章。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 26, EP160