Numable 创作文档同一份内容也在终端里:`numable docs <主题>`English

capabilities —— 能做什么、做不到什么、各平台差在哪

读者:做工具的用户,和替他干活的 AI。两者读同一份。 动手之前先扫一遍这章:下面写「不支持」的地方,不是 bug,是那个平台没有这条路——设计时别依赖它。下面说的平台是 iPhone / iPad / Mac · Android · 鸿蒙 · Windows 桌面版。 怎么做见 numable docs workflow。

组件

组件是包的主体:一份 .rcn 画法 + 一份 .df 取数,所有平台用同一份源渲成同一张位图。

能力 状态 对你意味着什么
RCN 渲染(几何 + 样式 + 活数据) 各平台一致 写一份 .rcn 所有平台通用,不按平台分支
尺寸档位 layout(两位网格码,十位=宽格、个位=高格) App 内各平台任意码;桌面能放的档位各平台不同(见下节) w = 90×列 − 22、h = 98×行 − 38。常用:22=158×158、21=158×60、42=338×158、44=338×354
相对布局 各平台一致 尺寸用 {parent.w} / {parent.h},写死 158 换一档 layout 就错位
明暗主题 各平台一致 主题是纯渲染参数,切换只重渲、不重新取数——所以颜色必须写 浅|深 双分支,不能靠取数分支(check G7)
「跟随系统」档(日落自动切) 各平台一致;Android 桌面小组件在 App 进程还活着时可能仍是旧主题 App 内一律正确,不需要你做任何事
组件实例参数 各平台一致 同一个组件可以在仪表盘上摆多份、各带各的参数(城市、股票代码);默认值写在 .xwidget.params
组件上的可交互控件 不支持 组件是一张位图。整个组件点击有(events.onClick),组件内按钮、输入框、滚动都没有——需要交互就开一张页面
动画 不支持 组件是静态一帧。用户点下去到页面打开约 250ms 无反馈,所以点击流第一步要写 ui.haptic(check G22)
换了包内容,组件会自动换新 各平台一致 App 按包的版本号分代缓存:manifest.version 一 +1,旧的渲染图与旧的落盘数据一起失效。所以改完包必须 +1 —— 不 +1 就是「改了没反应」,而且不报错
改包之后先白一下再出新内容 各平台一致 这是分代缓存的正常表现,不是 bug:新一代没有缓存图可贴,于是走骨架、当场真取数。别为了消掉这一下去复用旧图
取不到数据时 App 画什么 各平台一致 取数失败但有上次的数据:照旧渲上次的数据,App 不加标记(过期提示是你的时间锚的事)。失败且没有任何数据:App 用空数据渲你的 .rcn,再在底部叠一枚胶囊「加载失败 · 重试」,没网时是「网络不可用」。所以 .rcn 必须在空数据下渲得出来(numable render --states empty 那张),别在空态上自己再画一套错误提示
缺凭证、超出免费额度 各平台一致 声明了 required: true 凭证而用户没绑:不取数,叠「需要凭证」胶囊。免费版用户盘上超过 6 个组件时,第 7 个起不再更新,叠「已暂停更新」胶囊。这两种都由 App 画,包里不用处理
新版本删掉或改名了某个 .xwidget 各平台一致 组件的文件名就是它的身份。用户盘上引用旧文件名的组件会显示「组件已下线」,包更新时被清掉。要换组件,新增一个文件,别改名

桌面小组件

桌面上那个组件,和 App 内仪表盘里的是同一张 .xwidget,但刷新机制完全不同,这是平台之间差异最大的一处。

iPhone / iPad / Mac Android 鸿蒙 Windows
有没有桌面小组件 有 有 有 没有
桌面能放的档位 22 / 42 / 44 4×4 网格内 16 档(含 21 / 11) 8 档(11 21 22 32 33 42 44 46) —
小组件会不会自己取数 会(到期时自己跑取数 + 联网) 会 不会(组件进程只读 App 上次渲好的图) —
数据新鲜度上限 系统给的时间线预算,大致 15 分钟量级 系统心跳 只在用户打开主 App 并渲过仪表盘之后才更新 —
刷新频率 系统决定 系统决定 用户选三档(120 / 60 / 30 分钟),下限 30 —
明暗跟谁 跟 App 的有效明暗,不是跟系统 同 同 —
资源上限 小组件进程 30MB 硬上限,超了直接被杀且不报错 无此限制 只读图,不涉及 —
显示态取图(首帧、系统重绘、选择列表缩略图) 缓存优先,秒显 同 只有这一条路 —
刷新态取图(到期刷新) 跳过图片缓存直接取数,失败才回落上一张旧图 同 不适用 —
切主题 / 切语言 连带刷新桌面小组件 同 同 不适用
「到期时把主 App 叫醒去取数」 — — 系统不允许,不是没做 —

对你意味着什么:

仪表盘刷新

能力 状态 对你意味着什么
声明式节奏 refresh 各平台一致 interval 支持时间窗 "09:30-16:10@60"(收盘后不空转)与裸秒 "3600";at 指定时点;tz 指定时区
刷新下限 各平台一致 仪表盘按最早到期的那个组件醒来,两次真取数之间最短 3 秒,写得再小也按 3 秒走。免费版用户的周期刷新会被放慢到每 5 分钟,Pro 用户按你写的节奏走;手动更新、首次加载、切语言、widget.refresh 不受影响。这是 App 打开时仪表盘上的节奏;桌面小组件各平台另有下限(iOS 15 分钟、Android 1 分钟、鸿蒙只显示 App 上次画好的图)。写多快,看上游接口的限流扛不扛得住
下拉刷新 iPhone / iPad · Android · 鸿蒙 有;Mac 没有下拉 Mac 用户靠长按组件手动更新,别在页面里写「下拉刷新」这种引导文案
长按组件 →「立即更新」并给成败反馈 各平台一致 用户有确定的手动出口,你不需要在组件上自己画刷新按钮
「上次更新 / 上次失败 / 停了多久」 各平台一致 App 已经记账;你要做的是组件上有自己的时间锚,让用户一眼看出数据是什么时候的
widget.refresh 原语 各平台一致 在交互流里主动刷组件:self / widget / bundle 三档,默认 bundle;只能刷本包的组件;5 秒内重复调用直接返回成功且不动作
提醒(到点就发,内容布防时就定死) iPhone / iPad · Mac · Android 交给系统定时,App 没开着也照响;鸿蒙需要先拿到代理提醒权益,拿到之前降级成「打开 App 时补响」;Windows 版靠常驻进程,退出后不响 能写成「几点几分说这句话」的提醒就写成这种,它是四个平台里最可靠的一档
提醒(到点取数、判条件才知道响不响)与后台任务 都是尽力而为:执行机会 = 打开 App · 桌面小组件刷新 · 系统给的后台时间片;Android 上用户打开「实时监测」(常驻一条通知)后能到点即查;鸿蒙主要靠桌面组件那一拍,Windows 版靠常驻定时器 节律写 interval,免费版用户下限 30 分钟(写更小也会被抬到 30),Pro 用户按你写的节律;文案不许写「每天准时」「实时」,写「每天至多一次、有机会就补」。详见 numable docs alerts

页面

页面是点组件之后打开的东西,放在 page/ 下,由 router.json 登记。

页型 状态 对你意味着什么
html(随包分发的网页) 各平台一致 写 HTML/CSS/JS 最自由;但页面里 fetch / XHR 被容器焊死(注入了 connect-src 'none'),取数一律经桥调 .df,见 numable docs bridge
xpage(整页级 RCN,不用网页) 各平台一致 渲染最快、与组件同一套 DSL;叶子只有 Canvas 与 input,容器支持 list / grid / flex / waterfall 等布局
form(.xform 表单页) 各平台一致 收用户输入的标准做法,14 种字段组件
外链(非本包的 http(s) 页) 有差异:iPhone / iPad · Android · 鸿蒙 在容器内开一个带域名和锁标的单页壳;Mac 与 Windows 版交给系统浏览器 别把第三方站点当成你的包的一部分来设计导航,用户可能是在另一个浏览器里看它

页面的共同约束:

约束 检查方式 对你意味着什么
容器顶部有浮动胶囊(‹ / ••• / ✕),恒浮在内容之上 check G17(xpage)/ 人审(html) XPage 根写 "paddingTop": "${@contentInset.top}pt";H5 的 body 写 padding-top: calc(var(--xb-content-top) + 16px)。不让位就是顶部一截被压住,而且各平台都不报错
容器不画标题栏 — 页面标题写在 router.json 的 title,不要自己画一条假导航栏
内容恒定手机宽 人审 大屏上页面是居中的窄列,不要做响应式断点
浮层(sheet / dialog)以容器为边界,高度固定比例 — sheet 恒占容器高的 0.8、dialog 恒 0.6,不随内容伸缩;内容多了内部滚动,少了留白
H5 输入控件字号 ≥ 16px check G15 小于 16px 时 iOS 聚焦会把整页放大

容器与返回栈

事实 对你意味着什么
页面开在一个容器里,容器有自己的返回栈 从 A 开 B、从 B 开 C,一路压栈;‹ 逐层退回,✕ 一次关掉整个容器
容器顶部最多三个按钮(‹ / ••• / ✕),不画标题栏 标题写 router.json 的 title;自己再画一条导航栏 = 两条并排
‹ 只在栈里真有上一页时出现 首页上没有 ‹,别把「返回上一页」当成用户一定有的出口
Android / 鸿蒙的系统返回键与 ‹ 同义 不需要你接;也不要用页面里的 JS 去劫持它
从组件、深链、外部进来时可能没有栈 那种情况下呈现 page 的子页会先立一个容器再压栈,行为与栈内一致
外链(非本包的 http(s))进的是另一种壳:只有 ✕ 和一个域名,没有 ••• 别指望外链页还能用包里的导航;用户 ✕ 掉之后回到原来那页

XPage 能做什么

xpage 是不写网页的整页级 RCN。它与组件同一套 DSL,但多了交互与状态。

能力 对你意味着什么
八种容器布局 absolute stack flex flow list pager grid waterfall;grid / waterfall 用 columnCount 定列数
叶子只有两种 Canvas(一块 RCN 画布)与 input(裸输入框,零视觉,外观靠底下垫一块 Canvas)
页面状态 pageState 一页一份的键值表,节点用 ${键} 读;流里用 xpage.patchState 改
局部重画 xpage.redraw 只重画一个节点、不重跑取数;整页重画、软重载、硬重进各有一个 action
节点事件 onClick / longClick / onChange / onSubmit / onReachEnd 等,值是一条 .af
节点长按菜单 节点上写 menu 数组;同时写了 longClick 则菜单永远不弹
条件显隐 visible 判假的节点整个不进页面:不占位、也不跑它的取数

逐字段写法见 numable docs page,能用的 action 见 numable docs af。

交互

交互写成 .af 流,挂在组件的 events 或页面节点的 events 上。能用什么 action,取决于载体:

载体 判据 能用 不能用
事件流 用户手指参与(点组件、点节点、深链) UI 原语、导航、表单、写回、刷新、data.* —
渲染流(depends 里的取数) 取反 请求、解析、data.* 一切 UI 与导航;widget.updateParams / widget.refresh 会被硬拒

事件流有 15 秒超时,等用户的那几个 action(确认框、开页面、跳外部 App)会暂停计时。

常用原语:

能力 状态 对你意味着什么
整组件点击 events.onClick 各平台一致 值是字符串 → 当导航串走;是结构体(@[file://…af] 或内联)→ 当交互流走。整组件 onClick 的流改不了自己的参数
参数编辑 events.onEdit 各平台一致 两种写法:跳一张页面(html / xpage / form)让用户填,或直接挂一条 .af。目标必须真的具备写回能力,否则 check G12d / G12e 拦下
widget.updateParams 写回参数 各平台一致 逐键合并,值为 null 表示删键回落默认;只接受标量,给对象/数组直接失败
startPageForResult / singleValue 各平台一致 开一张子页(page / sheet / dialog)拿一个返回值,返回 {value, cancelled}。startPageForResult 不写 container 时缺省 sheet;singleValue 要显式写
ui.toast / ui.alert / ui.confirm / ui.haptic / ui.showLoading 各平台一致(仅事件流) 别在页面里用 window.alert / window.confirm,它们会逃出容器且各平台不一致
节点长按菜单 各平台一致 XPage 节点写 menu 数组;节点同时有 longClick 时菜单不弹
ui.presentSheet / nav.openForResult 各平台一致(仅事件流) startPageForResult 的两个底层原语,返回的是裸结果或 null。新写的流用 startPageForResult 就够了
input.focus / blur / clear / selectAll / setValue 各平台一致(仅 xpage) 用流去操作 input 节点(按节点 id);组件事件流里调是空转
xpage.patchState / setState / redraw / redrawPage / reloadPage / reenterPage 各平台一致(仅 xpage) 改页面数据 + 决定重画多少。日常只用 patchState + redraw;setState 是整份替换,没写进来的键会被清掉
installBundle(提议安装另一个包) 各平台一致 只弹安装面板,装不装是用户的动作,流拿不到结果。别在它之后提示「已安装」
page.close / page.setResult 各平台一致 关掉本页 / 带值关掉本页。setResult 自己会关页,不用再补 page.close
widget.pick(打开「加到仪表盘」面板) iPhone / iPad / Mac · Android · Windows 版可用;鸿蒙从组件的事件流里调恒失败 添加组件入口放在页面里,而不是组件上;一个按钮就够,别手抄一份组件目录(check G24 / G27)
跳外部 App(nav.open 第三方 scheme) 各平台一致 第一次跳会弹确认框,用户点头后按包记住;自动链路(取数、定时刷新)发起的跳转一律静默拒绝,必须是用户点出来的
目标 App 没装时回落网页(nav.open 的 fallback) iPhone / iPad / Mac · Android · Windows 版可用;鸿蒙上不触发(系统自己弹提示) fallback 只认 http(s)(check G16);被用户拒绝时不回落

取数

能力 状态 对你意味着什么
request(HTTP + 解析) 各平台一致 支持 queryParams / header / body / formData / timeout;formatType 缺省 string,要 JSON 显式写 "json"
域名白名单 manifest.network 各平台一致,run 同一套判据 集合必须与 .df 里实际请求的 host 完全相等(check G3)。安装时这份清单会展示给用户看
逐跳重定向守卫 各平台一致,run / render 同一套判据 请求中途跳出白名单会被拒(network_blocked:redirect_escaped:<host>)。用短链或会 302 到 CDN 的接口时,优先直接请求跳到的地址;确实要经过跳转,再把跳到的那个 host 也声明上
服务端代理 不支持 数据从用户设备直连数据源。被墙的源在中国大陆取不到;要登录态 cookie 的站点做不了
按地区选源 ${@app.region} 各平台一致(.df / .af) 值是 cn(中国大陆)/ overseas / 空串(说不准)。有被墙的兜底源时,cn 下跳过它,别让用户先白等一轮超时
htmlParse / xmlParse 各平台的解析引擎不同 别依赖 xmlParse;XML 用 formatType:"string" 拿原文再 split:: 切
data.get / set / remove / has / merge / keys / getAll / clear 各平台一致 包内持久化,跨启动;命名空间恒为本包。没有 scope 参数,写了静默忽略
首屏缓存(有缓存先渲、后台校新) 各平台一致 页面消费的 .df 该带缓存,否则每次进页都白等一轮网络(发布档 check G23 会提醒)。组件的 .df 默认不缓存——组件缓存了,刷新就等于没刷。需要凭证的源:把 credential.state 返回的 fp 拼进缓存键,用户换绑后旧缓存自然失效
取数失败的出口 check G26 组件的 .df 只要有请求就必须有 action:"error" 出口,否则失败时流报成功、空数据覆盖好数据
表达式方法集 各平台一致,但比直觉小 没有 abs / avg / filter / groupBy / push;未知方法一律静默求空,不报错。全表见 numable docs methods

首屏缓存的六条判据(页面消费的 .df 逐条对):

判据 对你意味着什么
缓存必先出 只要有一份没过期太久的数据,首屏就用它渲,不许因为它不新鲜就先渲骨架
穿透必真取 下拉刷新那条路要先删缓存键再取,否则下拉又命中缓存,数字纹丝不动
必须有时间锚 先出的旧数据要在屏上说出自己有多旧。没有时间锚的旧数字比空白更糟
失败不覆盖 写回缓存前判一下关键字段非空;取数失败就不写、继续渲旧值。一次超时写进一份空数据,那个组件会一直空到下次恰好成功
组件的 .df 绝不缓存 组件的刷新意图就是「跳过缓存直接取数」,数据层再压一层缓存 = 刷了等于没刷。页面与组件共用一份 .df 时,缓存由入参开关控制、默认关
结构变了要能识别 缓存里存一个自己的结构版本号,读的时候先比结构再比时间。对不上就当没有缓存 —— 否则升级后第一眼是坏的、再看又正常,最难查的那种

TTL 分两个量:ttl 决定「多旧算不新鲜、要后台校新」,hardTTL 决定「多旧就不配再出场、退回骨架」。分档按这个域的数据自己多久变一次来定:

域特征 ttl hardTTL
连续行情(7×24) 60 秒 6 小时
天气 10 分钟 12 小时
内容榜单 5 分钟 24 小时
平台统计 / 个人面板 15 分钟 24 小时
日更 / 工作日更 1 小时 48 小时

写法见 numable docs df。

多语言

两张表,判据一句话:宿主读来直接显示的字段用 B 表,包内容自己引用的文案用 A 表。

表 写在哪 怎么用 用于
A 表(内容文案) 资产自己的顶层 i18n: {locale: {key: 文本}} 正文写 ${@i18n.key} .rcn · .xform · .af · .df · .xpage
B 表(元数据) 裸字段是基准语言,旁挂 i18n: {locale: {字段: 值}} 宿主自己挑 manifest 的 title / subtitle / description / category · .xwidget 的 title / sub · router.json 的 title · .xjob 的 title / sub / message / form
事实 对你意味着什么
切语言会重新取数 仪表盘对每个包做一次 widget.refresh;要中英两版数据,.df 里直接读 ${@app.language}。数据缓存按语言分开,切完先出骨架、取到再显示。.df 也可以有顶层 i18n 表,写 ${@i18n.key} 直接出成品文案
兜底链:精确匹配 → 语言前缀 → en-US → 基准语言 manifest.lang 声明裸字段是哪门语言
A 表里的空串 = 刻意留白,不兜底;B 表里的空串 = 缺 不想显示就写空串,别删键
发布档必需门是 zh + en 其余语言缺覆盖只是提醒
XPage 节点层的 ${@i18n.x} 只认这一页顶层 i18n 表里的 key,.rcn 表里的 key 在节点层看不到 节点上用的 key 写进 .xpage 顶层 i18n;组件内文案仍住 .rcn 的表(check G35 会拦缺 key)
iOS 桌面小组件的系统选择列表 那一层的文字只能跟系统语言,不跟 App 语言

细节见 numable docs i18n。

发布

形态 怎么装 验签 对你意味着什么
个人自用 文件直接放进 App 工作区目录 免验签,明文直读 边写边看,改一个字就生效;组件上会标出这是本机包
上架分发 从商店下载 Ed25519 签名 + 逐文件 sha256,验不过直接拒绝渲染 发布后不能再手改包内文件;要改就改源、重新发一版

备份与还原

事实 对你意味着什么
App 能导出 / 导入 .nbk 备份文件,各平台互通 用户换机不会丢你的包
备份内容:本地创作包、下载包、data.* 里的数据、仪表盘布局与实例参数、App 设置 用 data.* 存的用户数据(打卡记录、自选列表)会跟着走
不备份:登录 token、凭证、外部 App 授权记忆、缓存、桌面小组件的系统绑定 还原后用户要重新把小组件放回桌面;别把「一次性授权过」当成永久状态来设计
同一个包已存在时,默认不覆盖 还原不会把用户手上更新的版本改回去
用户开了多设备同步时,data.* 在设备之间按顶层键合并,同一个键两边都改了留后写的那份 用户亲手记下、丢了补不回来的记录(打卡、服药、体重),一条一个键(log.2026-09-23、dose.<时间戳>):两台设备各记一笔,两笔都在;整份放进一个键,两台设备各记一笔会丢一笔。后台任务按日期记的取数结果可以放在一个键里(data.merge 一天一格):同一天两边写的是同一份数据,最坏是很少同步的设备上缺一天

规划中,现在不能用

下面这些在设计中,现在写进包里不会生效。不要给用户承诺,也不要围着它们设计流程。

能力 现在能做的替代
鸿蒙桌面小组件的真后台刷新 按「主 App 打开时才更新」来设计文案
.xmenu 作为包内可写的菜单文件 菜单写节点上的 menu 内联数组