EP145. “标记正确答案与用 wp.data 锁定保存按钮”
🔒 登录后可标记已读实现「点星标标记正确答案」:新增 correctAnswer 数字类型属性记录正确答案的下标,星标图标根据是否命中当前下标动态切换实心/空心样式,删除某一答案时如果它正好是当前正确答案,要把 correctAnswer 重置为 undefined。这一讲最有意思的部分是从「单个 Block」跳到「整个编辑器全局」的视角:用 wp.data.subscribe() 监听编辑器任何数据变化,检查页面上所有「本插件的 Block」有没有still未标记正确答案的,有就用 lockPostSaving() 锁住保存按钮,没有就 unlockPostSaving() 解锁——因为一个页面可能同时插入好几个本插件的 Block 实例,判断「能不能保存」不能只看某一个 Block 自己的状态。最后用立即执行函数表达式(IIFE)把这段全局监听逻辑的变量隔离起来,避免污染全局作用域。
涉及文件
wp-content/plugins/are-you-paying-attention/src/index.js(修改)
代码实现
src/index.js(完整文件):
import "./index.scss"
import {TextControl, Flex, FlexBlock, FlexItem, Button, Icon} from "@wordpress/components"
(function() {
let locked = false
wp.data.subscribe(function() {
const results = wp.data.select("core/block-editor").getBlocks().filter(function(block) {
return block.name == "ourplugin/are-you-paying-attention" && block.attributes.correctAnswer == undefined
})
if (results.length && locked == false) {
locked = true
wp.data.dispatch("core/editor").lockPostSaving("noanswer")
}
if (!results.length && locked) {
locked = false
wp.data.dispatch("core/editor").unlockPostSaving("noanswer")
}
})
})()
wp.blocks.registerBlockType("ourplugin/are-you-paying-attention", {
title: "Are You Paying Attention?",
icon: "smiley",
category: "common",
attributes: {
question: {type: "string"},
answers: {type: "array", default: [""]},
correctAnswer: {type: "number", default: undefined}
},
edit: EditComponent,
save: function (props) {
return null
}
})
function EditComponent (props) {
function updateQuestion(value) {
props.setAttributes({question: value})
}
function deleteAnswer(indexToDelete) {
const newAnswers = props.attributes.answers.filter(function(x, index) {
return index != indexToDelete
})
props.setAttributes({answers: newAnswers})
if (indexToDelete == props.attributes.correctAnswer) {
props.setAttributes({correctAnswer: undefined})
}
}
function markAsCorrect(index) {
props.setAttributes({correctAnswer: index})
}
return (
<div className="paying-attention-edit-block">
<TextControl label="Question:" value={props.attributes.question} onChange={updateQuestion} style={{fontSize: "20px"}} />
<p style={{fontSize: "13px", margin: "20px 0 8px 0"}}>Answers:</p>
{props.attributes.answers.map(function (answer, index) {
return (
<Flex>
<FlexBlock>
<TextControl value={answer} onChange={newValue => {
const newAnswers = props.attributes.answers.concat([])
newAnswers[index] = newValue
props.setAttributes({answers: newAnswers})
}} />
</FlexBlock>
<FlexItem>
<Button onClick={() => markAsCorrect(index)}>
<Icon className="mark-as-correct" icon={props.attributes.correctAnswer == index ? "star-filled" : "star-empty"} />
</Button>
</FlexItem>
<FlexItem>
<Button isLink className="attention-delete" onClick={() => deleteAnswer(index)}>Delete</Button>
</FlexItem>
</Flex>
)
})}
<Button isPrimary onClick={() => {
props.setAttributes({answers: props.attributes.answers.concat([""])})
}}>Add another answer</Button>
</div>
)
}
关键改动点:
correctAnswer: {type: "number", default: undefined}:默认值故意用undefined而不是0——如果用0当默认值,会跟「第一个答案(下标 0)确实是正确答案」这个合法状态混淆;用undefined能干净地表示「还没有选择任何正确答案」- 星标图标用三元运算符(
条件 ? 值A : 值B)动态决定icon是"star-filled"还是"star-empty"——JSX 的{}里只能放「表达式」,不能直接写if/else这种「语句」,三元运算符是 JSX 里实现条件逻辑最常见的写法 deleteAnswer()里多了一段判断:如果要删除的下标正好等于当前correctAnswer,删除的同时也要把correctAnswer重置为undefined——不然删掉正确答案后,correctAnswer这个数字依然指向原数组里现在变成了别的答案的下标,会张冠李戴wp.data是 WordPress 提供的全局数据仓库工具(Redux 风格),wp.data.select(store).method()用来读取数据,wp.data.dispatch(store).method()用来触发动作/写入数据——这一讲用到的core/block-editor这个 store 管的是「当前编辑器里所有 Block」,core/editor这个 store 管的是「整篇文章的保存/发布状态」wp.data.subscribe(callback):只要编辑器里任何数据发生变化(不只是这个插件的 Block,任何 Block 的任何改动),都会重新调用一次这个回调——保证每次检查用的都是最新状态wp.data.select("core/block-editor").getBlocks()返回页面上所有 Block 的数组(不分类型),用.filter()先按block.name筛出属于本插件的 Block,再检查这些 Block 里有没有correctAnswer还是undefined的——results.length大于 0 就说明「至少有一个本插件的 Block 还没选正确答案」wp.data.dispatch("core/editor").lockPostSaving("标识符")/.unlockPostSaving("标识符"):这一对函数用一个自定义字符串(这里是"noanswer")当「锁的名字」,方便区分「是谁锁的、后续解锁时对应哪一把锁」,用同一个名字调用解锁才能对应上;配合一个模块内的locked布尔变量,避免重复调用锁定/解锁(if (... && locked == false)只在状态真的发生变化时才触发一次)- 立即执行函数表达式(IIFE):
(function() { ... })()——把let locked = false和wp.data.subscribe(...)这段全局监听逻辑包在一个立刻执行、执行完就销毁的匿名函数里,locked这个变量因此只存在于这个函数的作用域内,不会泄漏到全局作用域跟别的插件/WordPress 核心变量的同名变量打架——这跟之前给普通函数取个足够独特名字的做法(ourStartFunction)相比,是更彻底、不需要绞尽脑汁想名字的写法
[截图:Gutenberg 编辑器里点击星标把某个答案标为正确答案后,星标变实心的效果,以及还有 Block 未标正确答案时顶部发布/保存按钮被锁住的状态]
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
wp.data.select(store) | WordPress 全局数据 API | 读取指定数据仓库(store)里的数据 |
wp.data.dispatch(store) | WordPress 全局数据 API | 触发指定数据仓库的动作(写入/修改状态) |
wp.data.subscribe(callback) | WordPress 全局数据 API | 监听编辑器任何数据变化,每次变化都重新调用回调 |
core/block-editor 的 getBlocks() | WP 数据仓库方法 | 获取当前编辑器页面上的全部 Block(不分类型) |
core/editor 的 lockPostSaving(id) / unlockPostSaving(id) | WP 数据仓库方法 | 用指定标识锁定/解锁文章保存按钮 |
| IIFE(立即执行函数表达式) | JS 语法模式 | (function(){...})(),隔离变量作用域,避免污染全局命名空间 |
常见坑
correctAnswer的默认值设成0——会跟「第一项确实是正确答案」这个合法状态混淆,无法准确表达「尚未选择」- 删除答案时忘记同步检查并重置
correctAnswer——删掉的正好是当前标记为正确的那一项后,correctAnswer这个下标依然指向数组里现在的另一个答案,造成错位 - 只检查「当前这一个 Block 有没有选正确答案」就决定要不要锁保存按钮——页面上如果有多个本插件的 Block 实例,必须遍历全部实例统一判断,否则会出现「明明还有一个没选、却能保存」或反过来的问题
lockPostSaving/unlockPostSaving重复调用同名锁而不检查当前状态——虽然 WordPress 本身能处理,但配合外部locked变量做一次性触发,代码逻辑更清晰、也避免不必要的重复调用
延伸 / 后续讲座会用到
编辑器端的功能全部完成,下一讲开始做前台:访客看到的 Quiz 长什么样、点击答案后怎么判断对错、怎么给出视觉反馈。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 25, EP145