WP DEVELOP

EP190-191. “ColorPalette 颜色选择器与「存名字不存色值」的抽象层”

首页 WordPress 开发课程 BLOCK THEME(2024 最佳实践) · EP190-191
约 23 分钟· #EP190-191#BLOCK THEME(2024 最佳实践)
🔒 登录后可标记已读

📌 说明:EP190 的 transcript 在「Let me explain what I mean.」这句话结束,EP191 开头是同一句话的重复接续——两者是同一段实操内容被切成两个文件,合并成一篇笔记。


按钮 Block 这一讲轮到「选颜色」功能,并顺手修了一个上一讲遗留的小 bug(没选链接时按钮整个消失)。核心是用 WordPress 内建的 ColorPalette 组件在右侧检查器面板提供蓝/橙/深橙三个可选颜色。这一讲后半段是作者反复强调的一个设计哲学:数据库里存的应该是「颜色的名字」(比如 "blue"),而不是「颜色的十六进制值」(比如 "#0d3b66"——这样以后设计团队想调整「蓝色」具体是哪个色号,只需要改一处 CSS class 定义,不用满数据库找出所有存了旧色号的记录去替换。为了在「用户点击的是色值」和「要存的是名字」之间转换,用到 WordPress 提供的 getColorObjectByColorValue() 反查工具函数。最后把颜色列表抽成独立文件 inc/ourColors.js,方便以后被多个 Block 复用。


涉及文件

  • wp-content/themes/fictional-university-block-theme/our-blocks/genericbutton.js (修改)
  • wp-content/themes/fictional-university-block-theme/inc/ourColors.js (新建)

代码实现

inc/ourColors.js(新建,抽出来方便复用)

const ourColors = [
  { name: "blue", color: "#0d3b66" },
  { name: "orange", color: "#ee964b" },
  { name: "dark-orange", color: "#f95738" }
]

export default ourColors

our-blocks/genericbutton.js(完整文件)

import ourColors from "../inc/ourColors"
import { link } from "@wordpress/icons"
import { ToolbarGroup, ToolbarButton, Popover, Button, PanelBody, PanelRow, ColorPalette } from "@wordpress/components"
import { RichText, InspectorControls, BlockControls, __experimentalLinkControl as LinkControl, getColorObjectByColorValue } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
import { useState } from "@wordpress/element"

registerBlockType("ourblocktheme/genericbutton", {
  title: "Generic Button",
  attributes: {
    text: { type: "string" },
    size: { type: "string", default: "large" },
    linkObject: { type: "object", default: { url: "" } },
    colorName: { type: "string", default: "blue" }
  },
  edit: EditComponent,
  save: SaveComponent
})

function EditComponent(props) {
  const [isLinkPickerVisible, setIsLinkPickerVisible] = useState(false)

  function handleTextChange(x) {
    props.setAttributes({ text: x })
  }

  function buttonHandler() {
    setIsLinkPickerVisible(prev => !prev)
  }

  function handleLinkChange(newLink) {
    props.setAttributes({ linkObject: newLink })
  }

  const currentColorValue = ourColors.filter(color => {
    return color.name == props.attributes.colorName
  })[0].color

  function handleColorChange(colorCode) {
    // from the hex value that the color palette gives us, we need to find its color name
    const { name } = getColorObjectByColorValue(ourColors, colorCode)
    props.setAttributes({ colorName: name })
  }

  return (
    <>
      <BlockControls>
        <ToolbarGroup>
          <ToolbarButton onClick={buttonHandler} icon={link} />
        </ToolbarGroup>
        <ToolbarGroup>
          <ToolbarButton isPressed={props.attributes.size === "large"} onClick={() => props.setAttributes({ size: "large" })}>
            Large
          </ToolbarButton>
          <ToolbarButton isPressed={props.attributes.size === "medium"} onClick={() => props.setAttributes({ size: "medium" })}>
            Medium
          </ToolbarButton>
          <ToolbarButton isPressed={props.attributes.size === "small"} onClick={() => props.setAttributes({ size: "small" })}>
            Small
          </ToolbarButton>
        </ToolbarGroup>
      </BlockControls>
      <InspectorControls>
        <PanelBody title="Color" initialOpen={true}>
          <PanelRow>
            <ColorPalette disableCustomColors={true} clearable={false} colors={ourColors} value={currentColorValue} onChange={handleColorChange} />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
      <RichText allowedFormats={[]} tagName="a" className={`btn btn--${props.attributes.size} btn--${props.attributes.colorName}`} value={props.attributes.text} onChange={handleTextChange} />
      {isLinkPickerVisible && (
        <Popover position="middle center">
          <LinkControl settings={[]} value={props.attributes.linkObject} onChange={handleLinkChange} />
          <Button variant="primary" onClick={() => setIsLinkPickerVisible(false)} style={{ display: "block", width: "100%" }}>
            Confirm Link
          </Button>
        </Popover>
      )}
    </>
  )
}

function SaveComponent(props) {
  return (
    <a href={props.attributes.linkObject.url} className={`btn btn--${props.attributes.size} btn--${props.attributes.colorName}`}>
      {props.attributes.text}
    </a>
  )
}

关键改动点:

  • 先修一个 bug:linkObject 补默认值linkObject: {type: "object", default: {url: ""}}——之前没有默认值时,如果用户插入按钮却没点链接图标选一个链接,save 组件里的 props.attributes.linkObject.url 会因为 linkObject 整个是 undefined 而报错,导致整个按钮在前台完全不显示;给一个带空字符串 url 的默认对象,就能保证「哪怕没选链接,按钮本身依然正常显示,只是点了没反应」
  • ColorPalette@wordpress/components:WordPress 内建的颜色选择器组件,效果类似一排小圆点色块供用户点选——放进 InspectorControls + PanelBody(标题「Color」)+ PanelRow 这套右侧检查器面板的标准写法里(跟 Featured Professor 章节学过的模式一致)
  • disableCustomColors={true} + clearable={false}——这才是作者选择自己写颜色选择器、而不是用 theme.json 调色板的真正原因disableCustomColors 关掉「点击色块旁边的加号自己挑一个任意颜色」的功能,clearable 关掉「清除已选颜色」的选项——效果是终端用户只能从预先设定好的几个颜色里选,不能自定义任何颜色。作者提到 theme.json 的调色板机制目前没有办法做到这种「完全锁死不给end user 覆盖空间」的效果,所以选颜色这个功能宁愿自己在 Block 代码里手写
  • 核心设计哲学:数据库存「颜色名字」,不存「颜色色值」colorName: {type: "string", default: "blue"}——属性存的是 "blue"/"orange"/"dark-orange" 这样的名字字符串,不是 "#0d3b66" 这样的十六进制色值。作者给的理由:色值只是给用户在选择器界面上「看一眼大概是什么颜色」用的预览手段,真正决定「这个名字对应的颜色到底长什么样」的权威定义应该只存在于 CSS class(btn--blue 等)里;如果反过来把色值存进数据库,以后设计团队想统一微调「蓝色」的具体色号,就得把数据库里存过的所有旧色值挨个找出来替换,而存名字的话只需要改一处 CSS 定义就全站生效
  • ColorPalette 组件本身只认色值,不认名字:这就是为什么需要两个转换方向的胶水代码——
    • 色值 → 名字(用户点击某个颜色时触发):getColorObjectByColorValue(ourColors, colorCode) 是 WordPress 提供的官方工具函数(从 @wordpress/block-editor 导入),传入「颜色列表」和「用户点的色值」,返回匹配的那个颜色对象;用解构 const {name} = getColorObjectByColorValue(...) 直接取出 name 字段,存进 colorName 属性
    • 名字 → 色值ColorPalette 需要用色值来正确高亮显示"当前选中的是哪个"):官方没有提供反过来的工具函数,作者自己用数组 .filter() 写:ourColors.filter(color => color.name == props.attributes.colorName)[0].color——遍历颜色数组,筛出名字匹配当前 colorName 的那一项(结果数组只会有一项),取它的 .color 字段
  • className 从写死的 btn--blue 换成动态拼接 ` btn--${props.attributes.colorName} editRichText)和 save(最终 <a>` 标签)两处都要同步改
  • 颜色列表抽成独立文件 inc/ourColors.js:用 export default ourColors 导出、在需要的 Block 文件里 import ourColors from "../inc/ourColors" 引入——这样如果以后有更多 Block(不只是按钮)也需要用到同一套品牌色,不用在每个文件里各自重复定义一遍
  • 改动 save 输出结构后,已插入的旧 Block 实例会再次出现「区块已被修改」的冲突提示:这是这几讲反复遇到的问题(JS 端的 save 函数决定的输出内容一旦变化,跟数据库里存的旧版本对不上就会报冲突),作者借这个机会又一次预告:马上要学的 PHP render_callback 方式能从根本上避免这个烦恼

Hook / Function 速查

名称类型用途
ColorPalette@wordpress/componentsReact 组件提供一组预设颜色供用户点选,disableCustomColors/clearable 控制是否允许自定义/清除
getColorObjectByColorValue(颜色数组, 色值)@wordpress/block-editorWP 官方工具函数根据色值反查对应的颜色对象(含名字),官方没有提供反方向(名字→色值)的对应函数

常见坑

  • 给对象类型的属性(如 linkObject)不设默认值——用户没有主动设置这个属性时,save 函数里访问它的子字段会报错,导致整个 Block 在前台消失
  • 数据库直接存十六进制颜色色值而不是颜色名字——以后统一调整某个品牌色时,没法一次性生效,需要挨个查找替换所有旧记录
  • 忘记同时处理「色值→名字」和「名字→色值」两个方向的转换——ColorPalette 组件的 value 需要色值才能正确高亮当前选中项,onChange 拿到的却是色值需要转成名字存起来,两个方向缺一个都会导致选择器要么不能正确回显、要么存错数据
  • 只用 theme.json 的调色板机制却指望能完全禁止终端用户自定义颜色——录课当时的 theme.json 机制无法做到这一点,需要终端用户完全不能覆盖颜色选项时应该像这里一样自己写 Block 代码

[截图:点击按钮 Block 后右侧检查器面板的 Color 分组,展开显示蓝/橙/深橙三个色块的 ColorPalette 选择器]


延伸 / 后续讲座会用到

下一讲要回到 Banner Block,学习作者更推荐的 PHP render_callback 方式,同时加上「上传/选择背景图」的功能。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 28, EP190, EP191