EP160. “用 post meta 建立双向关系与 useEffect 清理函数”
🔒 登录后可标记已读目前 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.php:onInit() 里用 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 = featuredprofessor、post_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」时,profId是undefined/空字符串,不应该发请求或者尝试展示预览内容;这一讲修复的另一个体验细节:如果不加这层判断,新插入的 Block 会在选值之前短暂地显示上一次(或者别的实例)的旧预览内容,观感很奇怪
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
register_meta($对象类型, $meta名, $选项数组) | WP 内建 function | 注册一个可以通过 REST API 读写的自定义元数据字段 |
single: false(register_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_meta的single设成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