Appearance
快速开始
新项目可先运行 hb-sdk create my-miniapp 获得 Vanilla JavaScript + Vite HelloWorld;模板只演示 Toast,业务代码可以直接替换。
如果页面不需要小黑盒开放能力,可以不接入 SDK。SDK 导入时会自动启动握手,能力调用会自动等待;使用受保护能力前需在 package.json#heybox.permissions 声明对应权限,详见权限声明。
交互控件与握手状态
auth.login() 等可能展示授权 UI 的能力必须保留原始可信用户手势。不要在点击回调中等待握手;页面加载时读取并订阅持久握手状态,只在 status === 'ready' 时启用按钮:
ts
import hbSDK, { type MiniProgramSDKHandshakeState } from '@heybox/hb-sdk'
import { computed, onUnmounted, ref } from 'vue'
const handshakeState = ref<MiniProgramSDKHandshakeState>(hbSDK.getHandshakeState())
const stopHandshakeState = hbSDK.onHandshakeStateChange(state => {
handshakeState.value = state
})
const sdkReady = computed(() => handshakeState.value.status === 'ready')
onUnmounted(stopHandshakeState)把 sdkReady 用作按钮的 disabled 条件。订阅会立即回放当前状态,因此晚挂载组件也能得到 ready 或 failed 终态。ready 生命周期事件只在握手成功瞬间派发且不会重放,不能作为当前状态来源。
读取当前用户
声明 userInfo 后,可以读取按小程序隔离的 app_user_id 和已授权资料。该能力不依赖公开 network 权限:
ts
import hbSDK from '@heybox/hb-sdk'
async function getCurrentUser() {
const result = await hbSDK.user.getInfo()
return result.userInfo
}user.getInfo() 不展示授权弹窗,也不要求可信用户手势。未登录时返回未登录状态;已登录时隐式授予当前本地用户 scopes(identity、profile),静默返回 userInfo.app_user_id、profile.nickname 和 profile.avatar。平台为当前版本启用 Steam 账号能力时,还会授予 steam_account,已绑定用户返回可选 userInfo.steam_id。这条读取不返回授权码或 OpenAPI token;需要建立开发者服务端会话时使用 auth.login()。
获取服务端授权码
声明 userInfo 后,可通过 auth.login() 获取短期授权码。登录可能展示 UI,因此应绑定到按钮点击等明确的用户操作。平台为当前版本启用 Steam 账号能力后,联网小程序可在 scopes 中按需加入 steam_account,并由开发者服务端通过 OpenAPI 读取 steam_id。该调用内部使用 Host 受信请求;平台为当前版本启用 Steam 账号能力后,无网络的 local 模式调用会返回 SERVER_API_REQUIRED,兼容 legacy 模式保持既有行为;示例中的 network.request() 另需声明并获得 network 平台批准。
SDK 会自动完成握手。登录按钮应按上面的持久握手状态启用;点击回调直接调用 auth.login(),不要先等待其他异步任务,以免首次操作丢失可信手势。
ts
import hbSDK, { HbMiniProgramSDKError } from '@heybox/hb-sdk'
// 这是你自己的后端接口,不是黑盒 OpenAPI 地址。
async function loginFromUserAction() {
try {
const result = await hbSDK.auth.login({ scopes: ['profile'] })
await hbSDK.network.request({
url: 'https://api.example.com/session/heybox',
method: 'POST',
data: { code: result.code },
})
} catch (error) {
if (error instanceof HbMiniProgramSDKError && error.code === 'AUTHORIZATION_CANCELLED') {
return
}
throw error
}
}返回值与边界以用户身份与登录为准。页面只把 code 提交给自己的服务端。不要在页面初始化阶段自动调用 auth.login()。
文件与下载
文件能力的完整用法、沙箱模型、路径规则、配额与错误码见文件能力与沙箱;本节只给最短上手路径。
files.sandbox 是当前小程序隔离的逻辑根。file() / directory() 只创建 lazy proxy,不执行 I/O;第一次真实 I/O 仍由 Runtime 和 Host 校验当前 session、授权、mode、kind 和路径。
ts
import hbSDK, { HbMiniProgramSDKError } from '@heybox/hb-sdk'
const exportDirectory = hbSDK.files.sandbox.directory('exports')
await exportDirectory.create({ recursive: true })
const controller = new AbortController()
try {
const file = await hbSDK.network.download({
url: 'https://cdn.example.com/report.json',
to: exportDirectory,
suggestedName: 'report.json',
overwrite: true,
maxBytes: 8 * 1024 * 1024,
signal: controller.signal,
onProgress({ loaded, total, lengthComputable }) {
console.log(lengthComputable ? `${loaded}/${total}` : loaded)
},
})
console.log(await file.readText())
} catch (error) {
if (error instanceof HbMiniProgramSDKError && error.code === 'METHOD_FORBIDDEN') {
// 当前 Host 不支持 V1 文件能力。
return
}
throw error
}上传已有文件时使用 network.upload(),由 Host 直接传输可读 FileHandle;需要 network 与 filesystem 权限,并单独协商上传能力。raw/multipart 示例见上传受控文件。
当前平台状态如下:
| Host | files.sandbox | 外部 picker | files.saveFile() | network.download() |
|---|---|---|---|---|
| 新 PC | 支持 | 支持 | 支持 | 支持 |
| Android | 支持 | 支持 | 已适配客户端支持 | 支持 |
| iOS | 支持 | 支持 | 支持 | 支持 |
| OHOS | 按宿主协商 | 按宿主协商 | 按宿主协商 | 按宿主协商 |
| Web | 不支持 | 不支持 | 不支持 | 不支持 |
| Browser Dev Host | 不支持 | 不支持 | 不支持 | 不支持 |
当前 Runtime 认识但 Host 未实现时返回 METHOD_FORBIDDEN;旧 Runtime 收到新 method 时返回 METHOD_NOT_FOUND。新 PC 注入真实文件与流式下载 primitive;Android/iOS 1.3.396+ 还需在 启动时通过 system.describe 宣告对应 Native operations。缺失 handler、协商超时或 operation 不完整时对应能力保持关闭,且不提供 throwing stub、Blob 或内存假下载。
Mobile 的可用性完全由 Host 宣告的 operation 集决定,Runtime/H5 不按操作系统分支判断能力: Android 已适配客户端支持 files.saveFile() 和精确 SAF 文件的 readwrite 选择,仍需系统授予写权限;较早客户端可能拒绝。精确 SAF 文件的写入不保证原子替换,也不支持作为 network.download() 的原子替换目标,详见保存到用户指定位置。
路径与创建
- 逻辑相对路径统一使用
/。拒绝空字符串、绝对路径、空 segment、.、..、反斜杠、尾分隔符、NUL 和控制字符。 create()的recursive与exclusive默认均为false。默认行为是不截断内容的 ensure-exists;exclusive: true在同名实体已存在时失败。writeText()/writeBytes()可以在父目录存在时隐式创建目标,但不会自动创建父目录。exists()在祖先/目标缺失或 kind mismatch 时返回false;授权撤销、quota 与 I/O 错误仍抛出。file.remove()与directory.remove()只删除 sandbox 对象;外部 picker/saveFile 授权调用删除时抛出FILE_ACCESS_DENIED。- 非空目录默认不能删除并抛出
DIRECTORY_NOT_EMPTY;确认删除整个目录树后显式调用directory.remove({ recursive: true })。 directory.list()默认最多返回 1000 项,显式limit也不能超过 1000;超限抛出DIRECTORY_LIST_LIMIT_EXCEEDED。- Host 文件错误只暴露稳定 code 与 canonical 安全文案;原始 message、data、native path 和 credential 不会进入小程序。
FileStat.size是字节数;modifiedAt/createdAt若存在,单位为 Unix epoch milliseconds。
外部授权与保存
pickFiles()、pickDirectory() 和 saveFile() 必须直接从按钮点击等可信用户操作调用。picker 取消会抛 FILE_PICKER_CANCELLED,不会返回空数组或 undefined。
ts
async function saveFromUserAction(text: string) {
const file = await hbSDK.files.saveFile({ suggestedName: 'report.json' })
await file.writeText(text)
}saveFile() 请求 Host 返回精确 File 授权,不会同时创建或保留 Directory 授权。新 PC 当前先让 用户选择保存目录,再以 suggestedName 独占创建空文件;同名文件抛出 FILE_ALREADY_EXISTS, 不会静默覆盖。V1 不接受 accept;扩展名过滤只属于 pickFiles({ accept: ['.json'] })。外部授权 仅在当前 Runtime session 有效。
pickFiles() / pickDirectory() 默认返回 mode: 'read' 的授权;只有显式传入 multiple: true 时,pickFiles() 才允许 Host 返回多个文件。需要修改文件、在所选目录中创建 文件,或把外部授权用作下载目标时,必须在 picker options 中显式请求 mode: 'readwrite'。
单个 Runtime session 最多保留 1024 个 picker/saveFile direct handle;整批选择超过剩余额度时抛出 FILE_QUOTA_EXCEEDED,不会保留部分授权。Sandbox 派生项与目录枚举结果使用逻辑引用,不持续占用该额度。
下载边界
- 下载固定使用 GET,不接受 method、body、上传或 Range 字段。
- File target 必须已存在且为
readwrite,成功后返回同一个 File proxy;Directory target 必须存在且为readwrite,成功后返回新的子 File proxy。 - Directory 默认不覆盖同名文件;只有显式
overwrite: true才允许原子替换。 - Directory target 的子文件名按安全的
Content-Disposition filename*/filename、suggestedName、URL basename、download.bin依次选择。 timeout是网络空闲超时,不是整个下载总时长。取消或失败不会改动原目标。AbortSignal只发出取消意图并等待 Host 终态;如果 Host 已先完成原子提交,Promise 仍以成功结果 resolve,否则抛出DOWNLOAD_CANCELLED。- 公开 request 与 download 都不跟随重定向;3xx 或 partial representation 会失败。
- 进度从
loaded: 0开始并单调不减;原子提交后、Promise resolve 前会再发送一次 final progress,因此空文件或 Host 已报告最终值时允许相邻值相同。只有无歧义、identity-encoded 的Content-Length才使lengthComputable=true;压缩、chunked 或重复长度都视为未知 total,可信长度必须在提交前与实际字节数一致。 onProgress在本地同步执行;回调异常不会中断下载。成功时最后一次回调发生在文件提交后、Promise resolve 前。
默认单例
大多数小程序页面都应该使用默认实例。0.6 起不再对业务代码提供独立实例工厂。