# 最佳实践
基于 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)守卫——不要假设设备始终可用 loading和error分开管理,不要合并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} />
# 错误处理原则
- 永远不要吞掉错误——至少
console.warn - 清理操作用
.catch(() => {})——best-effort,不要因为清理失败而影响正常流程 - 给用户有意义的错误信息——不要直接显示堆栈
// 业务操作:完整错误处理
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(() => {});
# 下一步
- App 详解 — AutumnApp 完整用法
- Service 系统 — 创建自定义 Service
- 快速开始 — 创建你的第一个插件