
从 React 巨石到 Astro 孤岛:PPT 幻灯片系统的组件化重构与解耦实践
在之前的探索中,我们曾通过 @revealjs/react + echarts-for-react 快速搭建了一套基于 React 的数据幻灯片播放页。然而随着项目的深入,原有的架构逐渐暴露出痛点:整个 PPT 树被 React 全量接管、静态文本页承受额外的 JS 运行时开销、以及新建不同类型的 PPT 时需反复修改多处代码。
为了实现类似 Astro 博客(在 src/content/blog/ 下新增一个 Markdown 文件即可全自动处理路由、导航与渲染)的极简体验,我们对 PPT 模块进行了一次彻底的架构重构。
本文将复盘本次重构的设计思路、技术权衡、分层实现与关键踩坑经验。
一、 重构背景与核心痛点
1. 旧架构存在的问题
- 伪“孤岛”,真“巨石”:之前的方案在页面顶层使用了
client:only="react",将整个 Reveal.js 演示文稿(从外层容器到每一页静态封面、文本说明)都包裹在 React 树中,违背了 Astro 的**零 JS 默认静态输出(Islands Architecture)**理念。 - 第三方 React 封装层受限:
@revealjs/react是一个轻量的 React 包装层,无法直接使用原生 Reveal.js 强大的插件生态(如 Highlight、Markdown、Notes、Math 等)与完整的生命周期事件。 - 扩展成本高,违反开放封闭原则(OCP):若想新增一套不同板式或不同主题的 PPT,往往需要深入修改动态路由页面
[id].astro中的分发逻辑,导致页面逻辑越变越复杂。
2. 我们的重构目标
- 纯正的 Astro 孤岛架构:Reveal.js 的容器骨架与静态幻灯片全部由 Astro 静态渲染;仅在包含 ECharts 图表的局部区域引入 React。
- 极简的开发者体验(DX):新增一套 PPT 只需在
src/ppts/下新建一个.astro文件,路由、导航、配置均自动生效,改动处降为 1 处。 - 高内聚低耦合:将 PPT 组件拆解为乐高积木(Cover、EchartContainer、Table 等),每套 PPT 可自主控制自身的主题、尺寸与动画配置。
二、 架构演进与技术权衡
在重构过程中,我们探讨了两种实现路径:
| 方案 | 组织方式 | 优点 | 缺点 | 最终结论 |
|---|---|---|---|---|
| 路径 1:纯 JSON 数据驱动 | 在数据集合中定义 JSON,包含元数据与幻灯片数据数组 | 结构统一,只写 JSON 数据 | 表达力受限,无法写 JSX/条件渲染,无法自由混用复杂自定义排版 | ❌ 过于死板 |
路径 2:独立 .astro 组装 + 动态路由外壳 |
每套 PPT 是一个独立的 .astro 组件,[id].astro 作为纯净外壳 |
极度灵活,支持数据驱动与声明式混合;[id].astro 彻底解耦 |
需约定 meta 导出规范 |
✅ 采用(最佳实践) |
三、 分层架构与具体实现
重构后的系统结构清晰,各层职责单一明确:
src/
├── components/ # 🧱 乐高式幻灯片组件库
│ ├── Chart.jsx # [React] 专职负责 ECharts 图表渲染引擎(React 孤岛)
│ ├── Cover.astro # [Astro] 封面/文本幻灯片组件(输出 <section>,纯静态零 JS)
│ └── EchartContainer.astro # [Astro] 图表幻灯片组件(输出 <section>,内嵌 React Chart)
│
├── ppts/ # 📄 PPT 组装与声明目录(唯一操作入口)
│ └── test.astro # 独立 PPT 文件:自主定义 Reveal 配置与幻灯片排版
│
└── pages/ppt/ # ⚙️ 核心基础设施(完全自动化)
├── [id].astro # 通用动态路由与 HTML Shell
└── index.astro # PPT 目录列表页(自动提取各 PPT 的 meta)
1. 图表 React 孤岛 (src/components/Chart.jsx)
仅封装 ECharts 核心逻辑,不引入任何 Reveal.js 依赖:
import React from 'react';
import ReactEChartsRaw from 'echarts-for-react';
const ReactECharts = ReactEChartsRaw?.default || ReactEChartsRaw;
export default function Chart({ title, source, option, height = '750px' }) {
const chartOption = option || buildChartOption(title, source || []);
return (
<div style={{ width: '100%', height }}>
<ReactECharts
option={chartOption}
opts={{ renderer: 'canvas' }}
style={{ width: '100%', height: '100%' }}
notMerge={true}
lazyUpdate={true}
/>
</div>
);
}
2. 幻灯片积木组件 (Cover.astro & EchartContainer.astro)
单张幻灯片被拆分为不同类型的独立 Astro 组件,负责输出 <section> 标签:
Cover.astro:输出纯静态 HTML/CSS,零 JavaScript 注入。EchartContainer.astro:输出带有背景的<section>,并通过client:only="react"挂载<Chart />孤岛。
---
// src/components/EchartContainer.astro
import Chart from './Chart.jsx';
const { title, source, background = '#09090b', transition } = Astro.props;
---
<section data-background-color={background} data-transition={transition}>
<div class="slide-container chart-slide">
{source?.length > 0 ? (
<Chart title={title} source={source} client:only="react" />
) : (
<p>无图表数据</p>
)}
</div>
</section>
3. 单套 PPT 组装文件 (src/ppts/test.astro)
每套 PPT 导出自身的 meta,并全权掌控自己的 Reveal.js 配置与页面排版:
---
// src/ppts/test.astro
import 'reveal.js/reset.css';
import 'reveal.js/reveal.css';
import 'reveal.js/theme/black.css';
import Cover from '../components/Cover.astro';
import EchartContainer from '../components/EchartContainer.astro';
import data from '../data/ppts/test/data.json';
export const meta = {
title: 'TOP1 历史数据展示',
description: '基于 Astro、Reveal.js 与 ECharts 的组件化幻灯片演示',
note: '共收录 90 项核心历史数据,使用键盘方向键浏览',
};
---
<div class="reveal">
<div class="slides">
<!-- 封面页 -->
<Cover title={meta.title} subtitle={meta.description} note={meta.note} />
<!-- 数据驱动批量渲染 90 张图表 -->
{data.map((item) => (
<EchartContainer title={item.title} source={item.source} />
))}
</div>
</div>
<script>
import Reveal from 'reveal.js';
import RevealHighlight from 'reveal.js/plugin/highlight';
const deck = new Reveal(document.querySelector('.reveal'), {
hash: true,
slideNumber: true,
transition: 'slide',
width: 1600,
height: 900,
margin: 0.04,
plugins: [RevealHighlight],
});
deck.initialize().then(() => {
setTimeout(() => window.dispatchEvent(new Event('resize')), 100);
});
deck.on('slidechanged', () => window.dispatchEvent(new Event('resize')));
deck.on('resize', () => window.dispatchEvent(new Event('resize')));
</script>
4. 通用动态路由页 (src/pages/ppt/[id].astro)
动态路由不再做任何类型判断,只需动态导入目标 PPT 组件并渲染:
---
// src/pages/ppt/[id].astro
export function getStaticPaths() {
const pptModules = import.meta.glob('../../ppts/*.astro');
return Object.entries(pptModules).map(([path, loader]) => ({
params: { id: path.replace(/^.*\/([^/]+)\.astro$/, '$1') },
props: { loader },
}));
}
const { loader } = Astro.props;
const module = await (loader as () => Promise<any>)();
const DeckContent = module.default;
const meta = module.meta ?? {};
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{meta.title ?? 'PPT'}</title>
</head>
<body>
<DeckContent />
</body>
</html>
四、 关键技术点与踩坑经验
1. 为什么使用 client:only="react" 而不是 client:visible?
- 背景:ECharts 在初始化时需要依赖真实的浏览器 Canvas / DOM API。
- 踩坑:如果使用
client:visible或client:load,Astro 会在构建时执行服务端渲染(SSR)。在 SSR 阶段,echarts-for-react的 CommonJS 导出格式可能导致Element type is invalid: expected a string or class/function but got: object报错。 - 解决:在图表孤岛中使用
client:only="react",明确指示 Astro 该组件无需进行服务端预渲染,不仅彻底避免了 SSR 环境下的 DOM 报错,还大幅加速了多达 90 张图表的静态构建时间。
2. ECharts 在 Reveal.js 缩放与切换时的尺寸自适应
- 背景:Reveal.js 会通过 CSS
transform: scale()对幻灯片进行动态等比缩放,且在翻页前隐藏非当前页。 - 解决:在 Reveal.js 初始化后及触发
slidechanged、resize事件时,主动向窗口广播window.dispatchEvent(new Event('resize')),触发echarts-for-react内部的重绘监听,确保每一张图表在切入视野时都能精确充满容器。
3. CJS 与 ESM 的 Interop 处理
- 在 Vite / Astro 环境下导入
echarts-for-react时,部分模块打包可能存在default.default的嵌套结构。 - 通过
const ReactECharts = ReactEChartsRaw?.default || ReactEChartsRaw;做一层防御性解构,确保开发与生产打包的一致性。
五、 重构收益与总结
| 维度 | 旧架构 (React 全量接管) | 新架构 (Astro 孤岛 + 独立 Deck) |
|---|---|---|
| 新建一套 PPT | 需配置路由、注册表、在 JSX 中写 Switch 映射(改动 3~4 处) | 只需在 src/ppts/ 新建 1 个 .astro 文件 |
| 客户端 JS 体积 | 包含整套 React 运行时 + 幻灯片全量 DOM | 静态文本 0 JS,仅图表部分按需引入 React |
| Reveal.js 灵活性 | 受限于第三方 React 封装层 | 直接使用原生 Reveal.js,插件、主题随心定制 |
| 组件复用度 | 逻辑与样式混杂在单一组件中 | 乐高积木式拆分(Cover, EchartContainer 等) |
通过本次重构,我们不仅彻底解锁了 Astro 在复杂交互式演示场景下的潜力,更建立了一套优雅、可扩展且符合开发直觉的幻灯片基础设施。未来即使接入表格、图片全屏背景、视频幻灯片等新型排版,也只需新增对应的 Astro 组件即可!