WP DEVELOP

EP189. “LinkControl 链接选择器与 useState 驱动 Popover”

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

给按钮 Block 加上真正的「选链接」功能:点击悬浮工具栏上的链接图标,弹出一个可以搜索站内文章/页面、也能直接输入任意 URL 的选择器(复用 WordPress 官方现成的 LinkControl 组件,不用自己写搜索逻辑)。「弹窗是否可见」这个状态用 React 的 useState(而不是 Block 的 attributes)管理——因为这纯粹是编辑器里临时的 UI 状态,用户下次打开页面时不需要记得「上次链接选择器是不是开着的」,不该跟着内容一起存进数据库。选好的链接对象(包含 URL、标题、文章 ID 等)存进新增的 linkObject 属性,最终 save 阶段只取用其中的 url 字段。


涉及文件

  • wp-content/themes/fictional-university-block-theme/our-blocks/genericbutton.js (修改)
  • wp-content/themes/fictional-university-block-theme/package.json (新增依赖 @wordpress/icons

代码实现

终端命令:安装链接图标专用的包(@wordpress/scripts 无法像其他包一样自动识别别名)

npm install @wordpress/icons

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

import { link } from "@wordpress/icons"
import { ToolbarGroup, ToolbarButton, Popover, Button } from "@wordpress/components"
import { RichText, BlockControls, __experimentalLinkControl as LinkControl } 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" }
  },
  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 })
  }

  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>
      <RichText allowedFormats={[]} tagName="a" className={`btn btn--${props.attributes.size} btn--blue`} 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--blue`}>
      {props.attributes.text}
    </a>
  )
}

关键改动点:

  • 新增 linkObject 属性,类型是 object:WordPress 的 LinkControl 选中一个链接后,返回的不只是一个 URL 字符串,而是一整个对象(包含 URL、标题、文章 ID、文章类型等信息)——这里选择把整个对象原样存下来,而不是只挑 url 一个字段存成字符串,保留了以后想用其他信息(比如按 ID 而不是写死的 URL 做更稳健的关联)的可能性
  • isLinkPickerVisibleuseState 而不是 Block attributes 管理:这是这一讲刻意强调的设计判断——「链接选择弹窗现在是否可见」纯粹是一个临时的编辑器 UI 状态,用户离开页面重新打开时,没有必要记得上次弹窗是开着的,所以不应该存进数据库(也就是不该做成 attributes 的一部分),用组件本地的 React 状态就够了
  • import { useState } from "@wordpress/element":在 WordPress 环境下,React 是通过 @wordpress/element 这个包「转手」提供给开发者的,而不是直接 import React from 'react'——作者推测这是 WordPress 特意设计的一层抽象,万一未来官方想换掉 React、改用别的前端库,开发者只依赖 @wordpress/element 这个抽象层,而不是直接依赖 React 本身,替换起来影响更小
  • ToolbarButton onClick={buttonHandler} icon={link}——只给图标不给文字的工具栏按钮:跟大/中/小那三个纯文字按钮不同,这里用 icon prop 传入一个图标组件(link,从 @wordpress/icons 引入的官方链接图标),而不是在标签内容里写文字
  • @wordpress/icons 是一个例外,需要真正 npm install:这门课其他 WordPress 包(@wordpress/blocks@wordpress/block-editor@wordpress/components@wordpress/element 等)都能被 @wordpress/scripts 的 Webpack 配置自动识别、转成读取浏览器全局已加载的对应模块,不需要真的执行安装;但 @wordpress/icons 这个包目前没有被那份自动识别的清单覆盖到,所以必须先老老实实 npm install @wordpress/icons,装完之后要重启 npm run start 任务
  • buttonHandler() 用「翻转前一个值」的写法实现开关切换setIsLinkPickerVisible(prev => !prev)——给 setState 系列函数传一个「基于前一个值计算新值」的函数,而不是直接给一个写死的新值,这样点击同一个按钮就能在「显示/隐藏」之间来回切换
  • {isLinkPickerVisible && (<Popover>...</Popover>)}:这门课反复用过的 JSX 条件渲染写法——&& 短路运算,只有左边条件为真时,右边的 JSX 才会被渲染;为假时 && 整体是 false,React 直接跳过不渲染任何东西
  • Popover@wordpress/components:现成的「悬浮弹层」组件,position="middle center" 控制弹层相对于触发它的元素居中显示在下方——不需要自己写定位/动画的 CSS
  • __experimentalLinkControl as LinkControl——起了个更好读的别名:这是 WordPress 内建的「链接选择器」组件,支持搜索站内文章/页面、也支持直接粘贴/输入任意 URL;因为它的原始导出名字带着 __experimental 前缀(说明这是当时还处于「实验性」阶段、以后可能改名或调整的 API),作者提前用 as LinkControl 起了个干净的别名,方便代码里书写和阅读
  • <LinkControl settings={[]} value={props.attributes.linkObject} onChange={handleLinkChange} />settings 传空数组表示不需要额外的自定义设置项(比如「在新标签页打开」这类选项);value 回显当前已选的链接对象;onChange 在用户选中新链接时触发,传入的参数(这里命名为 newLink)就是新的链接对象
  • handleLinkChange(newLink):直接用 props.setAttributes({linkObject: newLink}) 把整个新链接对象存进属性
  • 弹窗内的「Confirm Link」按钮:点击后调用 setIsLinkPickerVisible(false) 把弹窗关掉——这里给了 style={{display: "block", width: "100%"}} 让这个按钮变成块级元素、撑满弹层宽度,纯粹是视觉调整
  • save 阶段最终只用到 linkObject.url 这一个字段href={props.attributes.linkObject.url}——虽然 linkObject 存了一整套信息(ID、标题、文章类型等),作者提到「如果想做得更严谨/抗变化,可以存 ID、用 REST API 或 PHP 在渲染时动态查询当前的最新链接(这样即使以后固定链接改了也不会失效)」,但眼下这个阶段没必要做得这么复杂,直接用当时选中的 URL 就够用
  • 改动 save 输出后,已有的旧 Block 实例会再次触发「区块已被修改」的冲突提示:跟 EP184 遇到的情况一样,需要手动点击「尝试恢复区块」、重新走一遍选择流程再保存——这也是作者提前预告「以后学会用 PHP render_callback 就能避免这个问题」的其中一个原因

Hook / Function 速查

名称类型用途
useState@wordpress/elementReact Hook(WordPress 转手提供)管理纯编辑器 UI 层面的临时状态,不需要持久化到数据库时优先用这个而不是 attributes
Popover@wordpress/componentsReact 组件提供悬浮弹层容器,position 控制相对触发元素的定位
__experimentalLinkControl(起别名为 LinkControlReact 组件(@wordpress/block-editorWordPress 内建的链接选择器,支持搜索站内内容或输入任意 URL
link@wordpress/icons图标资源官方链接图标,需要真正执行 npm install @wordpress/icons 才能使用

常见坑

  • 把「弹窗是否可见」这种纯 UI 状态也存进 Block attributes——这类数据没必要持久化,应该用 useState 管理在组件本地
  • 以为所有 @wordpress/* 包都不需要真正 npm install——@wordpress/icons 是例外,不装的话 import { link } from "@wordpress/icons" 会找不到模块
  • setState 的开关切换直接写 setIsLinkPickerVisible(true)——这样点击按钮永远只会「打开」,做不到「再点一次就关闭」的切换效果,要用 prev => !prev 这种基于前一个值取反的写法
  • save 阶段忘记同步把 href="#" 改成 props.attributes.linkObject.url——链接选择器选完之后前台按钮还是死链接
  • 修改了 save 函数的输出后,没有重新回到编辑器手动点「尝试恢复区块」并保存——数据库里存的还是旧版 HTML,前台看不到最新效果

[截图:点击按钮 Block 工具栏上的链接图标后弹出的 LinkControl 悬浮搜索框,可以搜索站内文章/页面或输入 URL]


延伸 / 后续讲座会用到

下一讲要给按钮加上「选颜色」的功能,把目前写死的 btn--blue 换成可以在编辑器里动态选择的选项。


Sources

Udemy:

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