• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

这节我们来画下流程图。

创建个项目:

npx create-vite audio-flow

image.png

进入项目,安装下 reactflow

npm install
npm install --save @xyflow/react

去掉 index.css

image.png

然后改下 App.tsx

import {
    addEdge,
    Background,
    BackgroundVariant,
    Connection,
    Controls,
    MiniMap,
    OnConnect,
    ReactFlow,
    useEdgesState,
    useNodesState,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
    { id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
    { id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

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: "100vw", height: "100vh" }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}>
                <Controls />
                <MiniMap />
                <Background variant={BackgroundVariant.Lines} />
            </ReactFlow>
        </div>
    );
}

我们写了下基础代码,加了两个 node,一个 edge,然后加了 Controles、Background、MiniMap 组件。

跑起来看一下:

npm run dev

image.png

2024-08-29 14.42.48.gif

没啥问题,只是流程图不在正中央。

加个 fitView 就好了:

image.png

image.png

接下来分别实现这三种自定义节点:

image.png

我们用 tailwind 来写样式。

按照 tailwind 文档里的步骤安装 tailwind:

npm install -D tailwindcss postcss autoprefixer

npx tailwindcss init -p

会生成 tailwind 和 postcss 配置文件:

修改下 content 配置,也就是从哪里提取 className:

/** @type {import('tailwindcss').Config} */
export default {
    content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
    theme: {
        extend: {},
    },
    plugins: [],
};

tailwind 会提取 className 之后按需生成最终的 css。

改下 index.css 引入 tailwind 基础样式:

@tailwind base;
@tailwind components;
@tailwind utilities;

在 main.tsx 里引入:

image.png

如果你没安装 tailwind 插件,需要安装一下:

这样在写代码的时候就会提示 className 和对应的样式值:

不知道 className 叫啥的样式,还可以在 tailwind 文档里搜:

接下来创建振荡器的自定义节点:

components/OscillatorNode.tsx

import { Handle, Position } from "@xyflow/react";

export interface OscillatorNodeProps {
    id: string;
    data: {
        frequency: number;
        type: string;
    };
}

export function OscillatorNode({ id, data }: OscillatorNodeProps) {
    return (
        <div className={"bg-white shadow-xl"}>
            <p className={"rounded-t-md p-[8px] bg-pink-500 text-white"}>
                振荡器节点
            </p>
            <div className={"flex flex-col p-[8px]"}>
                <span>频率</span>
                <input
                    type="range"
                    min="10"
                    max="1000"
                    value={data.frequency}
                />
                <span className={"text-right"}>{data.frequency}赫兹</span>
            </div>
            <hr className={"mx-[4px]"} />
            <div className={"flex flex-col p-[8px]"}>
                <p>波形</p>
                <select value={data.type}>
                    <option value="sine">正弦波</option>
                    <option value="triangle">三角波</option>
                    <option value="sawtooth">锯齿波</option>
                    <option value="square">方波</option>
                </select>
            </div>
            <Handle type="source" position={Position.Bottom} />
        </div>
    );
}

就是一个标题,一个 input,一个 select,用 tailwind 写下样式。

可以通过 data 传入 frequency、type

用一下:

image.png

import {
    addEdge,
    Background,
    BackgroundVariant,
    Connection,
    Controls,
    MiniMap,
    OnConnect,
    ReactFlow,
    useEdgesState,
    useNodesState,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { OscillatorNode } from "./components/OscillatorNode";

const initialNodes = [
    {
        id: "1",
        position: { x: 0, y: 0 },
        data: { frequency: 300, type: "square" },
        type: "osc",
    },
    { id: "2", position: { x: 0, y: 300 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

const nodeTypes = {
    osc: OscillatorNode,
};

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: "100vw", height: "100vh" }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                nodeTypes={nodeTypes}
                fitView>
                <Controls />
                <MiniMap />
                <Background variant={BackgroundVariant.Lines} />
            </ReactFlow>
        </div>
    );
}

看下效果: image.png

可以看到,节点替换为了我们自定义的节点,并且根据传入的 data 做了表单回显。

接下来写下第二种自定义节点:

components/VolumeNode.tsx

import { Handle, Position } from "@xyflow/react";

export interface VolumeNodeProps {
    id: string;
    data: {
        gain: number;
    };
}

export function VolumeNode({ id, data }: VolumeNodeProps) {
    return (
        <div className={"rounded-md bg-white shadow-xl"}>
            <Handle type="target" position={Position.Top} />

            <p className={"rounded-t-md p-[4px] bg-blue-500 text-white"}>
                音量节点
            </p>
            <div className={"flex flex-col p-[4px]"}>
                <p>Gain</p>
                <input
                    type="range"
                    min="0"
                    max="1"
                    step="0.01"
                    value={data.gain}
                />
                <p className={"text-right"}>{data.gain.toFixed(2)}</p>
            </div>

            <Handle type="source" position={Position.Bottom} />
        </div>
    );
}

主要是上下两个 Handle、中间一个 input。

用一下:

const initialNodes = [
    {
        id: "1",
        position: { x: 0, y: 0 },
        data: { frequency: 300, type: "square" },
        type: "osc",
    },
    {
        id: "2",
        position: { x: 0, y: 300 },
        data: { gain: 0.6 },
        type: "volume",
    },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

const nodeTypes = {
    osc: OscillatorNode,
    volume: VolumeNode,
};

看下效果:

image.png

可以看到,音量节点也渲染出来了。

然后来写最后一个节点:输出节点

image.png

components/OutputNode.tsx

import { Handle, Position } from "@xyflow/react";
import { useState } from "react";

export function OutputNode() {
    const [isRunning, setIsRuning] = useState(false);

    function toggleAudio() {
        setIsRuning((isRunning) => !isRunning);
    }

    return (
        <div className={"bg-white shadow-xl p-[20px]"}>
            <Handle type="target" position={Position.Top} />

            <div>
                <p>输出节点</p>
                <button onClick={toggleAudio}>
                    {isRunning ? (
                        <span role="img">🔈</span>
                    ) : (
                        <span role="img">🔇</span>
                    )}
                </button>
            </div>
        </div>
    );
}

用一下:

image.png

加一个节点类型,然后加一个节点、一条边。

const initialNodes = [
    {
        id: "1",
        position: { x: 0, y: 0 },
        data: { frequency: 300, type: "square" },
        type: "osc",
    },
    {
        id: "2",
        position: { x: 0, y: 300 },
        data: { gain: 0.6 },
        type: "volume",
    },
    { id: "3", position: { x: 0, y: 500 }, data: {}, type: "out" },
];
const initialEdges = [
    { id: "e1-2", source: "1", target: "2" },
    { id: "e2-3", source: "2", target: "3" },
];

const nodeTypes = {
    osc: OscillatorNode,
    volume: VolumeNode,
    out: OutputNode,
};

看下效果:

image.png

这样,三种自定义节点就都画出来了。

案例代码上传了小册仓库

总结

我们创建了 vite 项目,引入了 tailwind 来写样式。

然后实现了流程图的绘制,主要是三种自定义节点的绘制:

振荡器节点、音量节点、输出节点。

流程图画完了,下节来开发音频部分的功能。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
81.ReactFlow振荡器调音:项目介绍
Next
83.ReactFlow振荡器调音:合成声音