WP DEVELOP

EP194-196. “MediaUpload 背景图选择器与 apiFetch 查特定尺寸 URL”

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

📌 说明:EP194 的 transcript 在「First, let's just make sure that we're holding on to this ID value in a block attribute.」这句话结束,EP196 开头是同一句话的重复接续,中间夹了 EP195 一条不含实操步骤的「Quick Note」——三篇合并成一篇笔记。

📌 并入 EP195 的提醒:这一讲涉及的 PHP 判断(if (!$attributes['imgURL']))在较新版本的 PHP 环境下,如果这个属性压根不存在会触发「访问未定义数组键」的警告,正确写法应该是 if (!isset($attributes['imgURL']))。不过要注意——对照这一讲实际的代码快照,banner.php 里这处判断当时仍然写的是 !$attributes['imgURL'],没有同步改成 isset() 版本,这是作者口头提醒但代码快照没跟上的一处遗留细节,如果在自己的环境中遇到警告,可以按提醒自行改成 isset() 版本。


给 Banner Block 加上「在右侧检查器面板选择/上传自定义背景图」的功能。用 WordPress 内建的 MediaUpload(配合 MediaUploadCheck 做权限校验)打开标准的媒体库选择窗口,选中图片后只拿到一个媒体 ID(imgID),但界面上需要的是「裁切成 Banner 专用宽高比(pageBanner 尺寸,主题章节已经注册过)的那张图的 URL」——默认返回的数据不包含自定义尺寸,所以要额外用 apiFetch 发一次请求,专门查这个媒体 ID 对应的 pageBanner 尺寸 URL,存进 imgURL 属性。PHP 端的 render_callback 直接使用已经存好的 imgURL,如果完全没设置过(imgURL 为空)就退回写死的默认背景图。


涉及文件

  • wp-content/themes/fictional-university-block-theme/our-blocks/banner.js (修改)
  • wp-content/themes/fictional-university-block-theme/our-blocks/banner.php (修改)

代码实现

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

import apiFetch from "@wordpress/api-fetch"
import { Button, PanelBody, PanelRow } from "@wordpress/components"
import { InnerBlocks, InspectorControls, MediaUpload, MediaUploadCheck } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
import { useEffect } from "@wordpress/element"

registerBlockType("ourblocktheme/banner", {
  title: "Banner",
  supports: {
    align: ["full"]
  },
  attributes: {
    align: { type: "string", default: "full" },
    imgID: { type: "number" },
    imgURL: { type: "string" }
  },
  edit: EditComponent,
  save: SaveComponent
})

function EditComponent(props) {
  useEffect(
    function () {
      async function go() {
        const response = await apiFetch({
          path: `/wp/v2/media/${props.attributes.imgID}`,
          method: "GET"
        })
        props.setAttributes({ imgURL: response.media_details.sizes.pageBanner.source_url })
      }
      go()
    },
    [props.attributes.imgID]
  )

  function onFileSelect(x) {
    props.setAttributes({ imgID: x.id })
  }

  return (
    <>
      <InspectorControls>
        <PanelBody title="Background" initialOpen={true}>
          <PanelRow>
            <MediaUploadCheck>
              <MediaUpload
                onSelect={onFileSelect}
                value={props.attributes.imgID}
                render={({ open }) => {
                  return <Button onClick={open}>Choose Image</Button>
                }}
              />
            </MediaUploadCheck>
          </PanelRow>
        </PanelBody>
      </InspectorControls>
      <div className="page-banner">
        <div className="page-banner__bg-image" style={{ backgroundImage: `url('${props.attributes.imgURL}')` }}></div>
        <div className="page-banner__content container t-center c-white">
          <InnerBlocks allowedBlocks={["ourblocktheme/genericheading", "ourblocktheme/genericbutton"]} />
        </div>
      </div>
    </>
  )
}

function SaveComponent() {
  return <InnerBlocks.Content />
}

our-blocks/banner.php:没有自定义背景图时退回默认图

<?php

if (!$attributes['imgURL']) {
  $attributes['imgURL'] = get_theme_file_uri('/images/library-hero.jpg');
}

?>

<div class="page-banner">
      <div class="page-banner__bg-image" style="background-image: url('<?php echo $attributes['imgURL'] ?>')"></div>
      <div class="page-banner__content container t-center c-white">
        <?php echo $content; ?>
      </div>
    </div>

关键改动点:

  • 新增两个属性imgID(number,选中的媒体附件 ID)、imgURL(string,实际要用的图片地址)——两个都存,而不是只存 ID 现查,是为了让 PHP 端不用重新执行一次查询,直接读 imgURL 就够用
  • MediaUploadCheck + MediaUpload——标准的媒体选择器套路MediaUploadCheck 包在最外层,负责检查当前登录用户是否有权限上传媒体(没权限的话内部内容不会显示);MediaUpload 是真正的选择器组件,用三个 prop 驱动:
    • value:当前已选中的媒体 ID,用来在重新打开选择窗口时正确高亮显示「这张已经是你选过的」
    • onSelect:用户选好图片、点击选择器窗口里的「Select」按钮后触发,拿到的参数是一个描述这个媒体的对象(这一讲用打印到控制台的方式实地查看过它的结构,最终只用到里面的 .id 字段)
    • render:这是一个「返回 JSX 决定触发按钮长什么样」的函数,拿到一个解构出来的 open 方法——把这个 open 绑定到自己写的按钮的 onClick 上,点击就会弹出标准的 WordPress 媒体库窗口
  • onFileSelect(x) { props.setAttributes({imgID: x.id}) }:只从选中结果里取 .id,存进 imgID
  • 默认返回的媒体数据不包含自定义图片尺寸:WordPress 原生返回的媒体对象只带 full/large/medium/thumbnail 这几个内建尺寸的 URL,主题章节自己注册的 pageBanner 自定义宽高比不在这份默认数据里——所以需要额外单独查一次
  • apiFetch 按媒体 ID 单独查一次完整数据:请求路径是 /wp/v2/media/{ID}(WordPress 内建的媒体 REST 端点),这次返回的完整 JSON 里能在 media_details.sizes.pageBanner.source_url 这条路径下找到需要的 URL
  • useEffect 监听 imgID 变化去发这次请求,而不是直接放在 onFileSelect:作者解释的理由——如果只在「刚选完图片」那一刻查一次,那么「页面刷新后重新加载已经存过的 imgID,但还没有对应 imgURL」这种情况就没法覆盖到;用 useEffect 监听 imgID 本身,不管这个值是「用户刚选的」还是「刷新页面后从数据库读回来的」,只要它存在/变化,都会触发一次查询,更可靠
  • useEffect 内部同样要包一层 async function go() {...} 再立即调用:这门课反复出现的固定写法,因为 useEffect 本身不能直接接收一个 async 函数
  • 前端展示 imgURL 用模板字符串拼接:` url('${props.attributes.imgURL}') `
  • PHP 端的默认图回退逻辑if (!$attributes['imgURL']) { $attributes['imgURL'] = get_theme_file_uri(...) }——如果这个属性是空字符串/不存在(用户从来没选过自定义背景图),就临时把它改写成默认的 library-hero.jpg 路径;后面统一 echo $attributes['imgURL'] 输出,不用写两套不同的输出逻辑
  • 为什么要把最终选定的 URL(而不是只有 ID)存进 attribute、PHP 端直接读:跟 EP193 一致的设计思路——避免在 PHP 里重新写一遍「根据 ID 查特定尺寸 URL」的查询逻辑,JS 端已经查好存下来的值可以直接复用

Hook / Function 速查

名称类型用途
MediaUploadCheckReact 组件(@wordpress/block-editor校验当前用户是否有权限上传媒体,包裹住实际的选择器
MediaUploadReact 组件(@wordpress/block-editor标准媒体选择器,value/onSelect/render 三个 prop 驱动交互
apiFetch({path, method})@wordpress/api-fetchWordPress 官方 JS 工具发送带认证的 REST API 请求,这里用来查询 /wp/v2/media/{ID}
useEffect(callback, [依赖])@wordpress/elementReact Hook监听某个属性变化(这里是 imgID)时重新查询对应的图片 URL

常见坑

  • 只在 onSelect 触发的那一刻查一次图片 URL——刷新页面后重新加载已存的 imgID 时不会重新查询,容易导致 imgURL 缺失或过期,更稳妥的做法是用 useEffect 监听 imgID 本身
  • 以为选中媒体后返回的数据自带所有自定义图片尺寸——默认只包含内建的 full/large/medium/thumbnail自定义尺寸需要额外单独请求 /wp/v2/media/{ID} 才能拿到
  • PHP 端判断「是否有自定义背景图」时直接用 !$attributes['imgURL'] 而不加 isset()——较新 PHP 环境下如果这个键完全不存在会触发警告(EP195 提醒,注意这一讲的代码快照本身还没有修正这一点)
  • useEffect 内部直接把回调函数标记成 async——不被支持,需要在内部另外定义并立即调用一个 async function

[截图:Banner Block 右侧检查器面板点击 Choose Image 按钮后弹出的 WordPress 标准媒体库选择窗口]


延伸 / 后续讲座会用到

下一讲要处理一个小细节:让默认背景图的路径更「健壮」,不要写死假设 WordPress 安装在根目录。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 28, EP194, EP195, EP196