mcpField
S
svelte-flow

svelte-flow

github.com/freightcognition开发442 次安装2 周前更新
代码仓库

关于 svelte-flow

svelte-flow 是面向使用 @xyflow/svelte 构建节点界面的开发者指南,涵盖图形初始化、自定义节点与边、事件处理、布局及性能注意事项。

主要功能

  1. 01配置节点和边
  2. 02创建自定义节点与边类型
  3. 03处理图形交互事件
  4. 04使用 Dagre 排列节点
  5. 05使用图形工具与组件
  6. 06管理状态并导出图像

适用场景

  1. 01构建 Svelte 节点编辑器
  2. 02添加自定义图形交互
  3. 03分层排列图形元素
  4. 04将图形视图导出为图像

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/freightcognition/linehaulai-claude-marketplace --skill svelte-flow

安装后在 Claude Code 中输入/svelte-flow确认已加载。

详细介绍

Svelte Flow

使用 Svelte Flow(@xyflow/svelte)构建基于节点的 UI 的专家指南。

安装

npm install @xyflow/svelte

核心设置

<script lang="ts">
  import { SvelteFlow, Background } from '@xyflow/svelte';
  import '@xyflow/svelte/dist/style.css';
  
  // Use $state.raw for nodes and edges (Svelte 5)
  let nodes = $state.raw([
    { id: '1', data: { label: 'Node 1' }, position: { x: 0, y: 0 } },
    { id: '2', data: { label: 'Node 2' }, position: { x: 0, y: 100 } }
  ]);
  
  let edges = $state.raw([
    { id: 'e1-2', source: '1', target: '2' }
  ]);
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Background />
</SvelteFlow>

关键概念

节点

每个节点都需要:

  • id:唯一标识符
  • position:{ x: number, y: number }
  • data:包含自定义属性的对象(通常包括 label)
  • 可选:type、style、hidden、width、height、sourcePosition、targetPosition
const node = {
  id: '1',
  type: 'custom',
  position: { x: 100, y: 100 },
  data: { label: 'My Node', color: '#ff0000' },
  style: 'background: #f0f0f0',
  width: 150,
  height: 100
};
边

每条边都需要:

  • id:唯一标识符
  • source:源节点 ID
  • target:目标节点 ID
  • 可选:type、label、animated、markerEnd、markerStart、style、hidden
const edge = {
  id: 'e1-2',
  source: '1',
  target: '2',
  type: 'smoothstep',
  label: 'connects to',
  animated: true,
  markerEnd: { type: MarkerType.ArrowClosed }
};
内置类型

边类型:

  • default:直线
  • smoothstep:平滑的 90 度转弯
  • step:直角 90 度转弯
  • straight:default 的别名
  • bezier:贝塞尔曲线

节点类型:

  • default:基本矩形节点
  • input:只有源句柄的节点
  • output:只有目标句柄的节点

自定义节点


<script lang="ts" module>
  import type { Node, NodeProps } from '@xyflow/svelte';
  
  export type CustomNodeType = Node<{
    label: string;
    color?: string;
  }>;
</script>

<script lang="ts">
  import { Handle, Position } from '@xyflow/svelte';
  
  let { data }: NodeProps<CustomNodeType> = $props();
</script>

<div class="custom-node" style:background={data.color}>
  <Handle type="target" position={Position.Left} />
  <div>{data.label}</div>
  <Handle type="source" position={Position.Right} />
</div>

<style>
  .custom-node {
    padding: 10px;
    border-radius: 5px;
    border: 1px solid #ddd;
  }
</style>

注册自定义节点:

<script lang="ts">
  import CustomNode from './CustomNode.svelte';
  
  const nodeTypes = {
    custom: CustomNode
  };
  
  let nodes = $state.raw([
    { id: '1', type: 'custom', data: { label: 'Custom', color: '#ff7000' }, position: { x: 0, y: 0 } }
  ]);
</script>

<SvelteFlow bind:nodes {nodeTypes} bind:edges fitView />

自定义边


<script lang="ts">
  import { BaseEdge, EdgeLabel, getStraightPath, type EdgeProps } from '@xyflow/svelte';
  
  let { id, sourceX, sourceY, targetX, targetY, label }: EdgeProps = $props();
  
  let [edgePath, labelX, labelY] = $derived(
    getStraightPath({ sourceX, sourceY, targetX, targetY })
  );
</script>

<BaseEdge {id} path={edgePath} />
{#if label}
  <EdgeLabel x={labelX} y={labelY}>
    <div class="edge-label">{label}</div>
  </EdgeLabel>
{/if}

可用的路径辅助函数:

  • getStraightPath()
  • getBezierPath()
  • getSmoothStepPath()

注册自定义边:

<script lang="ts">
  const edgeTypes = {
    custom: CustomEdge
  };
</script>

<SvelteFlow bind:nodes bind:edges {edgeTypes} fitView />

更新状态

**重要:**节点和边不可变。创建新对象以触发更新。

<script lang="ts">
  // ❌ Won't work - direct mutation
  nodes[0].position.x = 100;
  
  // ✅ Works - create new array
  nodes = nodes.map((node) => {
    if (node.id === '1') {
      return { ...node, position: { ...node.position, x: 100 } };
    }
    return node;
  });
  
  // ✅ Also works - using helper from useSvelteFlow
  import { useSvelteFlow } from '@xyflow/svelte';
  const { updateNode } = useSvelteFlow();
  
  updateNode('1', (node) => ({
    ...node,
    position: { ...node.position, x: 100 }
  }));
</script>

事件处理

常用事件处理器:

<SvelteFlow
  bind:nodes
  bind:edges
  onnodeclick={(event) => console.log('node clicked', event.targetNode)}
  onnodedrag={(event) => console.log('node dragging', event.targetNode)}
  onnodedragstop={(event) => console.log('drag ended', event.targetNode)}
  onedgeclick={(event) => console.log('edge clicked', event.edge)}
  onconnect={(params) => {
    edges = [...edges, { id: `e${params.source}-${params.target}`, ...params }];
  }}
/>

可用事件:

  • onnodeclick、onnodedoubleclick
  • onnodedrag、onnodedragstart、onnodedragstop
  • onedgeclick、onedgedoubleclick
  • onconnect、onconnectstart、onconnectend
  • onbeforedelete(异步,可阻止删除)
  • oninit、onmove、onmovestart、onmoveend

布局算法

Dagre(分层布局)
import dagre from '@dagrejs/dagre';
import { Position } from '@xyflow/svelte';

function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
  const dagreGraph = new dagre.graphlib.Graph();
  dagreGraph.setDefaultEdgeLabel(() => ({}));
  
  const isHorizontal = direction === 'LR';
  dagreGraph.setGraph({ rankdir: direction });
  
  const nodeWidth = 172;
  const nodeHeight = 36;
  
  nodes.forEach((node) => {
    dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  });
  
  edges.forEach((edge) => {
    dagreGraph.setEdge(edge.source, edge.target);
  });
  
  dagre.layout(dagreGraph);
  
  const layoutedNodes = nodes.map((node) => {
    const nodeWithPosition = dagreGraph.node(node.id);
    return {
      ...node,
      targetPosition: isHorizontal ? Position.Left : Position.Top,
      sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
      position: {
        x: nodeWithPosition.x - nodeWidth / 2,
        y: nodeWithPosition.y - nodeHeight / 2,
      },
    };
  });
  
  return { nodes: layoutedNodes, edges };
}

高级功能

浮动边

动态连接到节点边界上最近点的边:


<script lang="ts">
  import { BaseEdge, getBezierPath, type EdgeProps } from '@xyflow/svelte';
  import { getEdgeParams } from './utils';
  
  let { id, source, target, markerEnd }: EdgeProps = $props();
  
  const { sx, sy, tx, ty, sourcePos, targetPos } = $derived(
    getEdgeParams(source, target)
  );
  
  let [edgePath] = $derived(
    getBezierPath({
      sourceX: sx,
      sourceY: sy,
      sourcePosition: sourcePos,
      targetX: tx,
      targetY: ty,
      targetPosition: targetPos,
    })
  );
</script>

<BaseEdge {id} path={edgePath} {markerEnd} />

使用 ConnectionMode.Loose 注册:

<script lang="ts">
  import { ConnectionMode } from '@xyflow/svelte';
</script>

<SvelteFlow
  bind:nodes
  bind:edges
  edgeTypes={{ floating: FloatingEdge }}
  connectionMode={ConnectionMode.Loose}
  fitView
/>
邻近连接

拖动节点靠近彼此时自动连接:

<script lang="ts">
  const MIN_DISTANCE = 150;
  
  function getClosestEdge(node: Node, nodes: Node[]) {
    const closestNode = nodes.reduce(
      (res, n) => {
        if (n.id !== node.id) {
          const dx = n.position.x - node.position.x;
          const dy = n.position.y - node.position.y;
          const d = Math.sqrt(dx * dx + dy * dy);
          
          if (d < res.distance && d < MIN_DISTANCE) {
            res.distance = d;
            res.node = n;
          }
        }
        return res;
      },
      { distance: Number.MAX_VALUE, node: null }
    );
    
    if (!closestNode.node) return null;
    
    const closeNodeIsSource = closestNode.node.position.x < node.position.x;
    
    return {
      id: closeNodeIsSource
        ? `${node.id}-${closestNode.node.id}`
        : `${closestNode.node.id}-${node.id}`,
      source: closeNodeIsSource ? closestNode.node.id : node.id,
      target: closeNodeIsSource ? node.id : closestNode.node.id,
      class: 'temp',
    };
  }
  
  function onNodeDrag({ targetNode: node }) {
    const closestEdge = getClosestEdge(node, nodes);
    
    edges = edges.filter(e => e.class !== 'temp');
    
    if (closestEdge && !edges.some(e => e.source === closestEdge.source && e.target === closestEdge.target)) {
      edges = [...edges, closestEdge];
    }
  }
  
  function onNodeDragStop() {
    edges = edges.map((edge) => 
      edge.class === 'temp' ? { ...edge, class: '' } : edge
    );
  }
</script>

<SvelteFlow
  bind:nodes
  bind:edges
  onnodedrag={onNodeDrag}
  onnodedragstop={onNodeDragStop}
  fitView
/>
边重新连接

允许用户将边的端点拖到其他节点:


<script lang="ts">
  import {
    BaseEdge,
    EdgeReconnectAnchor,
    getBezierPath,
    type EdgeProps
  } from '@xyflow/svelte';
  
  let { sourceX, sourceY, targetX, targetY, selected, ...props }: EdgeProps = $props();
  
  const [edgePath] = $derived(
    getBezierPath({ sourceX, sourceY, targetX, targetY })
  );
  
  let reconnecting = $state(false);
</script>

{#if !reconnecting}
  <BaseEdge path={edgePath} {...props} />
{/if}

{#if selected}
  <EdgeReconnectAnchor
    bind:reconnecting
    type="source"
    position={{ x: sourceX, y: sourceY }}
  />
  <EdgeReconnectAnchor
    bind:reconnecting
    type="target"
    position={{ x: targetX, y: targetY }}
  />
{/if}
删除节点并重新连接边
<script lang="ts">
  import { getIncomers, getOutgoers, getConnectedEdges, type OnBeforeDelete } from '@xyflow/svelte';
  
  const onbeforedelete: OnBeforeDelete = async ({ nodes: deletedNodes }) => {
    let remainingNodes = [...nodes];
    
    edges = deletedNodes.reduce((acc, node) => {
      const incomers = getIncomers(node, remainingNodes, acc);
      const outgoers = getOutgoers(node, remainingNodes, acc);
      const connectedEdges = getConnectedEdges([node], acc);
      
      const remainingEdges = acc.filter((edge) => !connectedEdges.includes(edge));
      
      const createdEdges = incomers.flatMap(({ id: source }) =>
        outgoers.map(({ id: target }) => ({
          id: `${source}->${target}`,
          source,
          target,
        }))
      );
      
      remainingNodes = remainingNodes.filter((rn) => rn.id !== node.id);
      
      return [...remainingEdges, ...createdEdges];
    }, edges);
    
    nodes = remainingNodes;
    
    return true;
  };
</script>

<SvelteFlow bind:nodes bind:edges {onbeforedelete} fitView />

组件

控件
<script lang="ts">
  import { SvelteFlow, Controls } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Controls showLock={false} showFitView={true} showZoom={true} />
</SvelteFlow>
小地图
<script lang="ts">
  import { SvelteFlow, MiniMap } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <MiniMap />
</SvelteFlow>
背景
<script lang="ts">
  import { SvelteFlow, Background, BackgroundVariant } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</SvelteFlow>

变体:Dots、Lines、Cross

面板(自定义控件)
<script lang="ts">
  import { SvelteFlow, Panel } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Panel position="top-left">
    <button>Custom Button</button>
  </Panel>
</SvelteFlow>

位置:top-left、top-center、top-right、bottom-left、bottom-center、bottom-right

实用函数

import {
  getConnectedEdges,
  getIncomers,
  getOutgoers,
  addEdge,
  applyEdgeChanges,
  applyNodeChanges
} from '@xyflow/svelte';

// Get edges connected to nodes
const connectedEdges = getConnectedEdges(nodes, edges);

// Get nodes that connect TO a node
const incomers = getIncomers(targetNode, nodes, edges);

// Get nodes that a node connects TO
const outgoers = getOutgoers(sourceNode, nodes, edges);

// Add edge with validation
edges = addEdge(connection, edges);

状态管理(替代方案)

用于集中管理状态:

// store.svelte.js
let nodes = $state.raw([...]);
let edges = $state.raw([...]);

export const getNodes = () => nodes;
export const getEdges = () => edges;
export const setNodes = (newNodes) => nodes = newNodes;
export const setEdges = (newEdges) => edges = newEdges;

<script>
  import { getNodes, getEdges, setNodes, setEdges } from './store.svelte.js';
</script>

<SvelteFlow bind:nodes={getNodes, setNodes} bind:edges={getEdges, setEdges} />

导出为图像

<script lang="ts">
  import { useSvelteFlow } from '@xyflow/svelte';
  
  const { toObject, getViewport } = useSvelteFlow();
  
  async function downloadImage() {
    const { toPng } = await import('html-to-image');
    const dataUrl = await toPng(document.querySelector('.svelte-flow'));
    const a = document.createElement('a');
    a.setAttribute('download', 'flow.png');
    a.setAttribute('href', dataUrl);
    a.click();
  }
</script>

性能提示

  • 对节点和边使用 $state.raw(避免深层响应式)
  • 对于大型图(1000 个以上节点),设置 elevateNodesOnSelect={false} 和 elevateEdgesOnSelect={false}
  • 使用 minZoom 和 maxZoom 限制缩放级别
  • 对于超大型图,考虑使用虚拟化

常用属性

<SvelteFlow
  bind:nodes
  bind:edges
  nodeTypes={customNodeTypes}
  edgeTypes={customEdgeTypes}
  defaultEdgeOptions={{ animated: true, type: 'smoothstep' }}
  connectionLineType={ConnectionLineType.Straight}
  connectionMode={ConnectionMode.Loose}
  fitView
  minZoom={0.1}
  maxZoom={2}
  snapToGrid={true}
  snapGrid={[15, 15]}
  elevateNodesOnSelect={true}
  elevateEdgesOnSelect={true}
  deleteKeyCode="Delete"
  selectionKeyCode="Shift"
  multiSelectionKeyCode="Meta"
  panOnScroll={true}
  zoomOnScroll={true}
  zoomOnDoubleClick={true}
  zoomOnPinch={true}
  panOnDrag={true}
/>

评论

支持 Markdown