非 React 界面开发

# 非 React 界面开发

秋之盒插件的 App 和 Card 支持 自定义 DOM 挂载,不依赖 React。适用于需要直接操作 DOM 的场景:Canvas 渲染、WebGL、集成非 React 库、或追求极致性能的场景。

# 两种渲染模式

AutumnAppAutumnCard 都支持两种互斥的渲染模式:

模式 字段 说明
React component: React.FC<AppProps> 大多数场景,使用 React + Ant Design
自定义 DOM mount: (container: HTMLElement) => () => void 直接操作 DOM,不依赖 React

提供 componentmount,二选一。两者都提供时使用 component

# 基本用法

import type { AutumnApp } from '@autumnbox/sdk';

export const CanvasApp: AutumnApp = {
  id: 'canvas-demo',
  name: 'app.name.canvas_demo',
  icon: 'data:image/svg+xml,...',
  mount: (container: HTMLElement) => {
    // container 是一个空的 DOM 容器
    const canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 600;
    canvas.style.width = '100%';
    canvas.style.height = '100%';
    container.appendChild(canvas);

    const ctx = canvas.getContext('2d')!;
    ctx.fillStyle = '#6366f1';
    ctx.fillRect(0, 0, 800, 600);
    ctx.fillStyle = 'white';
    ctx.font = '32px system-ui';
    ctx.fillText('Hello from Canvas!', 50, 300);

    // 返回清理函数
    return () => {
      canvas.remove();
    };
  },
};

关键点:

  • container 是宿主提供的空 <div>,你可以在里面放任何 DOM 元素
  • 必须返回一个清理函数 () => void,在 Tab 关闭或组件卸载时调用
  • 清理函数负责移除 DOM 元素、取消定时器、断开连接等

# 实际示例:xterm 终端

xterm.js 等库不依赖 React,用 mount 模式最自然:

import type { AutumnApp } from '@autumnbox/sdk';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';

export const TerminalApp: AutumnApp = {
  id: 'terminal',
  name: 'app.name.terminal',
  icon: 'data:image/svg+xml,...',
  shallSelectAdbDevice: true,
  mount: (container: HTMLElement) => {
    const term = new Terminal({
      theme: { background: '#1e1e2e' },
      fontSize: 14,
    });
    const fitAddon = new FitAddon();
    term.loadAddon(fitAddon);
    term.open(container);
    fitAddon.fit();

    // 写入欢迎信息
    term.writeln('Terminal ready.');

    // 响应容器尺寸变化
    const observer = new ResizeObserver(() => fitAddon.fit());
    observer.observe(container);

    return () => {
      observer.disconnect();
      term.dispose();
    };
  },
};

# 访问插件服务

mount 模式下无法使用 React Hooks,需要通过全局方式获取 Service:

import type { AutumnApp, PluginContext } from '@autumnbox/sdk';

let ctx: PluginContext;

// main.ts 中保存 context
export function main(context: PluginContext): void {
  ctx = context;
}

export const MyDomApp: AutumnApp = {
  id: 'dom-app',
  name: 'app.name.dom_app',
  icon: 'data:image/svg+xml,...',
  mount: (container: HTMLElement) => {
    // 通过 context 获取服务
    const shell = ctx.getService(ShellService);
    const title = ctx.t('app.name.dom_app');

    const heading = document.createElement('h2');
    heading.textContent = title.value;
    container.appendChild(heading);

    // 订阅翻译变化
    const unsub = title.subscribe((newTitle) => {
      heading.textContent = newTitle;
    });

    return () => {
      unsub();
      heading.remove();
    };
  },
};

如果你需要响应式地访问服务,context.t(key) 返回 IReadonlyState<string>,可以用 .subscribe() 订阅变化。其他 Service 的响应式状态也同理。

# Card 的 mount 模式

Card 同样支持 mount

import type { AutumnCard } from '@autumnbox/sdk';

export const StatusCard: AutumnCard = {
  id: 'status',
  name: 'card.name.status',
  order: 10,
  mount: (container: HTMLElement) => {
    container.innerHTML = `
      <div style="padding: 12px;">
        <div style="font-weight: 600;">设备状态</div>
        <div style="color: #888; font-size: 13px;">运行中</div>
      </div>
    `;

    return () => {
      container.innerHTML = '';
    };
  },
};

# 何时选择 mount 模式

场景 推荐
标准表单/列表/详情 UI component(React)
Canvas/WebGL 渲染 mount
集成 xterm.js 等非 React 库 mount
集成 Monaco Editor mount
需要极致 DOM 性能控制 mount
需要使用 antd 组件 component(React)

mount 模式无法使用 antd 组件和 React Hooks。如果需要混合使用 React 和自定义 DOM,推荐用 component 模式 + useRef 持有 DOM 引用。

# 混合模式:React 中嵌入自定义 DOM

如果你主要用 React,但局部需要直接操作 DOM(如 Canvas),推荐在 component 中使用 useRef

import React, { useRef, useEffect } from 'react';
import type { AutumnApp } from '@autumnbox/sdk';

const CanvasAppView: React.FC = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d')!;
    // 绑定操作...

    return () => {
      // 清理
    };
  }, []);

  return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />;
};

export const CanvasApp: AutumnApp = {
  id: 'canvas',
  name: 'app.name.canvas',
  icon: 'data:image/svg+xml,...',
  component: CanvasAppView,
};

这是最灵活的方式:享受 React 的生命周期管理,同时有完全的 DOM 控制权。