React 与 Web MIDI/HID:在 React 应用中构建声明式的硬件交互接口与设备状态实时映射

欢迎来到“肉体与代码的联姻”:React、Web MIDI 与 Web HID 的深度实战

各位好!欢迎来到今天的技术讲座。我是你们的讲师。

今天我们不讲什么“如何用 map 渲染列表”或者“怎么用 useEffect 避免无限循环”。那些东西,我在你的 App.js 里已经看腻了。今天,我们要干点更刺激的。我们要把手伸进你的电脑背后,去触碰那些真实的物理世界。

我们要谈论的是 Web MIDIWeb HID。我们要用 React 的声明式思维,去驯服那些像野兽一样的硬件设备。我们要把冰冷的二进制数据流,变成你 UI 上鲜活、滚动的状态。

准备好了吗?让我们把鼠标扔进垃圾桶,开始这场关于“触觉编程”的旅程。


第一部分:硬件界的“诺亚方舟”与“潘多拉魔盒”

在开始写代码之前,我们要先搞清楚我们手里拿的是什么牌。Web MIDI 和 Web HID,就像是浏览器这艘诺亚方舟里,装的两类不同的动物。

1. Web MIDI:数字管风琴的继承者

Web MIDI API 是为了那些“老古董”和“新酷炫”的乐器准备的。它模拟了传统的 MIDI 信号。想象一下,你的电脑是一个巨大的管风琴,或者一个合成器。

  • 它的特点: 它有节奏感,它是基于事件的。它有“通道”的概念,有“音符”的概念。
  • 它能干啥: 它可以让你控制合成器,让键盘上的按键变成钢琴键;它也可以让外部 MIDI 控制台控制你的网页音量、亮度、颜色。
  • 它的语言: 它是 3 字节或 3 字节以上的数据包。

2. Web HID:USB 线的直通卡

Web HID API 就不一样了。它是为了“万物皆可 USB”而生的。键盘、游戏手柄、无人机、甚至是你那台只有两颗灯泡的 Arduino。

  • 它的特点: 它是原始的,它是任意的。它没有固定的协议,它只管把数据“报告”给你。
  • 它能干啥: 它是通用的。你想控制一个 3D 打印机的温控?用 HID。你想用 Xbox 手柄玩网页游戏?用 HID。它就像一根 USB 线,直接插在浏览器和设备之间。
  • 它的语言: 它是“报告”。设备会每隔一段时间(或者一有变化)就向主机扔一个数据包。

第二部分:声明式 UI 的痛点——为什么我们需要“中间件”?

React 的核心哲学是什么?声明式。你告诉 React:“嘿,如果 statetrue,就渲染一个绿色的按钮;如果是 false,就渲染一个灰色的按钮。” React 负责把你的意图变成 DOM。

但是,硬件是命令式的。

  • React 说:“我想把灯变亮。”
  • 硬件说:“我要你发送这个十六进制的字节 0x90,带上这个音符 60,还有这个力度 127。”

如果你直接在 React 里写 document.querySelector('midi-device').send(...),那你这就不是 React 了,你是在写 jQuery。而且,React 的状态更新是批处理的,它是异步的。如果你在 useEffect 里直接发 MIDI,你可能会发现你的 UI 变了,但硬件没反应,或者反应慢了半拍。

我们的挑战: 如何用 React 的“声明式”思维,去驱动“命令式”的硬件?

答案:中间态。 我们需要一个“状态层”。这个状态层既是 React 的数据源,也是硬件的指令源。


第三部分:构建“状态驱动的硬件控制器”

让我们来设计一个架构。我们不要让组件直接跟硬件对话,我们要创建一个 HardwareContext。这个 Context 就像一个“翻译官”或者“调度员”。

1. 定义状态结构

首先,我们要定义硬件的状态长什么样。

// types.ts

// MIDI 状态:我们假设我们有一个 8 键的控制器
// 每个键的状态包含:是否按下,力度(0-127)
type MidiState = {
  [key: number]: {
    isOn: boolean;
    velocity: number;
  };
};

// HID 状态:假设我们有一个摇杆和两个按钮
type HidState = {
  joystickX: number; // -32767 到 32767
  joystickY: number;
  btnA: boolean;
  btnB: boolean;
};

// 合并后的全局硬件状态
type HardwareState = {
  midi: MidiState;
  hid: HidState;
};

2. 创建“硬件连接器”

我们需要一个 Hook 来处理连接和事件监听。这是枯燥的部分,但也是地基。

Web MIDI 的连接

// useMidiConnection.ts
import { useState, useEffect } from 'react';

export const useMidiConnection = () => {
  const [midiAccess, setMidiAccess] = useState<MIDIAccess | null>(null);
  const [input, setInput] = useState<MIDIInput | null>(null);

  useEffect(() => {
    const initMidi = async () => {
      try {
        // 请求权限:这是浏览器给你的“开门钥匙”
        const access = await navigator.requestMIDIAccess();
        setMidiAccess(access);

        // 找到第一个输入设备(通常叫 "Generic MIDI Keyboard")
        const inputs = Array.from(access.inputs.values());
        if (inputs.length > 0) {
          setInput(inputs[0]);
        }
      } catch (err) {
        console.error("MIDI 连接失败,可能是因为用户拒绝了权限或浏览器不支持", err);
      }
    };

    initMidi();
  }, []);

  return { midiAccess, input };
};

Web HID 的连接

Web HID 比较棘手,因为它需要用户点击才能授权(为了安全)。

// useHidConnection.ts
import { useState, useEffect } from 'react';

export const useHidConnection = (vendorId: number, productId: number) => {
  const [device, setDevice] = useState<HIDDevice | null>(null);

  useEffect(() => {
    const connect = async () => {
      try {
        const devices = await navigator.hid.getDevices();
        const targetDevice = devices.find(d => d.vendorId === vendorId && d.productId === productId);

        if (targetDevice) {
          await targetDevice.open();
          setDevice(targetDevice);
        } else {
          console.log("设备未找到,请插上设备并点击屏幕上的按钮授权。");
        }
      } catch (err) {
        console.error("HID 连接错误", err);
      }
    };

    // 注意:这里我们使用一个空的 effect 依赖,实际项目中可能需要手动触发
    // 或者监听 navigator.hid.getDevices() 的变化
    connect();
  }, []);

  return device;
};

3. 核心映射逻辑

现在,我们需要把硬件的原始事件,转换成我们的 HardwareState

// useHardwareState.ts
import { useState, useEffect, useCallback } from 'react';
import { useMidiConnection } from './useMidiConnection';
import { useHidConnection } from './useHidConnection';
import type { HardwareState, MidiState, HidState } from './types';

// MIDI 映射配置:哪个 MIDI 通道对应哪个 React 状态键?
const MIDI_NOTE_MAP = [0, 1, 2, 3, 4, 5, 6, 7]; // 假设 8 个键

export const useHardwareState = () => {
  const [state, setState] = useState<HardwareState>({
    midi: {},
    hid: { joystickX: 0, joystickY: 0, btnA: false, btnB: false },
  });

  const { input: midiInput } = useMidiConnection();
  const { device: hidDevice } = useHidConnection(0x1234, 0x5678); // 替换为你的 VID/PID

  // --- 处理 MIDI 事件 ---
  useEffect(() => {
    if (!midiInput) return;

    const handleMidiMessage = (e: MIDIInputEvent) => {
      const [status, note, velocity] = e.data;

      // 解析 MIDI 命令
      // 0x90 = Note On, 0x80 = Note Off
      const command = status >> 4;
      const noteIndex = note % 8; // 映射到我们的 8 键数组

      setState(prev => {
        const newMidiState = { ...prev.midi };

        if (command === 0x9 && velocity > 0) {
          // Note On
          newMidiState[noteIndex] = { isOn: true, velocity };
        } else if (command === 0x8) {
          // Note Off
          newMidiState[noteIndex] = { isOn: false, velocity: 0 };
        }

        return { ...prev, midi: newMidiState };
      });
    };

    midiInput.onmidimessage = handleMidiMessage;
    return () => { midiInput.onmidimessage = null; };
  }, [midiInput]);

  // --- 处理 HID 事件 ---
  useEffect(() => {
    if (!hidDevice) return;

    const handleInputReport = (e: HIDInputEvent) => {
      // 这里是解析二进制数据的地方
      // 假设报告描述符是:[Button1, Button2, X, Y]
      // 注意:这里需要根据你的具体设备 Report Descriptor 来解析
      const report = e.data;

      // 这是一个简化的假设解析
      const btnA = report[0] === 1;
      const btnB = report[1] === 1;
      const x = (report[2] - 127) * 2; // 简单的偏移
      const y = (report[3] - 127) * 2;

      setState(prev => ({
        ...prev,
        hid: { joystickX: x, joystickY: y, btnA, btnB }
      }));
    };

    hidDevice.addEventListener('inputreport', handleInputReport);
    return () => { hidDevice.removeEventListener('inputreport', handleInputReport); };
  }, [hidDevice]);

  return state;
};

看到了吗? 现在的 state 是一个纯粹的、干净的、符合 TypeScript 类型定义的数据对象。它不关心数据是从 MIDI 挤出来的,还是从 HID 传出来的。它只关心状态。


第四部分:将状态注入硬件——“反向控制”

现在,我们不仅能读取硬件,还能控制硬件。这才是真正的交互。假设我们有一个 8 个 LED 的 MIDI 控制台,我们想让 React 的状态直接驱动这些 LED。

我们需要一个 useHardwareControl Hook。它的逻辑和上面相反:监听 React 的 State 变化 -> 发送命令给硬件。

// useHardwareControl.ts
import { useEffect } from 'react';
import type { HardwareState, MidiState } from './types';

// MIDI 输出映射:哪个 React 状态对应哪个 MIDI CC/Note
const LED_MIDI_MAP = [0, 1, 2, 3, 4, 5, 6, 7];

export const useHardwareControl = (midiOutput: MIDIOutput | null) => {
  // 我们只关心 MIDI 状态的变化
  useEffect(() => {
    if (!midiOutput) return;

    // 这里我们需要订阅状态变化,或者依赖某个全局 store
    // 为了演示方便,我们假设我们有一个全局状态订阅机制
    // 实际上,在 React 中,直接在这里 useEffect 监听状态不太优雅,
    // 更好的做法是使用一个专门的 Effect 依赖 [state.midi]

    // 假设我们有一个全局的状态订阅器,这里简化处理:
    // 实际代码应该类似这样:
    /*
    useEffect(() => {
        const unsubscribe = globalStore.subscribe(renderHardware);
        return unsubscribe;
    }, []);
    */

    // 下面是一个模拟:监听 React 状态变化
    // 注意:这只是一个示例,生产环境需要更高效的 Diff 算法
    const listener = (newState: HardwareState) => {
        const oldState = listener.lastState || { midi: {} };
        listener.lastState = newState;

        LED_MIDI_MAP.forEach((midiNote, index) => {
            const newLight = newState.midi[index];
            const oldLight = oldState.midi[index];

            if (newLight && !oldLight) {
                // 亮起
                midiOutput.send([0x90, midiNote, 127]); // Note On, Velocity 127
            } else if (!newLight && oldLight) {
                // 熄灭
                midiOutput.send([0x80, midiNote, 0]); // Note Off
            } else if (newLight && newLight.velocity !== oldLight?.velocity) {
                // 亮度变化
                midiOutput.send([0x90, midiNote, newLight.velocity]);
            }
        });
    };

    // 初始化调用
    listener({ midi: {}, hid: { joystickX: 0, joystickY: 0, btnA: false, btnB: false } });

    return listener;
  }, [midiOutput]);
};

注意: 在 React 中直接在 useEffect 里写这种“状态监听器”有点反模式。通常我们会使用一个专门的“硬件层”组件,它订阅全局状态,然后渲染。或者,我们可以使用 useEffect 监听特定的状态变量。

让我们重构一下,让它更 React 化:

// 更好的方式:直接监听特定的状态变化
export const useLedController = (midiOutput: MIDIOutput | null, notes: number[]) => {
  useEffect(() => {
    if (!midiOutput) return;

    // 我们假设 notes 是一个数组,长度等于我们想控制的 LED 数量
    return (state: HardwareState) => {
        state.midi.forEach((light, index) => {
            if (index < notes.length) {
                if (light.isOn) {
                    midiOutput.send([0x90, notes[index], light.velocity]);
                } else {
                    midiOutput.send([0x80, notes[index], 0]);
                }
            }
        });
    };
  }, [midiOutput, notes]);
};

第五部分:实战演练——构建一个“赛博朋克”音序器

好了,理论太多了,手痒了吧?让我们来造一个东西。

目标: 创建一个 React 组件,它是一个 16 步的音序器。
硬件: 一个 MIDI 键盘。
交互: 当你按下键盘上的某个键时,屏幕上的格子会高亮;当你松开时,格子变暗。

1. 组件结构

// Sequencer.tsx
import React, { useState, useEffect, useMemo } from 'react';
import { useHardwareState } from './hooks/useHardwareState';

const Sequencer = () => {
  const hardwareState = useHardwareState(); // 获取全局状态
  const [step, setStep] = useState(0);

  // 我们需要映射 MIDI 键到音序器步进
  // 假设 MIDI 键 36-51 对应音序器的 0-15 步
  const midiNotes = useMemo(() => {
    return Array.from({ length: 16 }, (_, i) => 36 + i);
  }, []);

  // 核心逻辑:根据硬件状态渲染 UI
  // 注意:这里我们并没有直接在 render 中判断硬件状态,
  // 而是利用了硬件状态驱动的 UI 变化。

  return (
    <div style={{ padding: '20px', fontFamily: 'monospace', color: '#0f0', background: '#000' }}>
      <h1>React MIDI Sequencer</h1>
      <div style={{ display: 'flex', gap: '2px' }}>
        {midiNotes.map((note, index) => {
          const isPressed = hardwareState.midi[index]?.isOn;

          return (
            <div
              key={index}
              style={{
                width: '30px',
                height: '100px',
                background: isPressed ? '#0f0' : '#333',
                border: '1px solid #555',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                fontWeight: 'bold',
                transition: 'background 0.1s'
              }}
            >
              {note}
            </div>
          );
        })}
      </div>
      <p>当前步进: {step}</p>
    </div>
  );
};

export default Sequencer;

看懂了吗?
React 的 render 函数非常干净。它根本不知道什么是 MIDIInputEvent,它也不知道什么是 navigator.requestMIDIAccess。它只知道:if (isPressed) return green; else return gray;

这就是声明式硬件交互的魔力!你把“如何检测硬件”和“如何渲染 UI”完全解耦了。

2. 进阶:双向绑定

现在,让我们加上“反向控制”。我们想用 React 的 UI 来控制 MIDI 设备。

// InteractiveSequencer.tsx
import React, { useState, useEffect } from 'react';
import { useHardwareState } from './hooks/useHardwareState';

const InteractiveSequencer = () => {
  const [selectedNote, setSelectedNote] = useState(0);
  const hardwareState = useHardwareState();

  // 我们需要获取 MIDI 输出设备
  // 这里简化处理,实际应该从 Context 获取
  const midiOutput = useMemo(() => {
    // 伪代码:从 useMidiConnection 获取输出
    return null; 
  }, []);

  // 监听硬件状态变化,更新 UI 选中状态
  useEffect(() => {
    // 检查是否有任何 MIDI 键被按下
    const pressedKeys = Object.keys(hardwareState.midi).filter(k => hardwareState.midi[k].isOn);
    if (pressedKeys.length > 0) {
        const lastPressed = pressedKeys[pressedKeys.length - 1];
        setSelectedNote(parseInt(lastPressed));
    }
  }, [hardwareState.midi]);

  // 监听 UI 变化,发送 MIDI 命令
  useEffect(() => {
    if (midiOutput) {
        // 发送 Note On
        midiOutput.send([0x90, selectedNote, 127]);

        // 设置一个定时器来发送 Note Off (模拟按键释放)
        const timer = setTimeout(() => {
            midiOutput.send([0x80, selectedNote, 0]);
        }, 500);

        return () => clearTimeout(timer);
    }
  }, [selectedNote, midiOutput]);

  return (
    <div>
        <p>点击下方的格子来触发 MIDI 信号,或者直接按键盘。</p>
        <div style={{ display: 'flex' }}>
            {[0, 1, 2, 3].map((n) => (
                <div
                    key={n}
                    style={{
                        width: '50px',
                        height: '50px',
                        background: selectedNote === n ? '#f0f' : '#ccc',
                        margin: '5px',
                        cursor: 'pointer'
                    }}
                    onClick={() => setSelectedNote(n)}
                >
                    {n}
                </div>
            ))}
        </div>
    </div>
  );
};

第六部分:深入解析——那些你可能遇到的“坑”

讲了这么多美好的东西,现实往往是残酷的。在 React 和硬件交互的战场上,你会遇到很多坑。

1. 浏览器权限与安全策略

浏览器是胆小的。它不会自动连上你的 MIDI 设备或 HID 设备。你必须让用户主动授权

  • MIDI: 使用 navigator.requestMIDIAccess()。这会弹出一个浏览器原生的对话框。如果你的应用没有用户交互(比如点击按钮),这会直接失败。
  • HID: 这更严格。浏览器要求你必须有一个用户手势(Click, Keydown 等)才能调用 navigator.hid.requestDevice()

解决方案: 在你的应用入口或者一个“设置”面板里,放置一个显眼的“连接设备”按钮。

const ConnectButton = () => {
  const [loading, setLoading] = useState(false);

  const handleConnect = async () => {
    setLoading(true);
    try {
        // 触发连接逻辑...
        await navigator.hid.requestDevice({ filters: [...] });
    } catch (e) {
        console.error("连接被拒绝");
    } finally {
        setLoading(false);
    }
  };

  return <button onClick={handleConnect} disabled={loading}>{loading ? "连接中..." : "连接硬件"}</button>;
};

2. 事件循环与 React 批处理

这是一个经典问题。假设你有一个 HID 设备,它以 1000Hz 的频率发送数据(比如一个摇杆)。每次数据到达,useEffect 都会触发,导致 setState 被调用。

如果 React 的批处理机制没有生效(或者你使用了多个 useState),你的 UI 可能会因为过于频繁的重渲染而卡顿。

解决方案:

  1. 节流: 不要对每个字节都渲染。只在状态改变时渲染。
  2. 使用 useLayoutEffect 而不是 useEffect 如果你需要同步硬件状态和 UI(比如摇杆位置),useLayoutEffect 可以在浏览器绘制前执行,避免视觉上的跳动。
  3. 避免在渲染函数中做繁重计算: 确保你的硬件状态解析逻辑非常快。

3. 设备丢失

用户拔掉了 USB 线,或者重启了电脑。你的 inputdevice 变量变成了 null

解决方案: 监听 disconnect 事件。

device.addEventListener('disconnect', (event) => {
    console.log("设备断开连接", event.device);
    setDevice(null);
    // 重新连接逻辑...
});

4. MIDI 协议的复杂性

MIDI 1.0 的协议很古老。一个简单的 Note On 其实包含很多细节:Channel(通道),Note Number(音符),Velocity(力度)。

  • Status Byte: 0x9x (Note On), 0x8x (Note Off), 0xBx (Control Change)。
  • Data Bytes: 1 和 2。

如果你在处理 MIDI 数据时,不小心把 Status Byte 当成了 Note Number,你的程序就会崩溃或者发出奇怪的噪音。

最佳实践: 写一个简单的 MIDI 解析器工具函数。

const parseMidiMessage = (data) => {
    const status = data[0];
    const command = status >> 4; // 高 4 位是命令
    const channel = status & 0x0F; // 低 4 位是通道

    if (command === 0x9) { // Note On
        return {
            type: 'noteOn',
            channel,
            note: data[1],
            velocity: data[2]
        };
    } else if (command === 0x8) { // Note Off
        return {
            type: 'noteOff',
            channel,
            note: data[1],
            velocity: data[2]
        };
    } else if (command === 0xB) { // Control Change
        return {
            type: 'controlChange',
            channel,
            controlNumber: data[1],
            value: data[2]
        };
    }
    return null;
};

第七部分:Web HID 的“二进制噩梦”

Web MIDI 比较规范,但 Web HID 是任意的。每个 USB 设备的“报告描述符”都不一样。

  • Report Descriptor: 这是设备出厂时写死的一张说明书,告诉电脑“我的数据包长什么样”。
  • Input Report: 设备发给电脑的实际数据。

如果你要做一个通用的 HID 驱动,你必须解析 Report Descriptor。这对普通开发者来说太难了。

但是! 对于大多数消费级设备(键盘、手柄、无人机),我们可以使用一些库来解析,或者直接通过经验猜测。

例如,一个标准的 Xbox 手柄的输入报告通常是这样的:

  • Byte 0: Buttons (A, B, X, Y, LB, RB, Select, Start, Stick L, Stick R)
  • Byte 1: Buttons (D-Pad Up, Down, Left, Right)
  • Byte 2: Left Stick X
  • Byte 3: Left Stick Y

React 中的处理:

// 这是一个非常简化的解析器,实际需要根据设备文档
const parseXboxReport = (data) => {
    const buttons = {
        A: !!(data[0] & 0x01),
        B: !!(data[0] & 0x02),
        X: !!(data[0] & 0x04),
        Y: !!(data[0] & 0x08),
        LB: !!(data[0] & 0x10),
        RB: !!(data[0] & 0x20),
        Start: !!(data[0] & 0x40),
        Back: !!(data[0] & 0x80),
    };

    const leftStickX = (data[2] - 127) * 2; // 中心点归一化
    const leftStickY = (data[3] - 127) * 2;

    return { buttons, leftStickX, leftStickY };
};

记住,Web HID 的核心在于解析。你收到的只是一堆 Uint8Array,你需要把它们翻译成人话。


第八部分:高级模式——状态同步与异步

在实际应用中,你可能会遇到这样的情况:React 的状态更新是异步的,但硬件事件的触发是实时的。

例如,你有一个 LED 灯,你想让它根据 React 的 brightness 状态来改变亮度。

useEffect(() => {
  const interval = setInterval(() => {
      // 这里我们有一个定时器,每 50ms 检查一次状态
      // 在生产环境中,你应该使用一个状态订阅库(如 Recoil, Redux)或者 RxJS
      const brightness = myGlobalState.brightness;
      midiOutput.send([0xB0, 0x07, brightness]); // CC7: Main Volume
  }, 50);

  return () => clearInterval(interval);
}, [midiOutput]);

更好的方法:响应式流。

虽然我们今天主要讲原生 React,但如果你处理非常复杂的硬件交互,建议引入 RxJS (Reactive Extensions for JavaScript)

RxJS 可以让你把硬件事件变成一个流(Observable),然后使用 map, filter, debounceTime 等操作符来处理数据,最后再更新 React 的 State。

import { fromEvent } from 'rxjs';
import { map, throttleTime } from 'rxjs/operators';

// 将 MIDI 事件转为 RxJS 流
const midiStream$ = fromEvent(midiInput, 'midimessage');

// 管道:只取 Note On,并且每 100ms 取一次(防抖)
midiStream$.pipe(
  map(e => parseMidiMessage(e.data)),
  filter(msg => msg && msg.type === 'noteOn'),
  throttleTime(100)
).subscribe(msg => {
    // 这里是纯粹的逻辑处理,不需要管 React 的渲染
    updateSequencerState(msg.note);
});

第九部分:总结与展望

好了,各位同学。今天我们走得很远。

我们从 React 的声明式 UI 出发,跨越了代码的边界,来到了物理世界。我们学会了如何使用 navigator.requestMIDIAccess 拿到 MIDI 的钥匙,如何使用 navigator.hid.requestDevice 拿到 HID 的网线。

我们构建了一个架构:

  1. 底层: 事件监听器,将原始二进制数据解析成结构化对象。
  2. 中间层: 状态管理,将硬件数据映射到 React State。
  3. 顶层: 声明式组件,根据 State 自动渲染 UI。

核心思想回顾:

  • 不要直接在 Render 中操作硬件。 保持 UI 的纯粹。
  • 使用 Context 或 Store 作为桥梁。 不要让组件之间互相传递硬件引用。
  • 善用 useEffect 它是连接 React 世界和硬件世界的桥梁。
  • 处理异步和权限。 这不是小事,这是用户交互的第一步。

未来的展望:
随着 WebAssembly 的普及,我们可以把更复杂的硬件驱动逻辑放在 Wasm 里运行,提高性能。随着 Web Bluetooth 的进一步发展,我们甚至可以控制智能家居。

现在的浏览器,已经不仅仅是一个文档阅读器了。它是一个操作系统。而 React,正是连接这个操作系统与物理世界的最优雅的 UI 框架。

最后,给各位的建议:
去买一个 MIDI 键盘(几十块钱的都行),去弄一个 USB 游戏手柄。不要只盯着屏幕。去触摸,去感知。当你看到 React 的 UI 因为你的一个按键动作而实时跳动时,那种感觉,比写一个 console.log('Hello World') 要爽上一万倍。

好了,下课!记得把你的 USB 线插好,去写代码吧!


(附录:完整示例代码结构参考)

为了方便大家直接上手,这里给出一个简化版的完整项目结构:

src/
├── components/
│   ├── HardwareVisualizer.tsx  // 可视化硬件状态的组件
│   └── ControlPanel.tsx       // 控制面板
├── contexts/
│   └── HardwareContext.tsx    // 全局硬件状态 Context
├── hooks/
│   ├── useMidiConnection.ts   // MIDI 连接 Hook
│   ├── useHidConnection.ts    // HID 连接 Hook
│   └── useHardwareState.ts    // 状态映射 Hook
├── types/
│   └── index.ts               // 类型定义
└── App.tsx

祝大家编程愉快,硬件常亮!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注