WP DEVELOP

EP145. “标记正确答案与用 wp.data 锁定保存按钮”

首页 WordPress 开发课程 GUTENBERG BLOCK 入门(block.json) · EP145
约 19 分钟· #EP145#GUTENBERG BLOCK 入门(block.json)
🔒 登录后可标记已读

实现「点星标标记正确答案」:新增 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 = falsewp.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-editorgetBlocks()WP 数据仓库方法获取当前编辑器页面上的全部 Block(不分类型)
core/editorlockPostSaving(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