2024/01/07
使用 React Flow 创建幻灯片演示文稿

我们最近发布了 React Flow 2023 年年终调查的结果,并提供了一个使用 React Flow 本身制作的互动演示来展示关键发现。这个幻灯片应用中内置了许多有用的功能,因此我们想分享一下我们是如何构建它的!
🌐 We recently published the findings from our React Flow 2023 end-of-year survey with an interactive presentation of the key findings, using React Flow itself. There were lots of useful bits built into this slideshow app, so we wanted to share how we built it!

在本教程结束时,你将构建一个演示应用
🌐 By the end of this tutorial, you will have built a presentation app with
- 支持 markdown 幻灯片
- 视口周围的键盘导航
- 自动布局
- 单击拖动平移导航(Prezi 风格)
在此过程中,你将学习一些关于布局算法的基础知识、创建静态流程以及自定义节点的内容。
🌐 Along the way, you’ll learn a bit about the basics of layouting algorithms, creating static flows, and custom nodes.
完成后,应用将如下所示!
🌐 Once you’re done, the app will look like this!
为了跟随本教程,我们假设你对 React 和 React Flow 有基本的了解,但如果在学习过程中遇到困难,随时可以通过 Discord 联系我们!
🌐 To follow along with this tutorial we’ll assume you have a basic understanding of React and React Flow, but if you get stuck on the way feel free to reach out to us on Discord !
如果你想直接跳到最后或在我们进行时参考它,这里是包含最终代码的仓库 。
🌐 Here’s the repo with the final code if you’d like to skip ahead or refer to it as we go.
让我们开始吧!
🌐 Let’s get started!
设置项目
🌐 Setting up the project
我们建议在开始新的 React Flow 项目时使用 Vite ,这次我们也将使用 TypeScript。你可以使用以下命令创建一个新项目:
🌐 We like to recommend using Vite when starting new React Flow projects, and this time we’ll use TypeScript too. You can scaffold a new project with the following command:
npm create vite@latest -- --template react-ts如果你更愿意跟随 JavaScript,可以随意使用 react 模板。你也可以通过使用我们的 Codesandbox 模板在浏览器中跟随学习:
🌐 If you’d prefer to follow along with JavaScript feel free to use the react template
instead. You can also follow along in your browser by using our Codesandbox templates:
除了 React Flow,我们只需要引入一个依赖,react-remark,来帮助我们在幻灯片中渲染 Markdown。
🌐 Besides React Flow we only need to pull in one dependency,
react-remark, to help us render markdown
in our slides.
npm install @xyflow/react react-remark我们将修改生成的 main.tsx 以包含 React Flow 的样式,同时将应用封装在 <ReactFlowProvider /> 中,以确保我们能够在组件内部访问 React Flow 实例;
🌐 We’ll modify the generated main.tsx to include React Flow’s styles, as well as wrap the
app in a <ReactFlowProvider /> to make sure we can access the React Flow instance inside
our components;
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from '@xyflow/react';
import App from './App';
import '@xyflow/react/dist/style.css';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ReactFlowProvider>
{/* The parent element of the React Flow component needs a width and a height
to work properly. If you're styling your app as you follow along, you
can remove this div and apply styles to the #root element in your CSS.
*/}
<div style={{ width: '100vw', height: '100vh' }}>
<App />
</div>
</ReactFlowProvider>
</React.StrictMode>,
);本教程将略过应用的样式部分,因此可以随意使用你熟悉的任何 CSS 框架或样式解决方案。如果你打算以不同于直接编写 CSS 的方式来为应用设计样式,Tailwind CSS,你可以跳过对 index.css 的导入。
🌐 This tutorial is going to gloss over the styling of the app, so feel free to use any CSS
framework or styling solution you’re familiar with. If you’re going to style your app
differently from just writing CSS, Tailwind CSS, you can
skip the import to index.css.
你如何为你的应用进行样式设计取决于你自己,但你必须始终包含 React Flow 的样式!如果你不需要默认样式,至少你应该包含来自 @xyflow/react/dist/base.css 的基础样式。
我们演示文稿的每一张幻灯片都将是画布上的一个节点,所以让我们创建一个新文件 Slide.tsx,它将是我们用于渲染每张幻灯片的自定义节点。
🌐 Each slide of our presentation will be a node on the canvas, so let’s create a new file
Slide.tsx that will be our custom node used to render each slide.
import { type Node, type NodeProps } from '@xyflow/react';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node<SlideData, 'slide'>;
export type SlideData = {};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps<SlideNode>) {
return (
<article className="slide nodrag" style={style}>
<div>Hello, React Flow!</div>
</article>
);
}我们在这里将幻灯片的宽度和高度设置为常量(而不是在 CSS 中为节点设置样式),因为我们稍后会需要访问这些尺寸。我们还暂时定义了 SlideData 类型,以便能够正确地为组件的 props 提供类型。
🌐 We’re setting the slide width and height as constants here (rather than styling the node
in CSS) because we’ll want access to those dimensions later on. We’ve also stubbed out the
SlideData type so we can properly type the component’s props.
最后要做的是注册我们的新自定义节点并在屏幕上显示一些内容。
🌐 The last thing to do is to register our new custom node and show something on the screen.
import { ReactFlow } from '@xyflow/react';
import { Slide } from './Slide.tsx';
const nodeTypes = {
slide: Slide,
};
export default function App() {
const nodes = [{ id: '0', type: 'slide', position: { x: 0, y: 0 }, data: {} }];
return <ReactFlow nodes={nodes} nodeTypes={nodeTypes} fitView />;
}重要的是要记住在组件之外定义你的 nodeTypes 对象(或者使用 React 的 useMemo 钩子)!当 nodeTypes 对象发生变化时,整个流程都会重新渲染。
在基础内容整理好之后,你可以通过运行 npm run dev 启动开发服务器,并看到如下内容:
🌐 With the basics put together, you can start the development server by running
npm run dev and see the following:
还不是特别令人兴奋,但让我们添加 Markdown 渲染并创建几个并排的幻灯片吧!
🌐 Not super exciting yet, but let’s add markdown rendering and create a few slides side by side!
渲染 markdown
🌐 Rendering markdown
我们希望让在幻灯片中添加内容变得简单,所以我们希望能够在幻灯片中编写Markdown 。如果你不熟悉,Markdown 是一种用于创建格式化文本文档的简单标记语言。如果你曾经在 GitHub 上写过 README,那么你就使用过 Markdown!
🌐 We want to make it easy to add content to our slides, so we’d like the ability to write Markdown in our slides. If you’re not familiar, Markdown is a simple markup language for creating formatted text documents. If you’ve ever written a README on GitHub, you’ve used Markdown!
多亏了我们之前安装的 react-remark 包,这一步很简单。我们可以使用 <Remark /> 组件将一段 Markdown 内容渲染到我们的幻灯片中。
🌐 Thanks to the react-remark package we installed earlier, this step is a simple one. We
can use the <Remark /> component to render a string of markdown content into our slides.
import { type Node, type NodeProps } from '@xyflow/react';
import { Remark } from 'react-remark';
export const SLIDE_WIDTH = 1920;
export const SLIDE_HEIGHT = 1080;
export type SlideNode = Node<SlideData, 'slide'>;
export type SlideData = {
source: string;
};
const style = {
width: `${SLIDE_WIDTH}px`,
height: `${SLIDE_HEIGHT}px`,
} satisfies React.CSSProperties;
export function Slide({ data }: NodeProps<SlideNode>) {
return (
<article className="slide nodrag" style={style}>
<Remark>{data.source}</Remark>
</article>
);
}在 React Flow 中,节点可以存储可以在渲染过程中使用的数据。在这种情况下,我们通过向 SlideData 类型添加 source 属性并将其传递给 <Remark /> 组件来存储要显示的 Markdown 内容。我们可以使用一些 Markdown 内容更新我们的硬编码节点,以查看其效果:
🌐 In React Flow, nodes can have data stored on them that can be used during rendering. In
this case we’re storing the markdown content to display by adding a source property to
the SlideData type and passing that to the <Remark /> component. We can update our
hardcoded nodes with some markdown content to see it in action:
import { ReactFlow } from '@xyflow/react';
import { Slide, SLIDE_WIDTH } from './Slide';
const nodeTypes = {
slide: Slide,
};
export default function App() {
const nodes = [
{
id: '0',
type: 'slide',
position: { x: 0, y: 0 },
data: { source: '# Hello, React Flow!' },
},
{
id: '1',
type: 'slide',
position: { x: SLIDE_WIDTH, y: 0 },
data: { source: '...' },
},
{
id: '2',
type: 'slide',
position: { x: SLIDE_WIDTH * 2, y: 0 },
data: { source: '...' },
},
];
return <ReactFlow nodes={nodes} nodeTypes={nodeTypes} fitView minZoom={0.1} />;
}请注意,我们已经在 <ReactFlow /> 组件中添加了 minZoom 属性。我们的幻灯片相当大,默认的最小缩放级别不足以缩小查看多个幻灯片。
🌐 Note that we’ve added the minZoom prop to the <ReactFlow /> component. Our slides are
quite large, and the default minimum zoom level is not enough to zoom out and see multiple
slides at once.
在上面的节点数组中,我们通过对 SLIDE_WIDTH 常量进行一些手动计算,确保幻灯片之间有间距。在下一部分中,我们将提出一个算法,自动将幻灯片排列在网格中。
🌐 In the nodes array above, we’ve made sure to space the slides out by doing some manual
math with the SLIDE_WIDTH constant. In the next section we’ll come up with an algorithm
to automatically lay out the slides in a grid.
布置节点
🌐 Laying out the nodes
我们经常被问到如何自动布局流程中的节点,我们在布局指南中有一些关于如何使用常用布局库如 dagre 和 d3-hierarchy 的文档。在这里,你将编写你自己的超简单布局算法,这有点技术性,但请跟我们一起坚持下去!
🌐 We often get asked how to automatically lay out nodes in a flow, and we have some documentation on how to use common layouting libraries like dagre and d3-hierarchy in our layouting guide. Here you’ll be writing your own super-simple layouting algorithm, which gets a bit nerdy, but stick with us!
对于我们的演示应用,我们将通过从0,0开始构建一个简单的网格布局,并在每次有新幻灯片在左边、右边、上方或下方时更新x或y坐标。
🌐 For our presentation app we’ll construct a simple grid layout by starting from 0,0 and updating the x or y coordinates any time we have a new slide to the left, right, up, or down.
首先,我们需要更新我们的 SlideData 类型,以包含当前幻灯片左、右、上、下的幻灯片的可选 ID。
🌐 First, we need to update our SlideData type to include optional ids for the slides to
the left, right, up, and down of the current slide.
export type SlideData = {
source: string;
left?: string;
up?: string;
down?: string;
right?: string;
};将此信息直接存储在节点数据上为我们带来了一些有用的好处:
🌐 Storing this information on the node data directly gives us some useful benefits:
- 我们可以编写完全声明式的幻灯片,而无需担心节点和边的概念
- 我们可以通过访问连接幻灯片来计算演示文稿的布局
- 我们可以在每张幻灯片上添加导航按钮,以自动在它们之间导航。我们将在后面的步骤中处理这个问题。
魔法发生在我们将要定义的一个名为 slidesToElements 的函数中。该函数将接受一个包含所有幻灯片的对象,这些幻灯片通过它们的 id 来标识,以及一个用于指定起始幻灯片的 id。然后,它将处理每个相连的幻灯片,以构建一个节点和边的数组,我们可以将这个数组传递给 <ReactFlow /> 组件。
🌐 The magic happens in a function we’re going to define called slidesToElements. This
function will take an object containing all our slides addressed by their id, and an id
for the slide to start at. Then it will work through each connecting slide to build an
array of nodes and edges that we can pass to the <ReactFlow /> component.
算法将如下所示:
🌐 The algorithm will go something like this:
- 将初始幻灯片的 ID 和位置
{ x: 0, y: 0 }推入堆栈。 - 虽然该堆栈不为空……
- 从堆栈中弹出当前位置和幻灯片 ID。
- 通过 id 查找幻灯片数据。
- 将新节点推送到具有当前 ID、位置和幻灯片数据的节点数组上。
- 将幻灯片的 ID 添加到一组访问过的幻灯片中。
- 对于每个方向(左、右、上、下)…
- 确保幻灯片尚未被访问过。
- 获取当前位置,并根据方向通过加上或减去
SLIDE_WIDTH或SLIDE_HEIGHT来更新 x 或 y 坐标。 - 将新位置和新幻灯片的 ID 推送到堆栈上。
- 将新边推送到将当前幻灯片连接到新幻灯片的边数组上。
- 重复其余方向…
如果一切按计划进行,我们应该能够将下面显示的一堆幻灯片整理成一个整齐排列的网格!
🌐 If all goes to plan, we should be able to take a stack of slides shown below and turn them into a neatly laid out grid!

让我们看看代码。在一个名为 slides.ts 的文件中添加以下内容:
🌐 Let’s see the code. In a file called slides.ts add the following:
import { SlideData, SLIDE_WIDTH, SLIDE_HEIGHT } from './Slide';
export const slidesToElements = (initial: string, slides: Record<string, SlideData>) => {
// Push the initial slide's id and the position `{ x: 0, y: 0 }` onto a stack.
const stack = [{ id: initial, position: { x: 0, y: 0 } }];
const visited = new Set();
const nodes = [];
const edges = [];
// While that stack is not empty...
while (stack.length) {
// Pop the current position and slide id off the stack.
const { id, position } = stack.pop();
// Look up the slide data by id.
const data = slides[id];
const node = { id, type: 'slide', position, data };
// Push a new node onto the nodes array with the current id, position, and slide
// data.
nodes.push(node);
// add the slide's id to a set of visited slides.
visited.add(id);
// For every direction (left, right, up, down)...
// Make sure the slide has not already been visited.
if (data.left && !visited.has(data.left)) {
// Take the current position and update the x or y coordinate by adding or
// subtracting `SLIDE_WIDTH` or `SLIDE_HEIGHT` depending on the direction.
const nextPosition = {
x: position.x - SLIDE_WIDTH,
y: position.y,
};
// Push the new position and the new slide's id onto a stack.
stack.push({ id: data.left, position: nextPosition });
// Push a new edge onto the edges array connecting the current slide to the
// new slide.
edges.push({ id: `${id}->${data.left}`, source: id, target: data.left });
}
// Repeat for the remaining directions...
}
return { nodes, edges };
};为了简洁起见,我们省略了右、上和下方向的代码,但每个方向的逻辑都是相同的。我们还在注释中包含了相同的算法分解,以帮助你理解代码。
🌐 We’ve left out the code for the right, up, and down directions for brevity, but the logic is the same for each direction. We’ve also included the same breakdown of the algorithm as comments, to help you navigate the code.
下面是布局算法的演示应用,你可以编辑 slides 对象来看向不同方向添加幻灯片如何影响布局。例如,尝试将 4 的数据扩展以包含 down: '5',看看布局如何更新。
🌐 Below is a demo app of the layouting algorithm, you can edit the slides object to see
how adding slides to different directions affects the layout. For example, try extending
4’s data to include down: '5' and see how the layout updates.
如果你花一点时间玩这个演示,你很可能会遇到这个算法的两个限制:
🌐 If you spend a little time playing with this demo, you’ll likely run across two limitations of this algorithm:
- 可以构建一个在同一位置重叠两张幻灯片的布局。
- 算法将忽略从初始幻灯片无法到达的节点。
解决这些不足完全是可能的,但有点超出本教程的范围。如果你尝试一下,务必在我们的discord服务器 上分享你的解决方案!
🌐 Addressing these shortcomings is totally possible, but a bit beyond the scope of this tutorial. If you give a shot, be sure to share your solution with us on the discord server !
在我们的布局算法编写完成后,我们可以回到 App.tsx 并移除硬编码的节点数组,改用新的 slidesToElements 函数。
🌐 With our layouting algorithm written, we can hop back to App.tsx and remove the
hardcoded nodes array in favor of the new slidesToElements function.
import { ReactFlow } from '@xyflow/react';
import { slidesToElements } from './slides';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record<string, SlideData> = {
'0': { source: '# Hello, React Flow!', right: '1' },
'1': { source: '...', left: '0', right: '2' },
'2': { source: '...', left: '1' },
};
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides);
export default function App() {
return (
<ReactFlow
nodes={nodes}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ nodes: [{ id: initialSlide }] }}
minZoom={0.1}
/>
);
}我们流程中的幻灯片是静态的,所以我们可以将 slidesToElements 调用移到组件的 外部,以确保如果组件重新渲染,我们不会重新计算布局。或者,你也可以使用 React 的 useMemo 钩子在组件内部定义内容,但只计算一次。
🌐 The slides in our flow are static, so we can move the slidesToElements call outside
the component to make sure we’re not recalculating the layout if the component re-renders.
Alternatively, you could use React’s useMemo hook to define things inside the component
but only calculate them once.
因为我们现在有了“初始”幻灯片的概念,我们还使用 fitViewOptions 来确保在画布首次加载时,初始幻灯片是被聚焦的那个。
🌐 Because we have the idea of an “initial” slide now, we’re also using the fitViewOptions
to ensure the initial slide is the one that is focused when the canvas is first loaded.
在幻灯片之间导航
🌐 Navigating between slides
到目前为止,我们已经将演示文稿布局在网格中,但我们必须手动平移画布才能看到每一张幻灯片,这对于演示来说并不太实用!我们将添加三种不同的方式来在幻灯片之间导航:
🌐 So far we have our presentation laid out in a grid but we have to manually pan the canvas to see each slide, which isn’t very practical for a presentation! We’re going to add three different ways to navigate between slides:
- 单击以聚焦节点,通过单击它们跳转到不同的幻灯片。
- 每张幻灯片上的导航按钮,用于按任何有效方向顺序移动幻灯片。
- 使用箭头键进行键盘导航,以在不使用鼠标或直接与幻灯片交互的情况下浏览演示文稿。
点击时聚焦
🌐 Focus on click
<ReactFlow /> 元素可以接收一个 onNodeClick 回调,当 任何 节点被点击时触发。除了鼠标事件本身,我们还会收到被点击节点的引用,并且我们可以利用 fitView 方法据此平移画布。
🌐 The <ReactFlow /> element can receive an
onNodeClick callback that fires when any
node is clicked. Along with the mouse event itself, we also receive a reference to the
node that was clicked on, and we can use that to pan the canvas thanks to the fitView
method.
fitView 是 React Flow 实例上的一个方法,我们可以通过使用 useReactFlow 钩子来访问它。
import { useCallback } from 'react';
import { ReactFlow, useReactFlow, type NodeMouseHandler } from '@xyflow/react';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record<string, SlideData> = {
...
}
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides);
export default function App() {
const { fitView } = useReactFlow();
const handleNodeClick = useCallback<NodeMouseHandler>(
(_, node) => {
fitView({ nodes: [node], duration: 150 });
},
[fitView],
);
return (
<ReactFlow
...
fitViewOptions={{ nodes: [{ id: initialSlide }] }}
onNodeClick={handleNodeClick}
/>
);
}重要的是要记得在我们的 handleNodeClick 回调的依赖数组中包含 fitView。这是因为一旦 React Flow 初始化了视口,fitView 函数就会被替换。如果你忘记了这一步,你很可能会发现 handleNodeClick 根本没有任何作用(是的,我们自己有时也会忘记这一步
)。
调用 fitView 而不带任何参数会尝试将图中的每个节点都适配到视图中,但我们只想关注被点击的节点!FitViewOptions 对象允许我们提供一个我们想关注的节点数组:在本例中,这只是被点击的节点。
🌐 Calling fitView with no arguments would attempt to fit every node in the graph into
view, but we only want to focus on the node that was clicked! The
FitViewOptions object lets us provide an array
of just the nodes we want to focus on: in this case, that’s just the node that was
clicked.
滑动控件
🌐 Slide controls
点击以聚焦一个节点对于在聚焦特定幻灯片之前缩小以看到整体情况非常方便,但这并不是在演示文稿中导航的非常实用的方法。在这一步中,我们将为每张幻灯片添加一些控件,这些控件允许我们向任意方向移到相连的幻灯片。
🌐 Clicking to focus a node is handy for zooming out to see the big picture before focusing back in on a specific slide, but it’s not a very practical way for navigating around a presentation. In this step we’ll add some controls to each slide that allow us to move to a connected slide in any direction.
让我们在每张幻灯片中添加一个 <footer>,它可以有条件地在与之相连的幻灯片的任意方向渲染一个按钮。我们还将预先创建一个 moveToNextSlide 回调,稍后会使用它。
🌐 Let’s add a <footer> to each slide that conditionally renders a button in any direction
with a connected slide. We’ll also preemptively create a moveToNextSlide callback that
we’ll use in a moment.
import { type NodeProps, fitView } from '@xyflow/react';
import { Remark } from 'react-remark';
import { useCallback } from 'react';
...
export function Slide({ data }: NodeProps<SlideNide>) {
const moveToNextSlide = useCallback((id: string) => {}, []);
return (
<article className="slide nodrag" style={style}>
<Remark>{data.source}</Remark>
<footer className="slide__controls nopan">
{data.left && (<button onClick={() => moveToNextSlide(data.left)}>←</button>)}
{data.up && (<button onClick={() => moveToNextSlide(data.up)}>↑</button>)}
{data.down && (<button onClick={() => moveToNextSlide(data.down)}>↓</button>)}
{data.right && (<button onClick={() => moveToNextSlide(data.right)}>→</button>)}
</footer>
</article>
);
}你可以随意设置页脚的样式,但重要的是要添加 "nopan" 类,以防在与任何按钮交互时画布移动。
🌐 You can style the footer however you like, but it’s important to add the "nopan" class
to prevent prevent the canvas from panning as you interact with any of the buttons.
为了实现 moveToSlide,我们将再次使用 fitView。之前我们有一个对实际被点击节点的引用,用于传递给 fitView,但这次我们只有节点的 id。你可能会想通过 id 查找目标节点,但实际上没有必要!如果我们查看 FitViewOptions 的类型,我们可以看到我们传入的节点数组只需要有一个 id 属性:
🌐 To implement moveToSlide, we’ll make use of fitView again. Previously we had a
reference to the actual node that was clicked on to pass to fitView, but this time we
only have a node’s id. You might be tempted to look up the target node by its id, but
actually that’s not necessary! If we look at the type of
FitViewOptions we can see that the array of
nodes we pass in only needs to have an id property:
export type FitViewOptions = {
padding?: number;
includeHiddenNodes?: boolean;
minZoom?: number;
maxZoom?: number;
duration?: number;
nodes?: (Partial<Node> & { id: Node['id'] })[];
};Partial<Node> 意味着 Node 对象类型的所有字段都会被标记为可选,然后我们将其与 { id: Node['id'] } 进行交集以确保 id 字段始终是必需的。这意味着我们可以只传入一个带有 id 属性的对象,而不需要其他内容,fitView 就会知道如何处理它!
import { type NodeProps, useReactFlow } from '@xyflow/react';
export function Slide({ data }: NodeProps<SlideNide>) {
const { fitView } = useReactFlow();
const moveToNextSlide = useCallback(
(id: string) => fitView({ nodes: [{ id }] }),
[fitView],
);
return (
<article className="slide" style={style}>
...
</article>
);
}键盘导航
🌐 Keyboard navigation
拼图的最后一块是为我们的演示文稿添加键盘导航。每次都必须点击幻灯片才能移动到下一张并不是很方便,所以我们将添加一些键盘快捷键以便操作更轻松。React Flow 允许我们通过 <ReactFlow /> 组件上的处理函数(如 onKeyDown)来监听键盘事件。
🌐 The final piece of the puzzle is to add keyboard navigation to our presentation. It’s not
very convenient to have to always click on a slide to move to the next one, so we’ll add
some keyboard shortcuts to make it easier. React Flow lets us listen to keyboard events on
the <ReactFlow /> component through handlers like
onKeyDown.
到目前为止,目前聚焦的幻灯片是由画布的位置来暗示的,但如果我们想在整个画布上处理按键,我们需要_明确地_跟踪当前幻灯片。我们需要这样做,因为我们需要知道按下箭头键时要导航到哪一张幻灯片!
🌐 Up until now the slide currently focused is implied by the position of the canvas, but if we want to handle key presses on the entire canvas we need to explicitly track the current slide. We need to this because we need to know which slide to navigate to when an arrow key is pressed!
import { useState, useCallback } from 'react';
import { ReactFlow, useReactFlow } from '@xyflow/react';
import { Slide, SlideData, SLIDE_WIDTH } from './Slide';
const slides: Record<string, SlideData> = {
...
}
const nodeTypes = {
slide: Slide,
};
const initialSlide = '0';
const { nodes, edges } = slidesToElements(initialSlide, slides)
export default function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
const handleNodeClick = useCallback<NodeMouseHandler>(
(_, node) => {
fitView({ nodes: [node] });
setCurrentSlide(node.id);
},
[fitView],
);
return (
<ReactFlow
...
onNodeClick={handleNodeClick}
/>
);
}在这里,我们在我们的流程组件中添加了一点状态,currentSlide,并且我们确保每当点击一个节点时都会更新它。接下来,我们将编写一个回调来处理画布上的键盘事件:
🌐 Here we’ve added a bit of state, currentSlide, to our flow component and we’re making
sure to update it whenever a node is clicked. Next, we’ll write a callback to handle
keyboard events on the canvas:
export default function App() {
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const { fitView } = useReactFlow();
...
const handleKeyPress = useCallback<KeyboardEventHandler>(
(event) => {
const slide = slides[currentSlide];
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
const direction = event.key.slice(5).toLowerCase();
const target = slide[direction];
if (target) {
event.preventDefault();
setCurrentSlide(target);
fitView({ nodes: [{ id: target }] });
}
}
},
[currentSlide, fitView],
);
return (
<ReactFlow
...
onKeyPress={handleKeyPress}
/>
);
}为了节省一些输入,我们从按下的键中提取方向——如果用户按下 'ArrowLeft',我们将得到 'left',以此类推。然后,如果该方向确实有幻灯片连接,我们将更新当前幻灯片并调用 fitView 导航到它!
🌐 To save some typing we’re extracting the direction from the key pressed - if the user
pressed 'ArrowLeft' we’ll get 'left' and so on. Then, if there is actually a slide
connected in that direction we’ll update the current slide and call fitView to navigate
to it!
我们还阻止了箭头键的默认行为,以防止窗口上下滚动。这在本教程中是必要的,因为画布只是页面的一部分,但对于画布占据整个视口的应用来说,你可能不需要这样做。
🌐 We’re also preventing the default behavior of the arrow keys to prevent the window from scrolling up and down. This is necessary for this tutorial because the canvas is only one part of the page, but for an app where the canvas is the entire viewport you might not need to do this.
就这些!回顾一下,让我们看看最终结果,并讨论我们学到了什么。
🌐 And that’s everything! To recap let’s look at the final result and talk about what we’ve learned.
最后的想法
🌐 Final thoughts
即使你不打算制作下一个 Prezi ,我们在本教程中仍然查看了 React Flow 的一些有用功能:
🌐 Even if you’re not planning on making the next Prezi , we’ve still looked at a few useful features of React Flow in this tutorial:
- 使用
useReactFlow钩子来访问fitView方法。 onNodeClick事件处理程序,用于监听流程中每个节点的点击。[onKeyPress](/api-reference/react-flow#on-key-press)事件处理程序用于监听整个画布上的键盘事件。
我们也研究了如何自己实现一个简单的布局算法。布局是一个我们经常被问到的非常常见的问题,但如果你的需求不那么复杂,自己实现一个解决方案也可以取得不错的效果!
🌐 We’ve also looked at how to implement a simple layouting algorithm ourselves. Layouting is a really common question we get asked about, but if your needs aren’t that complex you can get quite far rolling your own solution!
如果你正在寻找关于如何扩展这个项目的想法,你可以尝试解决我们指出的布局算法问题,设计一个具有不同布局的更复杂的 Slide 组件,或者完全尝试其他的东西。
🌐 If you’re looking for ideas on how to extend this project, you could try addressing the
issues we pointed out with the layouting algorithm, coming up with a more sophisticated
Slide component with different layouts, or something else entirely.
你可以使用完整的 源代码 作为起点,或者你也可以继续在我们今天所做的基础上构建。我们很想看到你构建的内容,所以请通过我们的 Discord 服务器 或 Twitter 与我们分享。
🌐 You can use the completed source code as a starting point, or you can just keep building on top of what we’ve made today. We’d love to see what you build so please share it with us over on our Discord server or Twitter .