关键字

  • Storybook:组件文档

  • click-to-react-component:定位组件

  • dnd:拖拽高阶组件

  • allotment:拆分窗格

  • @babel/standalone:babel的浏览器版本

  • @types/babel__core:babel插件

  • reactflow

click-to-react-component

React19用不了

安装

1
npm install --save-dev click-to-react-component

引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
// @ts-ignore
import { ClickToComponent } from 'click-to-react-component';

ReactDOM.createRoot(document.getElementById('root')!).render(
<>
<ClickToComponent />
<App />
</>
)

可能有类型的报错,我们直接 @ts-ignore 忽略好了。

使用

img

按住Alt +左键就可以直接打开对应 的组件源码

如果按住Alt+右键就可以看到它所有的父级组件,然后选一个打开

image-20250329172340836

img

原理

dom 元素有 __reactFiber$ 属性可以拿到对应 fiber 节点,然后 _debugOwner 拿到父节点 fiber。_debugSource 拿到源码文件路径和行列号。

然后通过 vscode://file/xxx 的方式直接 vscode 打开对应文件行列号。

这样就完成了点击页面元素,打开对应源码的功能。

这里的 _debugSource 是 babel 插件做的,在 @babel/preset-env 里,每个项目会都自动引入这个插件。

然后加上用 dataset.xx 定义属性和对应的 className,用 @floating-ui 实现 popover。

这就是 click-to-react-component 的实现原理了。

@babel/standalone

安装

1
2
npm i --save @babel/standalone
npm i --save-dev @types/babel__standalone

reactflow

安装

1
2
npm install
npm install --save @xyflow/react

使用

1
import '@xyflow/react/dist/style.css';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { addEdge, Background, BackgroundVariant, BaseEdge, Connection, Controls, EdgeLabelRenderer, EdgeProps, getBezierPath, getStraightPath, Handle, MiniMap, OnConnect, Position, ReactFlow, useEdgesState, useNodesState, useReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';

const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, type: 'red', data: { label: '1' } },
{ id: '2', position: { x: 200, y: 300 }, type: 'blue', data: { label: '2' } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', type: 'custom' }];

interface NodePorps {
data: {
label: string
}
}
function RedNode({ data }: NodePorps) {
return <div style={{background: 'red', width: '100px', height: '100px', textAlign: 'center'}}>
<Handle type="source" position={Position.Right} />
<Handle type="target" position={Position.Bottom} />

<div>{data.label}</div>
</div>
}

function BlueNode({ data }: NodePorps) {
return <div style={{background: 'blue', width: '50px', height: '50px', textAlign: 'center', color: '#fff'}}>
<Handle type="source" position={Position.Bottom} />
<Handle type="target" position={Position.Top} />

<div>{data.label}</div>
</div>
}

function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {

const { setEdges } = useReactFlow();

const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});

const onEdgeClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
};

return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
// EdgeLabelRenderer 里的组件默认不处理鼠标事件如果要处理就要声明 pointerEvents: all
pointerEvents: 'all',
}}
>
<button onClick={onEdgeClick}>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}

export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

const onConnect = (params: Connection) => {
setEdges((eds) => addEdge(params, eds))
}

return (
<div style={{ width: '800px', height: '500px', border: '1px solid #000', margin: '50px auto' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={{
red: RedNode,
blue: BlueNode
}}
edgeTypes={{
custom: CustomEdge
}}
>
<Controls/>
<MiniMap zoomable/>
<Background variant={BackgroundVariant.Lines}/>
</ReactFlow>
</div>
);
}

当然,这里是一篇讲清楚 React Flow 使用方法的快速入门博客,围绕你提供的代码展开,重点是 API 用法清晰逻辑易懂,让你或别人读完就能上手开发流程图。


🧱 核心结构

React Flow 的基本结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={...}
onEdgesChange={...}
onConnect={...}
nodeTypes={...}
edgeTypes={...}
>
<Controls />
<MiniMap />
<Background />
</ReactFlow>

它的核心是:

  • nodes: 节点数组
  • edges: 边数组
  • onNodesChange: 节点拖动/修改时触发
  • onEdgesChange: 边更新时触发
  • onConnect: 连线时触发
  • nodeTypes, edgeTypes: 自定义节点/边的实现

🧠 节点定义

你可以定义任意风格的节点,核心组件是 Handle,它定义了可以连线的位置。

1
2
3
4
5
6
7
8
9
function RedNode({ data }: NodeProps) {
return (
<div style={{ background: 'red', width: 100, height: 100 }}>
<Handle type="source" position={Position.Right} />
<Handle type="target" position={Position.Bottom} />
<div>{data.label}</div>
</div>
);
}
  • type="source" 表示这是连线起点,target 是终点。
  • position 决定了哪个方向可以拖出线(例如右边或底部)。
  • data 是传入的数据,例如 label 文本。

你可以在 nodeTypes 里注册这些组件:

1
2
3
4
nodeTypes={{
red: RedNode,
blue: BlueNode
}}

对应的 node 配置就像这样:

1
{ id: '1', position: { x: 0, y: 0 }, type: 'red', data: { label: '1' } }

🧩 边定义(Edge)

你可以用内置的边类型(defaultstepsmoothstep 等),也可以像你代码中那样自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function CustomEdge(props: EdgeProps) {
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = props;
const { setEdges } = useReactFlow();

const [path, labelX, labelY] = getBezierPath({
sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition
});

return (
<>
<BaseEdge path={path} />
<EdgeLabelRenderer>
<div style={{ position: 'absolute', transform: `translate(${labelX}px, ${labelY}px)` }}>
<button onClick={() => setEdges(eds => eds.filter(e => e.id !== id))}>×</button>
</div>
</EdgeLabelRenderer>
</>
);
}

这个例子用的是 getBezierPath() 来生成一条贝塞尔曲线,EdgeLabelRenderer 则允许你在边上渲染交互组件。

注册方式类似:

1
2
3
edgeTypes={{
custom: CustomEdge
}}

⚙️ 状态管理 Hooks

React Flow 提供了一套官方 Hook 来简化节点/边状态的管理:

1
2
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

它们内置了对节点/边的增删改追踪,你只要传进 ReactFlow 组件就行。


🔌 连线逻辑 onConnect

每次用户拖线时,onConnect 会触发,你可以决定如何处理这条线:

1
2
3
const onConnect = (params: Connection) => {
setEdges(eds => addEdge(params, eds));
}

addEdge 是官方提供的一个工具函数,帮你生成 idsourcetarget 等字段。


🧰 附加组件:Controls、MiniMap、Background

  • <Controls />: 左下角放大缩小居中按钮
  • <MiniMap />: 右下角的小地图
  • <Background />: 背景网格,可选 dots, lines, cross 等风格
1
2
3
<Background variant={BackgroundVariant.Lines} />
<MiniMap zoomable />
<Controls />

✅ 最后完整结构复盘

1
2
3
4
5
6
7
8
9
10
11
12
13
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={{ red: RedNode, blue: BlueNode }}
edgeTypes={{ custom: CustomEdge }}
>
<Controls />
<MiniMap zoomable />
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>

🧭 小结:快速上手建议

操作 做法
添加节点 nodes 添加一个新对象
添加边 addEdge 或手动 setEdges([...edges, newEdge])
自定义样式 重写 node 或 edge 组件
删除节点/边 setNodes / setEdges 过滤掉对应 id
响应连线 onConnect 添加边
拖动/缩放/迷你图 加上 <Controls/> <MiniMap/> <Background/>

📦 提示:样式和依赖

确保你引入了 React Flow 样式:

1
import '@xyflow/react/dist/style.css';

并安装:

1
npm install @xyflow/react

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { addEdge, Background, BackgroundVariant, BaseEdge, Connection, Controls, EdgeLabelRenderer, EdgeProps, getBezierPath, getStraightPath, Handle, MiniMap, OnConnect, Position, ReactFlow, useEdgesState, useNodesState, useReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';

const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, type: 'red', data: { label: '1' } },
{ id: '2', position: { x: 200, y: 300 }, type: 'blue', data: { label: '2' } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', type: 'custom' }];

interface NodePorps {
data: {
label: string
}
}
function RedNode({ data }: NodePorps) {
return <div style={{background: 'red', width: '100px', height: '100px', textAlign: 'center'}}>
<Handle type="source" position={Position.Right} />
<Handle type="target" position={Position.Bottom} />

<div>{data.label}</div>
</div>
}

function BlueNode({ data }: NodePorps) {
return <div style={{background: 'blue', width: '50px', height: '50px', textAlign: 'center', color: '#fff'}}>
<Handle type="source" position={Position.Bottom} />
<Handle type="target" position={Position.Top} />

<div>{data.label}</div>
</div>
}

function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {

const { setEdges } = useReactFlow();

const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});

const onEdgeClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
};

return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
// EdgeLabelRenderer 里的组件默认不处理鼠标事件如果要处理就要声明 pointerEvents: all
pointerEvents: 'all',
}}
>
<button onClick={onEdgeClick}>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}

export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

const onConnect = (params: Connection) => {
setEdges((eds) => addEdge(params, eds))
}

return (
<div style={{ width: '800px', height: '500px', border: '1px solid #000', margin: '50px auto' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={{
red: RedNode,
blue: BlueNode
}}
edgeTypes={{
custom: CustomEdge
}}
>
<Controls/>
<MiniMap zoomable/>
<Background variant={BackgroundVariant.Lines}/>
</ReactFlow>
</div>
);
}