最佳实践

# 最佳实践

基于 AutumnBox 官方插件(main-plugin、plugin-store、serina 等)总结的开发模式和经验。

# 设备连接状态处理

插件 App 经常需要操作 ADB 设备。设备可能随时断开,你的 UI 需要优雅处理这种情况。

# 推荐模式

import { useRequiredDevice } from '@autumnbox/sdk/hooks';
import { useService } from '@autumnbox/sdk/hooks';
import { ShellService } from '@autumnbox/sdk/services';
import { Empty, Spin, Alert } from 'antd';

const DeviceApp: React.FC = () => {
  const device = useRequiredDevice();  // 设备断开时返回 null
  const shell = useService(ShellService);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string>();

  // 设备未连接 → 显示空状态
  if (!device) {
    return <Empty description="请先连接设备" />;
  }

  const runCommand = async () => {
    setLoading(true);
    setError(undefined);
    try {
      const output = await shell.exec(device, 'getprop ro.build.version.release');
      // 使用 output...
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      {error && <Alert type="error" message={error} closable />}
      <Button loading={loading} onClick={runCommand}>获取版本</Button>
    </div>
  );
};

关键点:

  • 总是用 if (!device) 守卫——不要假设设备始终可用
  • loadingerror 分开管理,不要合并
  • try/finally 确保 loading 状态一定被重置

# Shell 进程清理

长时间运行的 Shell 进程(如 logcat)需要在组件卸载时正确清理:

const LogcatViewer: React.FC = () => {
  const device = useRequiredDevice();
  const shell = useService(ShellService);
  const procRef = useRef<IAdbShellProcess | null>(null);
  const disposedRef = useRef(false);

  const killProcess = useCallback(() => {
    if (procRef.current) {
      procRef.current.close().catch(() => {});  // best-effort
    }
    procRef.current = null;
  }, []);

  // 卸载时标记 disposed 并清理进程
  useEffect(() => {
    return () => {
      disposedRef.current = true;
      killProcess();
    };
  }, [killProcess]);

  const startLogcat = useCallback(async () => {
    if (!device) return;
    killProcess();  // 先杀掉旧进程

    const proc = await shell.spawn(device, ['logcat']);
    if (disposedRef.current) {
      proc.close().catch(() => {});
      return;
    }
    procRef.current = proc;
    // 读取输出...
  }, [device, shell, killProcess]);

  // ...
};

关键点:

  • useRef 持有进程引用(不是 state,因为不影响渲染)
  • disposedRef 防止组件卸载后仍设置状态
  • close().catch(() => {}) — 最佳努力清理,不抛异常

# 响应式状态消费

Service 暴露 IReadonlyState<T> 属性。在 React 中用 useServiceState,在非 React 中用 subscribe

# React 中

const MyComponent: React.FC = () => {
  const devices = useService(DevicesService);
  // 自动订阅,设备列表变化时重渲染
  const deviceList = useServiceState(devices.devices);

  return <div>{deviceList.length} 台设备</div>;
};

# 非 React 中

export function main(context: PluginContext): () => void {
  const devices = context.getService(DevicesService);
  const unsub = devices.devices.subscribe((list) => {
    console.log(`设备数变化: ${list.length}`);
  });

  return () => unsub();  // 清理订阅
}

subscribe 返回的 unsubscribe 函数必须在清理时调用,否则会内存泄漏。

# 数据持久化

# 简单配置

import { usePluginFs } from '@autumnbox/sdk/hooks';

const useConfig = <T extends object>(defaultValue: T) => {
  const fs = usePluginFs();
  const [config, setConfig] = useState<T>(defaultValue);

  useEffect(() => {
    // 加载
    fs.exists('config.json').then(async (exists) => {
      if (exists) {
        const stream = await fs.readFile('config.json');
        const text = await new Response(stream).text();
        setConfig(JSON.parse(text));
      }
    });
  }, []);

  const save = useCallback(async (newConfig: T) => {
    setConfig(newConfig);
    await fs.writeFile('config.json', new Blob([JSON.stringify(newConfig, null, 2)]));
  }, [fs]);

  return [config, save] as const;
};

# 复杂数据(使用 LocalFileSystemService)

const localFs = useService(LocalFileSystemService);
const configPath = `${pluginHome}/sessions.json`;

// 读取
const sessions = await localFs.readJSONOrNull<Session[]>(configPath);

// 写入(带缩进,方便调试)
await localFs.writeJSON(configPath, sessions, 2);

# PluginContext 共享模式

在非 React 代码中(Service、工具函数),无法使用 Hooks。推荐在 main.ts 中保存 context 引用:

// src/pluginContext.ts
import type { PluginContext } from '@autumnbox/sdk';

let _ctx: PluginContext | null = null;

export function setPluginContext(ctx: PluginContext): void {
  _ctx = ctx;
}

export function getPluginContext(): PluginContext {
  if (!_ctx) throw new Error('PluginContext not initialised');
  return _ctx;
}
// src/main.ts
import { setPluginContext } from './pluginContext';

export function main(context: PluginContext): void {
  setPluginContext(context);
}
// src/services/MyService.ts — 在初始化阶段使用
import { getPluginContext } from '../pluginContext';

export class MyService {
  constructor() {
    const ctx = getPluginContext();
    // 使用 ctx.pluginHome, ctx.fs 等
  }
}

只在初始化阶段使用这个模式。不要在渲染回调中调用 getPluginContext()——React 组件应该通过 useService 获取 Service。

# 性能优化

# useCallback + useMemo

事件处理器和衍生数据用 useCallback / useMemo 包裹,避免不必要的重渲染:

const handleChange = useCallback((key: string, value: string) => {
  setProps(prev => prev.map(p => p.key === key ? { ...p, newValue: value } : p));
}, []);

const changedProps = useMemo(
  () => props.filter(p => p.newValue !== ''),
  [props]
);

# 避免在 render 中创建对象

// 坏:每次渲染创建新对象,导致子组件重渲染
<Component style={{ padding: 16 }} />

// 好:提取到组件外
const containerStyle = { padding: 16 };
<Component style={containerStyle} />

# 错误处理原则

  1. 永远不要吞掉错误——至少 console.warn
  2. 清理操作用 .catch(() => {})——best-effort,不要因为清理失败而影响正常流程
  3. 给用户有意义的错误信息——不要直接显示堆栈
// 业务操作:完整错误处理
try {
  await shell.exec(device, command);
} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  notify.push('error', '命令执行失败', message);
}

// 清理操作:best-effort
proc.close().catch(() => {});

# 下一步