EP205-207. “把编辑器成果导回 index.html 与主题内置默认图”
🔒 登录后可标记已读📌 说明: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(修改,slideBlock 新增第三参数注入主题图片目录路径)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(操作步骤,非纯代码):
- 在「外观 → 编辑器」打开首页模板,确认已经是最终布局(页头/Banner/事件博文/幻灯片/页脚)
- 去数据库
wp_posts表找到这条模板记录,把post_content字段的完整内容复制出来(或用编辑器右上角「⋮ → 导出」下载 zip,从里面的模板文件取) - 粘贴替换掉
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.php:slide 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