WP DEVELOP

EP205-207. “把编辑器成果导回 index.html 与主题内置默认图”

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

📌 说明:EP205 的 transcript 在「But let me show you what I would do.」这句话结束,EP207 开头是同一句话的重复接续,中间夹了 EP206 一条不含实操步骤的「Quick Note」——三篇合并成一篇笔记。

📌 并入 EP206 的提醒:这一讲 slide.php 一开始会写成 if ($attributes['themeimage']) / if (!$attributes['imgURL']),但更严谨的写法要考虑两种不同的「空」情况:isset() 只检查「键存在且不是 null」,如果还要确认「即使存在,值是不是空字符串」,应该用 empty()——!empty($attributes['themeimage']) 同时涵盖「键不存在」和「键存在但值为空字符串」两种情况,比单纯 isset() 更严谨。本讲后续代码块已按这个更严谨的版本书写。


把编辑器里手工搭好的首页布局(页头 + Banner + 事件博文 + 幻灯片 + 页脚),从数据库导出、正式写回 templates/index.html 源文件——这样新装这套主题的人打开首页编辑器时,默认就是这个成品布局,也是以后可以随时「清除自定义项」恢复回去的安全基线。写回过程中发现一个必须解决的问题:默认幻灯片里的三张示意图(bus/apples/bread)不能像 Banner 那样只提供单一兜底图——因为不同用户装这套主题后,媒体库里不会有跟开发者一样的这几张图;解法是不在模板文件里存写死的完整图片 URL,而是存一个相对文件名(比如 "bus.jpg"),再让 JS/PHP 分别拼上「主题当前安装目录」这个只有服务器才知道的动态前缀。


涉及文件

  • wp-content/themes/fictional-university-block-theme/templates/index.html (修改,写入真实布局)
  • wp-content/themes/fictional-university-block-theme/functions.php (修改,slide Block 新增第三参数注入主题图片目录路径)
  • wp-content/themes/fictional-university-block-theme/our-blocks/slide.js (修改,新增 themeimage 属性与对应处理逻辑)
  • wp-content/themes/fictional-university-block-theme/our-blocks/slide.php (修改,同步处理 themeimage

代码实现

把编辑器已经排好的实际布局导出、写回 templates/index.html(操作步骤,非纯代码):

  1. 在「外观 → 编辑器」打开首页模板,确认已经是最终布局(页头/Banner/事件博文/幻灯片/页脚)
  2. 去数据库 wp_posts 表找到这条模板记录,把 post_content 字段的完整内容复制出来(或用编辑器右上角「⋮ → 导出」下载 zip,从里面的模板文件取)
  3. 粘贴替换掉 templates/index.html 里原本的占位段落/标题

Banner 部分不需要特殊处理——之前已经设计好「没有自定义 imgURL 就用主题内置默认图」的兜底逻辑,模板里干脆直接删掉 imgID/imgURL 这两个属性即可:

<!-- wp:ourblocktheme/banner /-->

幻灯片部分:模板里用相对文件名 themeimage,不用完整 URL

<!-- wp:ourblocktheme/slide {"themeimage":"bus.jpg"} -->
...
<!-- wp:ourblocktheme/slide {"themeimage":"apples.jpg"} -->
...
<!-- wp:ourblocktheme/slide {"themeimage":"bread.jpg"} -->

functions.phpslide Block 新增第三参数,注入主题图片目录的完整 URL 前缀

new JSXBlock('slide', true, ['themeimagepath' => get_theme_file_uri('/images/')]);

our-blocks/slide.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/slide", {
  title: "Slide",
  supports: {
    align: ["full"]
  },
  attributes: {
    themeimage: { type: "string" },
    align: { type: "string", default: "full" },
    imgID: { type: "number" },
    imgURL: { type: "string", default: banner.fallbackimage }
  },
  edit: EditComponent,
  save: SaveComponent
})

function EditComponent(props) {
  useEffect(function () {
    if (props.attributes.themeimage) {
      props.setAttributes({ imgURL: `${slide.themeimagepath}${props.attributes.themeimage}` })
    }
  }, [])

  useEffect(
    function () {
      if (props.attributes.imgID) {
        async function go() {
          const response = await apiFetch({
            path: `/wp/v2/media/${props.attributes.imgID}`,
            method: "GET"
          })
          props.setAttributes({ themeimage: "", 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="hero-slider__slide" style={{ backgroundImage: `url('${props.attributes.imgURL}')` }}>
        <div className="hero-slider__interior container">
          <div className="hero-slider__overlay t-center">
            <InnerBlocks allowedBlocks={["ourblocktheme/genericheading", "ourblocktheme/genericbutton"]} />
          </div>
        </div>
      </div>
    </>
  )
}

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

our-blocks/slide.php:PHP 端同步处理 themeimage

<?php

if (!empty($attributes['themeimage'])) {
  $attributes['imgURL'] = get_theme_file_uri('/images/' . $attributes['themeimage']);
}

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

?>

      <div class="hero-slider__slide" style="background-image: url('<?php echo $attributes['imgURL'] ?>')">
        <div class="hero-slider__interior container">
          <div class="hero-slider__overlay t-center">
            <?php echo $content; ?>
          </div>
        </div>
      </div>

关键改动点:

  • 为什么 Banner 不需要类似 themeimage 的机制,Slide 却需要:Banner 只需要「一张兜底图」(library-hero.jpg),任何装了这套主题的人都会带着这张图;但幻灯片默认要展示三张各自不同的示意图(bus/apples/bread),没法用「只有一个兜底值」的机制去区分具体哪张幻灯片该用哪张图
  • 不把完整 URL 写进模板文件,而是存一个相对文件名:模板文件(templates/index.html)会随着整个主题打包分发给任何人使用,如果里面写死了开发者自己电脑上传的完整媒体库 URL(比如 .../wp-content/uploads/2022/03/bus-scaled.jpg),换一台服务器、换一个 WordPress 安装,这个 URL 根本不存在——存 "bus.jpg" 这样一个相对文件名,配合「主题目录 + /images/」这个能在任何环境下动态算出来的路径前缀,才能保证放到任何人的服务器上都正确显示
  • JS 端没有直接访问「当前主题目录」的简单方法:作者提到翻遍文档也没找到从纯客户端 JS 里拿到这个值的直接办法,只能借助已有的 JSXBlock 类第三参数(wp_localize_script() 机制,EP198 学过)把 PHP 算好的路径注入成全局变量——new JSXBlock('slide', true, ['themeimagepath' => get_theme_file_uri('/images/')]),这样浏览器端就能通过全局变量 slide.themeimagepath 读到「主题目录/images/」这段完整前缀
  • slide.js 新增 themeimage 属性(字符串,无默认值):模板文件里通过 Block 的 JSON 参数写入(比如 {"themeimage": "bus.jpg"}),这个值只是个文件名片段,需要跟 slide.themeimagepath 拼接才是完整 URL
  • 新增一个只在「首次加载」时运行一次的 useEffect:依赖数组给空数组 [](只在组件挂载时跑一次,不是每次属性变化都跑)——如果 themeimage 有值(说明这是模板文件事先指定的默认图),就拼出完整 URL 存进 imgURL:` ${slide.themeimagepath}${props.attributes.themeimage} `
  • 原本监听 imgID 变化的那个 useEffect 需要同步清空 themeimage:一旦用户真的上传/选择了自己的自定义图片(imgID 变化),说明不再需要理会模板里写死的默认文件名了,props.setAttributes({themeimage: "", imgURL: ...}) 顺手把 themeimage 清空,避免「用户明明换了图,themeimage 却还残留着旧值」这种数据不一致的情况
  • PHP 端 slide.php 补上对应的处理逻辑,且顺序很关键:先检查 !empty($attributes['themeimage'])——如果模板指定了默认文件名,直接拼出 get_theme_file_uri('/images/' . $attributes['themeimage']) 覆盖 imgURL;再检查 !isset($attributes['imgURL'])——如果连 imgURL 都完全没有(既没有 themeimage,用户也没上传过自定义图),才退回最终的通用兜底图 library-hero.jpg——两层判断依次兜底,保证任何情况下都有值可用
  • 验证「用户自定义会覆盖默认图」的效果:在编辑器里选一张自己的图替换掉某张幻灯片的默认背景,保存后前台正确显示新图;反过来「清除自定义项」恢复模板默认值后,又会重新显示 bus.jpg 这类主题自带的示意图——两个方向都要测试,确保兜底逻辑和用户自定义逻辑不会互相打架
  • 首页模板到此彻底完工:作者强调这一讲踩的坑(分发主题时要考虑「别人没有你的媒体库文件」)只对「默认就要展示具体图片内容」的场景(比如这里的示意性幻灯片)才需要这么麻烦,其余大多数模板文件(单篇文章、页面、归档页等)不会有这类特殊情况,可以很快搭完

[截图:全新安装这套主题后首页幻灯片自动显示 bus/apples/bread 三张主题内置示意图的效果]


Hook / Function 速查

名称类型用途
empty($变量)PHP 内建 function同时判断变量「不存在」或「存在但值为空」两种情况,比 isset() 多一层严谨性
useEffect(callback, [])(空依赖数组)React Hook 用法只在组件首次挂载时执行一次,不随后续任何属性变化重新触发

常见坑

  • 把编辑器里排版好的最终布局直接留在数据库里,不写回 templates/index.html 源文件——一旦有人误操作把模板改乱又想恢复,没有安全的「地面真相」版本可以回退
  • 主题分发用的默认图,在模板文件里写死开发者自己电脑上的完整媒体库 URL——换一台服务器/换一个全新的 WordPress 安装后,这个 URL 根本不存在,默认展示会直接失效
  • 忘记在「用户上传自定义图后」同步清空 themeimage 属性——旧的默认文件名残留在数据里,可能在某些判断顺序下造成图片显示逻辑混乱
  • slide.php 里两层兜底判断的顺序颠倒(先判断 imgURL 再判断 themeimage)——会导致模板指定的默认图片文件名完全不起作用,永远只会用最终的通用兜底图

延伸 / 后续讲座会用到

首页模板正式完工,下一讲开始搭建单篇文章、页面、归档页等其余模板,作者预告这些模板不需要处理类似的特殊图片兜底逻辑,会进展得快很多。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 28, EP205, EP206, EP207