EP201-204. “三层嵌套 Block 实现幻灯片(slideshow / slide)”
🔒 登录后可标记已读📌 说明:EP202 的 transcript 在「Now we just need to adjust this output to actually look like a slide.」这句话结束,EP204 开头是同一句话的重复接续,中间夹了 EP203 一条不含实操步骤的「Quick Note」——三篇合并成一篇笔记。EP201(页头/页脚)内容相对独立,一并收在这篇笔记的前半段。
📌 并入 EP203 的提醒:slide.php 里判断有没有自定义背景图时,if (!$attributes['imgURL']) 在较新 PHP 版本下如果这个键完全不存在会报警告,要改成 if (!isset($attributes['imgURL']))——课程配套的参考代码压缩包已经统一改成这个写法。
先用 EP199-200 建立的 PlaceholderBlock 套路,把全站通用的页头(logo/导航/搜索图标)和页脚做成两个可以插入任意模板的占位 Block(header、footer),顺带讨论了一下"官方标准做法"(2022 主题用的 parts/ 文件夹机制)跟自己这套做法的区别。然后是这一章份量最重的一个练习:把首页幻灯片改造成三层嵌套的 Block 结构——最外层 slideshow(只允许嵌套 slide)→ 中间层 slide(复制 banner 的代码改一下,负责单张幻灯片的背景图 + 只允许嵌套标题/按钮)→ 最内层复用已经写好的 genericheading/genericbutton。同时把原本只认「页面上第一个」滑动组件的 JS 脚本,改造成能同时正确驱动「一个页面上多个独立幻灯片实例」。
涉及文件
wp-content/themes/fictional-university-block-theme/functions.php(修改,新增 4 个 Block 注册)wp-content/themes/fictional-university-block-theme/our-blocks/header.js/header.php(新建)wp-content/themes/fictional-university-block-theme/our-blocks/footer.js/footer.php(新建)wp-content/themes/fictional-university-block-theme/our-blocks/slideshow.js/slideshow.php(新建)wp-content/themes/fictional-university-block-theme/our-blocks/slide.js/slide.php(新建,复制自banner.js/banner.php)wp-content/themes/fictional-university-block-theme/package.json(修改,start脚本新增两个入口)wp-content/themes/fictional-university-block-theme/src/modules/HeroSlider.js(修改,支持多实例)
代码实现
functions.php:新增页头/页脚占位 Block + 幻灯片两层 Block:
new PlaceholderBlock("eventsandblogs");
new PlaceholderBlock("header");
new PlaceholderBlock("footer");
// ...
new JSXBlock('banner', true, ['fallbackimage' => get_theme_file_uri('/images/library-hero.jpg')]);
new JSXBlock('genericheading');
new JSXBlock('genericbutton');
new JSXBlock('slideshow', true);
new JSXBlock('slide', true);
our-blocks/header.js / our-blocks/footer.js:跟 eventsandblogs.js 同样的极简占位写法:
wp.blocks.registerBlockType("ourblocktheme/header", {
title: "Fictional University Header",
edit: function () {
return wp.element.createElement("div", { className: "our-placeholder-block" }, "Header Placeholder")
},
save: function () {
return null
}
})
our-blocks/header.php / our-blocks/footer.php:原样搬运传统主题 header.php/footer.php 里 <header>/<footer> 元素内部的既有代码(此处从略,跟原 fictional-university 传统主题的对应文件一致,不做任何改动)
our-blocks/slideshow.js(新建,最外层容器,只允许嵌套 slide):
import { InnerBlocks } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
registerBlockType("ourblocktheme/slideshow", {
title: "Slideshow",
supports: {
align: ["full"]
},
attributes: {
align: { type: "string", default: "full" }
},
edit: EditComponent,
save: SaveComponent
})
function SaveComponent() {
return <InnerBlocks.Content />
}
function EditComponent() {
return (
<div style={{ backgroundColor: "#333", padding: "35px" }}>
<p style={{ textAlign: "center", fontSize: "20px", color: "#FFF" }}>Slideshow</p>
<InnerBlocks allowedBlocks={["ourblocktheme/slide"]} />
</div>
)
}
our-blocks/slideshow.php(新建,搬运传统主题 hero-slider 结构,去掉写死的三张幻灯片,改用 $content 动态输出):
<div class="hero-slider">
<div data-glide-el="track" class="glide__track">
<div class="glide__slides">
<?php echo $content; ?>
</div>
<div class="slider__bullets glide__bullets" data-glide-el="controls[nav]">
</div>
</div>
</div>
our-blocks/slide.js(新建,几乎整份复制自 banner.js,只改了 Block 名字/标题和最外层 class):
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: {
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.imgID) {
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="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(新建,跟 banner.php 结构一致,只是最外层 class 换成幻灯片专属的):
<?php
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>
src/modules/HeroSlider.js:从「只处理页面第一个滑动组件」改成「遍历处理页面上所有滑动组件」:
import Glide from "@glidejs/glide"
class HeroSlider {
constructor() {
const allSlideshows = document.querySelectorAll(".hero-slider")
allSlideshows.forEach(function (currentSlideshow) {
const dotCount = currentSlideshow.querySelectorAll(".hero-slider__slide").length
let dotHTML = ""
for (let i = 0; i < dotCount; i++) {
dotHTML += `<button class="slider__bullet glide__bullet" data-glide-dir="=${i}"></button>`
}
currentSlideshow.querySelector(".glide__bullets").insertAdjacentHTML("beforeend", dotHTML)
var glide = new Glide(currentSlideshow, {
type: "carousel",
perView: 1,
autoplay: 3000
})
glide.mount()
})
}
}
export default HeroSlider
关键改动点:
- 页头/页脚沿用 EP199-200 的
PlaceholderBlock套路:新建占位 Block、复制传统主题header.php/footer.php里<header>/<footer>元素内部的代码(掐头去尾,不要<!DOCTYPE>/<head>/<body>这些),跟事件博文区域一模一样的模式,不再赘述 - 「官方标准做法」vs 这里的做法:2022 默认主题用
parts/文件夹(比如parts/header.html)存放「一组核心 Block 的固定组合」,再用<!-- wp:template-part {"slug":"header"} /-->这样的注释在各模板里引用——但这套机制骨子里还是「core Block 的排列组合」;这门课选择做一个自定义 Block(our-blocks/header.js/.php),因为要的是自己的 HTML/CSS/查询逻辑,不是核心 Block 拼出来的结果,两种思路的本质差异不是「有没有用parts/」,而是「内容到底是不是纯核心 Block 组合」 - 三层嵌套结构的设计:
slideshow(最外层,只允许嵌套slide,InnerBlocks allowedBlocks={["ourblocktheme/slide"]})→slide(中间层,几乎是banner的复制品:同样支持全宽、同样有背景图选择器,只允许嵌套genericheading/genericbutton)→ 已有的标题/按钮 Block——三层各自只关心自己那一层的职责,不需要重新发明标题/按钮的编辑逻辑 slideshow的edit只给一层视觉提示,没有任何自定义交互:灰底深色背景 + 一行「Slideshow」标签文字,纯粹是为了在编辑器里能看出「这是幻灯片容器,内部嵌套的才是每一页」,避免用户困惑于分不清哪层是哪层slide几乎是完整复制banner.js/banner.php,只改了名字和最外层 class:因为两者需求高度重合(都需要背景图选择器、都只允许嵌套标题/按钮)——作者坦言「理想情况下应该抽一个共用的基类,这门课不追求那种程度的极致整洁」,直接复制粘贴改名字更实际slideshow.php/slide.php的 HTML 结构原样借用传统主题已经调试好的 Glide.js 幻灯片标记规范(hero-slider/glide__track/glide__slides/glide__bullets这些 class 名字),因为这是第三方 JS 插件 Glide 认识的固定结构,不是 WordPress 的规则,这部分代码本身就跟 WordPress 无关,是通用前端知识slideshow.php里把原本写死的三张幻灯片全部删掉,只保留最外层结构,中间用<?php echo $content; ?>动态输出——不管用户实际插入了两张还是十张slide,都由这一行处理HeroSlider.js的多实例改造:原本用document.querySelector(".hero-slider")只抓页面上第一个实例来初始化,改成document.querySelectorAll(".hero-slider")抓全部实例,再用.forEach()遍历——原本发生在最外层的选择逻辑(数幻灯片张数、生成导航圆点 HTML、初始化Glide)整段搬进forEach回调里,所有原本document.querySelector(...)的地方都要改成currentSlideshow.querySelector(...)(限定在「当前这个幻灯片实例」内部查找,而不是整个页面)——这样页面上有几个slideshow实例,就各自独立初始化几次,互不干扰- 验证多实例效果的操作细节:在编辑器里用「列表视图」图标选中整个
slideshowBlock,用右键菜单/三个点的「Duplicate」直接复制出第二份,比手动重新插入、重新配置省事很多;改造前会看到「圆点数量算错、幻灯片互相干扰」的 bug,改造后两个独立的幻灯片各自正确显示两个圆点、各自独立轮播
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
document.querySelectorAll(selector).forEach(callback) | 浏览器原生 API | 遍历页面上所有匹配的元素,让脚本支持同一页面多个组件实例 |
元素.querySelector(selector)(限定在某元素内部查找) | 浏览器原生 API | 相比 document.querySelector,把查找范围限定在指定元素内部,避免抓到别的实例的子元素 |
常见坑
- JS 组件脚本只用
document.querySelector(单数)初始化——一个页面有多个该组件的实例时,只有第一个能正常工作,其余的完全没反应或者互相干扰 - 多实例改造时,内部查找逻辑忘记从
document.querySelector换成currentSlideshow.querySelector——依然会抓到全局第一个匹配元素,而不是当前正在处理的这个实例内部的元素,导致圆点数量、幻灯片内容全部错位 slideshow.php里依然保留写死的固定张数幻灯片,没有换成echo $content——用户想加第三张、第四张幻灯片时不会生效slide.php的判断依然用!$attributes['imgURL']而不是isset()版本——较新 PHP 环境下会触发未定义键警告(EP203 提醒)
[截图:前台首页同时出现两个独立轮播的 Slideshow(各自的圆点导航数量正确、互不干扰),验证多实例改造生效]
延伸 / 后续讲座会用到
首页模板到此基本完工,下一讲开始搭建单篇文章/页面、归档页等其余模板文件。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 28, EP201, EP202, EP203, EP204