# 资源系统
插件有两种文件访问机制:打包资源(只读,来自 .atmb 包内)和插件文件系统(可读写,宿主本地存储)。本文完整讲解资源的放置、语法糖、加载机制和使用方式。
# 概览
| 机制 | 来源 | 读写 | 用途 | 获取方式 |
|---|---|---|---|---|
| 打包资源 | .atmb 内的 resources/ | 只读 | 图标、语言文件、模板、静态资源 | context.getResource() / usePluginResource() / PluginService |
| 插件文件系统 | {autumnHome}/plugins/data/{pluginPackageName}/ | 读写 | 用户配置、缓存、持久化状态 | context.fs / usePluginFs() |
简单记忆:打包资源是随身行李,文件系统是长期住所。
# 资源放置
将需要随插件分发的文件放在项目根目录的 resources/ 下:
my-plugin/
├── resources/
│ ├── icon.png ← 插件图标(128×128 PNG,透明背景)
│ ├── lang/ ← 国际化语言文件(自动加载,有语法糖)
│ │ ├── zh-CN.json
│ │ └── en-US.json
│ ├── templates/ ← 自定义资源(模板、配置等)
│ │ └── report.html
│ └── bin/ ← 嵌入的二进制文件
│ └── helper.sh
├── src/
└── package.json
构建时 autumnbox-sdk package 会将整个 resources/ 目录打包进 .atmb ZIP。路径结构原样保留。
放置规则:
resources/icon.png— 插件图标,用于插件列表、设置页、插件商店resources/lang/*.json— 特殊目录,宿主自动扫描注册为翻译内容- 其他子目录随意组织,运行时通过路径访问
# 语言文件与语法糖
resources/lang/ 是唯一有特殊处理的目录。
# 基本格式
文件名即 locale 代码(BCP 47),内容为 key-value JSON:
// resources/lang/zh-CN.json
{
"app.name.my-tool": "我的工具",
"card.name.status": "状态",
"settings.theme": "主题设置"
}
# <name> / <description> / <author> 语法糖
语言文件中有三个特殊占位 key。autumnbox-sdk package 打包时自动替换为带命名空间的 key:
// 开发者在 resources/lang/zh-CN.json 中写的
{
"<name>": "我的工具箱",
"<description>": "一个强大的 Android 设备工具集",
"<author>": "张三",
"app.name.toolkit": "工具箱"
}
打包后,.atmb 内的实际 JSON 变为:
// .atmb 包内 resources/lang/zh-CN.json 的实际内容
{
"plugin.name.@myplugins/toolkit": "我的工具箱",
"plugin.description.@myplugins/toolkit": "一个强大的 Android 设备工具集",
"plugin.author.@myplugins/toolkit": "张三",
"app.name.toolkit": "工具箱"
}
| 开发者写的 | 打包后变成 | 用途 |
|---|---|---|
"<name>" | "plugin.name.{pluginPackageName}" | 插件显示名称 |
"<description>" | "plugin.description.{pluginPackageName}" | 插件描述 |
"<author>" | "plugin.author.{pluginPackageName}" | 插件作者 |
为什么需要 namespace?
多个插件的翻译内容共享同一个 LanguageService。如果所有插件都直接用 "name" 作 key 会互相覆盖。语法糖在编译时自动加前缀,开发者写起来简洁,运行时又不冲突。
只有 <name>、<description>、<author> 这三个 key 会被替换,其他 key 原样保留。<name> 至少在一个语言文件中必须存在,否则构建会报错。
# 自动加载流程
宿主加载 .atmb 时自动处理语言文件,不需要手动注册:
加载 .atmb
→ 扫描 resources/lang/*.json
→ 解析文件名得到 locale(zh-CN.json → zh-CN)
→ 读取 JSON 内容
→ 注册到 LanguageService.load(pluginPackageName, locale, content)
- 文件名格式:
{locale}.json(BCP 47 格式,如zh-CN、en-US、ja-JP) - 不支持嵌套子目录(
lang/sub/xx.json会被忽略) - 语言文件在
pluginMain调用之前加载完毕,入口函数中即可使用context.t()
详见 国际化。
# 使用打包资源
宿主加载 .atmb 时,将 resources/ 下所有文件解压为 Map<path, Blob>。以下是三种访问方式:
# 方式一:context.getResource()
在 main.ts、Service 或任何非 React 环境中使用:
import type { PluginContext } from '@autumnbox/sdk';
export function main(context: PluginContext): void {
// 加载单个资源,返回 Blob 或 null
const template = await context.getResource('templates/report.html');
if (template) {
const html = await template.text();
console.log('模板内容:', html);
}
// 加载图片
const icon = await context.getResource('icon.png');
if (icon) {
const url = URL.createObjectURL(icon);
// 使用 url...
}
}
路径规则:参数是相对于 resources/ 的路径,不含 resources/ 前缀。例如 resources/lang/zh-CN.json 对应 lang/zh-CN.json。
# 方式二:usePluginResource Hook
在 React 组件中使用,提供响应式的加载状态:
import { usePluginResource } from '@autumnbox/sdk/hooks';
const IconDisplay: React.FC = () => {
const { data, loading, error } = usePluginResource('icon.png');
if (loading) return <Spin size="small" />;
if (error) return <Alert type="error" message={error.message} />;
if (!data) return <Empty description="图标不存在" />;
const url = URL.createObjectURL(data);
return <img src={url} alt="图标" style={{ width: 64, height: 64 }} />;
};
返回值:
| 字段 | 类型 | 说明 |
|---|---|---|
data | Blob \| null | 资源内容,不存在时为 null |
loading | boolean | 是否正在加载 |
error | Error \| null | 加载错误 |
usePluginResource 内部管理加载状态和清理。组件卸载时自动取消未完成的加载。
# 方式三:PluginService 资源方法
通过 IoC 容器获取 PluginService,它提供了更底层的资源访问 API。适合跨插件访问资源或需要批量操作的场景:
import { useService } from '@autumnbox/sdk/hooks';
import { PluginService } from '@autumnbox/sdk/hooks';
const MyComponent: React.FC = () => {
const pluginService = useService(PluginService);
// 获取原始 Blob
const blob = pluginService.getPluginResource('autumnbox.my-plugin', 'icon.png');
// 获取文本内容
const html = await pluginService.getPluginResourceAsText(
'autumnbox.my-plugin',
'templates/report.html'
);
// 获取 Data URI(自动推断 MIME 类型,适合 <img src>)
const dataUri = await pluginService.getPluginResourceAsSource(
'autumnbox.my-plugin',
'images/logo.svg'
);
// → "data:image/svg+xml;base64,..."
// 列出目录下的资源
const entries = pluginService.listResource('autumnbox.my-plugin', 'templates');
// → ['report.html', 'email.html']
};
PluginService 资源方法一览:
| 方法 | 返回值 | 说明 |
|---|---|---|
getPluginResource(pkg, path) | Blob \| null | 获取原始 Blob(同步) |
getPluginResourceAsText(pkg, path) | Promise<string \| null> | 获取文本内容 |
getPluginResourceAsSource(pkg, path) | Promise<string \| null> | 获取 Data URI(自动推断 MIME) |
listResource(pkg, path) | string[] | 列出目录下的直接子项(同步) |
getPluginResourceAsSource 的 MIME 推断
JSZip 解压出的 Blob 默认没有 MIME 类型。getPluginResourceAsSource 会根据文件扩展名自动修正:.svg → image/svg+xml、.png → image/png、.webp → image/webp 等。这对 SVG 尤为重要——浏览器拒绝渲染没有正确 MIME 的 SVG data URI。
# 三种方式的选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| React 组件内加载当前插件资源 | usePluginResource() | 自动管理加载状态 |
| main.ts / Service 内加载资源 | context.getResource() | 最简洁 |
| 需要 Data URI(img src) | PluginService.getPluginResourceAsSource() | 自动处理 MIME |
| 跨插件读取其他插件的资源 | PluginService.getPluginResource() | 需要指定 pluginPackageName |
| 列出资源目录内容 | PluginService.listResource() | 唯一支持目录列表的 API |
# 资源的限制
- 只读:打包资源来自 ZIP 包,不能修改。需要持久化的数据应使用插件文件系统
- Blob 格式:所有资源以
Blob形式返回。需要文本用blob.text(),需要 ArrayBuffer 用blob.arrayBuffer() - 全量驻留内存:
.atmb解压后所有资源 Blob 保持在内存中直到插件卸载。大文件(>1MB)会增加内存占用
# 插件文件系统
每个插件拥有独立的可读写数据目录。
# 数据目录路径
{autumnHome}/plugins/data/{pluginPackageName}/
autumnHome由宿主提供(Web 端通常是 OPFS,桌面端是用户数据目录)- 目录在插件首次加载时自动创建
# IFileSystem 接口
interface IFileSystem {
autumnHome(): string;
readFile(path: string): Promise<ReadableStream<Uint8Array>>;
writeFile(path: string, data: Blob): Promise<void>;
stat(path: string): Promise<IFileStat>;
remove(path: string): Promise<void>;
exists(path: string): Promise<boolean>;
mkdir(path: string): Promise<void>;
readdir(path: string): Promise<string[]>;
resolve(...segments: string[]): string;
tmpdir(): string;
}
# context.fs(非 React)
import type { PluginContext } from '@autumnbox/sdk';
export function main(context: PluginContext): void {
const { fs, pluginHome } = context;
// 写入配置
const configPath = fs.resolve(pluginHome, 'config.json');
await fs.writeFile(configPath, new Blob([JSON.stringify({ theme: 'dark' })]));
// 读取配置
if (await fs.exists(configPath)) {
const stream = await fs.readFile(configPath);
const text = await new Response(stream).text();
const config = JSON.parse(text);
}
}
# usePluginFs Hook(React)
import { usePluginFs } from '@autumnbox/sdk/hooks';
const ConfigPanel: React.FC = () => {
const fs = usePluginFs();
const saveConfig = async (config: object) => {
await fs.writeFile('config.json', new Blob([JSON.stringify(config)]));
};
};
# LocalFileSystemService(便捷方法)
通过 IoC 容器获取,提供 JSON/文本/Blob 的高级读写封装:
import { useService } from '@autumnbox/sdk/hooks';
import { LocalFileSystemService } from '@autumnbox/sdk/services';
const MyComponent: React.FC = () => {
const localFs = useService(LocalFileSystemService);
// JSON 读写
const config = await localFs.readJSONOrNull<{ theme: string }>('/path/to/config.json');
await localFs.writeJSON('/path/to/config.json', { theme: 'dark' }, 2);
// 文本读取
const text = await localFs.readText('/path/to/notes.txt');
// Blob 读取
const blob = await localFs.readBlob('/path/to/image.png');
};
LocalFileSystemService 操作宿主的完整文件系统,不限定在插件目录内。
# 打包资源 vs 文件系统 选择指南
| 场景 | 使用 | 原因 |
|---|---|---|
| 插件图标 | getResource('icon.png') | 图标打包在 .atmb 中 |
| 语言文件 | 自动(resources/lang/) | 宿主自动处理 |
| HTML 模板 | getResource('templates/*.html') | 模板随插件分发 |
| 用户设置 | context.fs.writeFile() | 需要持久化的可变数据 |
| 缓存数据 | context.fs.writeFile() | 可变数据 |
| 嵌入二进制 | getResource('bin/tool') | 二进制随插件分发 |
| 导出报告 | context.fs.writeFile() | 运行时生成的数据 |
| 宿主全局配置 | LocalFileSystemService | 需要访问宿主文件系统 |
# 完整示例:带缓存的资源加载
结合打包资源和文件系统,实现"首次从包内加载,后续从缓存读取":
import { usePluginResource, usePluginFs } from '@autumnbox/sdk/hooks';
const DataLoader: React.FC = () => {
const { data: bundledData, loading } = usePluginResource('data/default-config.json');
const fs = usePluginFs();
const loadConfig = async () => {
// 先尝试从缓存读取
if (await fs.exists('cache/config.json')) {
const stream = await fs.readFile('cache/config.json');
return await new Response(stream).json();
}
// 缓存不存在,从打包资源读取并缓存
if (!bundledData) return null;
const config = await bundledData.json();
await fs.mkdir('cache');
await fs.writeFile('cache/config.json', new Blob([JSON.stringify(config)]));
return config;
};
if (loading) return <Spin />;
return <Button onClick={() => loadConfig().then(console.log)}>加载配置</Button>;
};