开发文档

Docklet 是一个 Capacitor + React 的 Android 应用坞;目录应用以独立 origin 运行在壳的 iframe 里。本文覆盖技术栈、应用规范、桥接接口和开发注意事项。

技术栈

层技术
壳 UIReact 18 + TypeScript + Vite 5
原生容器Capacitor 6、Android WebView、Java 17
应用托管`AppHostClient` 拦截 `https://<id>.app.local/`,映射到 `filesDir/apps/<id>/`
原生插件`AppHostPlugin`(安装/文件/通知/外部打开)、`PhoneRelayPlugin`(对话中转)
状态存储壳:localStorage;应用:独立 origin 的 localStorage / IndexedDB / OPFS;文件:`data/<id>/`

壳本身不加载远程代码,所有目录应用都来自本地包(zip 导入或随 APK 分发)。

运行模型

  • 壳页面地址是 https://localhost/,由 Capacitor 的本地 WebView 提供。
  • 每个应用有独立 origin:https://<id>.app.local/,由原生 shouldInterceptRequest 拦截。
  • 应用放在 iframe 中,sandbox="allow-scripts allow-same-origin allow-forms"。
  • 独立 origin 的好处:localStorage / IndexedDB / OPFS 天然隔离,配额独立,卸载可整块清理。
  • 壳负责返回栈、键盘 insets、安全区和外部链接;应用不直接改 WebView。

目录结构

apps/<id>/                源码(只读),导入/更新时整体替换
  game.json | app.json    应用清单
  index.html | app.js     入口
android/.../assets/apps/   构建时同步进 APK
data/<id>/                应用数据(可写),更新保留、卸载删除
apps/<id>/.docklet/state.json  壳写的安装记录(应用不要依赖)

数据目录只能通过 fs 能力访问,路径必须落在自己的 data/<id>/ 下。

应用清单

包根目录必须有 game.json 或 app.json,缺失会被拒绝安装。

{
  "spec": 1,
  "id": "inventory",
  "name": "库存",
  "version": "1.0.0",
  "entry": "index.html",
  "icon": "📦",
  "description": "轻量的物品与数量记录",
  "author": "Docklet 官方",
  "updatedAt": "2026-11-06",
  "capabilities": ["fs", "notify"],
  "permissions": [
    { "name": "android.permission.POST_NOTIFICATIONS", "reason": "接收库存提醒" }
  ]
}
字段必填说明
id是^[a-z][a-z0-9-]{1,31}$,导入后不可变,决定 origin
name是显示名
version是语义化版本,用于「可更新」判断
entry是包内相对路径,必须是已存在的 .html / .js
icon否emoji 或图片路径
description否应用列表/长按菜单显示,单行省略
author / updatedAt否详情页展示
capabilities否只能使用下方枚举,未知值拒绝导入
permissions否每条必须有 reason,否则拒绝导入
导入会校验 zip 路径安全、入口存在、id 合法且不冲突、能力枚举、权限原因、体积上限;错误码包括 ZIP_UNSAFE、ENTRY_MISSING、MANIFEST_INVALID、TOO_LARGE、CAPABILITY_UNKNOWN、PERMISSION_NO_REASON。

能力与权限

能力(capabilities)

能力是壳注入的接口,不声明就没有;由壳在桥接层强制校验。

能力提供
fs读写自己的 data/<id>/
openExternal用系统浏览器打开 http/https 链接
clipboard写剪贴板
share系统分享
background后台监听/提醒
download把文件存到系统「下载」目录
notify发系统通知
storage应用私有键值存储(app.<id>.*)

权限(permissions)

Android 系统权限,声明后只在应用真正使用时申请,并且必须写清原因。安装页与详情页会把「声明」和「已生效」分开显示。

桥接接口

HTML 应用

应用通过 window.docklet 调用壳能力;未声明的能力会返回 CAPABILITY_DENIED。

方法说明
docklet.fs.read/write/list/delete只能操作自己的 data 目录
docklet.openExternal(url)只接受 http/https
docklet.download(url, filename?)存到系统下载目录
docklet.clipboard.write(text)写剪贴板
docklet.share(...)系统分享
docklet.notify(...)发通知(需通知权限)
docklet.shell.exit()关闭应用,回到来源页
docklet.ui.insets(){ top, bottom, ime },CSS px;键盘打开时 bottom 为 0
docklet.ui.onInsetsChange(fn)订阅 insets 变化,返回取消订阅

module 应用

入口是 .js 时按 module 加载,导出 mount(root, api),返回可选的 unmount。常用 api:version、manifest、storage.get/set/remove、audio.tone/noise/setMuted、score.get/add/best/finish/reset、shell.exit/restart、watch.start/stop/status/test、on('pause'|'resume')、openExternal、download、clipboard.write、ui.insets/onInsetsChange。

后台监听限制:最多 4 个应用同时监听,间隔最小 2 秒,文本最长 80 字,endpoint 只接受 http/https;策略存原生,应用卸载后清掉。

网络与安全

  • 应用代码必须随包分发,入口是本地文件;不要在运行时从网络加载主逻辑。
  • iframe sandbox 禁止顶层导航和弹窗;打开外链必须走 openExternal。
  • 能力按清单注入,壳不提供任意原生调用;未声明的接口直接拒绝。
  • 应用的存储只属于它自己的 origin;壳不读应用 localStorage。
  • 应用卸载会删除代码、data/<id>/ 和该 origin 的 WebView 存储。

本地开发

npm install
npx vite --port 5173        # 浏览器预览壳
npx cap sync android        # 同步 Web 资源到 Android
node scripts/sync-apps.mjs  # 同步 apps/ 到原生 assets
powershell -ExecutionPolicy Bypass -File .\\build-apk.ps1

调试:Android WebView 可通过 chrome://inspect 连接(debug 包),原生日志用 adb logcat。目录应用在浏览器里可直接访问 /apps/<id>/,但 origin 与真机不同,涉及存储/能力的行为以真机为准。

构建与版本

  • 版本号规则:首个公开版 1.0.1;小改 1.0.2;大改 1.1.1。
  • versionCode 每次发布必须递增,否则不能覆盖安装。
  • 构建号(YYYYMMDD-HHmm)只用于内部识别,不混进版本名。
  • 签名文件 android/*.jks 与 keystore.properties 不进仓库;换签名后旧包不能覆盖安装。

开发注意事项

键盘

WebView 是覆盖式键盘,窗口不会缩小。不要自己改 viewport 或监听 resize 猜高度,用 docklet.ui.insets() / onInsetsChange;壳的全屏层统一用 --kb-inset 让位。

返回键

壳只有一套返回栈;应用内的面板/抽屉要自己先关,再调用 shell.exit(),不要用 history.back() 造成壳与页面状态不一致。

密码类输入

不要用 type="password":部分 ROM 会强制切安全键盘,无法截图、部分机型无法输字母。用 type="text" + -webkit-text-security: disc。

大字号

壳只放大自己界面。应用内请用相对单位(rem / em / %),并给容器留可伸缩空间,不要写死像素行高。

存储与数据

  • localStorage 有配额,别存大文件;大文件用 fs 写 data/<id>/。
  • 应用更新不会动 data 目录;卸载会全部删除,需要保留的数据要让用户先导出。
  • 不要把数据写进代码目录,更新时会被整体替换。

界面

  • iframe sandbox 下 alert/confirm 不保证可用,用应用内 UI。
  • 避免依赖 window.top / 父页面 DOM;只能通过桥接通信。
  • 首屏保持小:图片按需加载,避免大体积字体和未压缩资源。

发布流程

  1. 更新清单 version,跑 build-apk.ps1 生成 APK。
  2. APK 上传 GitHub Releases,tag 用版本号(如 v1.0.1)。
  3. 官网 site/ 由 Cloudflare Pages 托管,输出目录填 site。
  4. 应用列表页展示当前内置应用;新应用先随壳分发,后续再接在线安装。