xwidget —— 组件声明(.xwidget)
字段全表(每个字段必不必填、在哪儿编、写了会怎样)见
numable docs xwidget-fields—— 那张表由编辑器的契约表生成,不会和代码分叉。本章讲的是规则与写法。
读者:做工具的用户,和替他干活的 AI。两者读同一份。
它是什么
一个组件 = 一个 xWidget/<名>.xwidget 文件。它是外壳 + 一份纯渲染配方:外壳管这个组件在仪表盘和组件面板里叫什么、多大、点了去哪、能不能改参数;配方(canvas)管这个组件怎么取数、怎么画、多久刷一次。
同一份 .xwidget 同时供三个地方消费:App 内的仪表盘、组件面板的选择列表、以及桌面小组件。所以尺寸档位不是随便填的 —— 它决定这个组件能不能放到桌面上去。
一个包里的组件是多个而不是一个:22 是最常用的小方组件档,发布要求至少 3 个组件且必须有一个 22 和一个 42/44。
最小可用示例
一个行情组件的完整声明,可直接复制后改:
{
"version": 2,
"title": "个股",
"sub": "价格 · 两个月形状",
"i18n": { "en-US": { "title": "Stock", "sub": "Price and its 2-month shape" } },
"layout": 22,
"params": { "secid": "1.600519" },
"events": {
"onClick": "/detail?secid=${secid}",
"onEdit": "/edit?ref=quote&secid=${secid}"
},
"canvas": {
"source": "@[file://rc/quote.rcn]",
"depends": [
{ "flow": "@[file://flow/quote.df]", "params": { "secid": "${secid}" } }
],
"refresh": {
"interval": ["09:30-16:10@60", "21:30-05:00@60", "3600"],
"at": ["15:05", "16:05", "05:05"],
"tz": "Asia/Shanghai"
}
}
}
怎么写
字段
| 字段 | 必填 | 类型 | 取值 / 一句注意 |
|---|---|---|---|
version |
是 | int | 恒 2 |
title |
是 | string | 组件名(仪表盘、组件面板、桌面小组件配置列表都显示)。裸值 = manifest.lang 那门 |
sub |
是 | string | 副标题,一行放得下。组件面板里跟在标题后面 |
i18n |
建议 | object | { "<locale>": { "title": …, "sub": … } },只认这两个键 |
layout |
是 | int | 两位网格码,见下 |
params |
否 | object | 组件实例参数的默认值,只有标量 |
events |
否 | object | 只有 onClick / onEdit 两个键 |
canvas |
是 | object | { source, depends?, refresh? },只有这三个键 |
jobs |
否 | array | 长按这个组件能加哪几条提醒,每条 { id, params? }。id = 本包 xJob/<id>.xjob 的文件名;params 把组件实例参数映射成提醒参数,值只能写 ${组件参数} 或字面量(取数输出是结果不是身份,长按那一刻还不存在)。不写这个键,长按菜单里就没有「添加提醒」。详见 numable docs alerts(G46) |
顶层只认上表这些键:scene preview previewData kind order open 这类键写了没有任何消费者,也不报错,不要写。canvas 里没有 params,也没有 events,也没有 onEdit —— canvas 只认 source / depends / refresh 三个键,写在里面的 onEdit 一个消费者都没有(交互事件只挂在外壳的 events 上)。克隆现成包时最容易连这块一起抄走,而且抄了不报错:长按组件就是没有「编辑参数」。
layout:网格码与尺寸
两位数,十位 = 宽占几格,个位 = 高占几格,两位各取 1–9,所以合法值是 11 到 99 之间的任意两位码,不是四个枚举。像素由两条式子推出:
w = 90 × 宽格 − 22 h = 98 × 高格 − 38
越界(某一位是 0)会静默回落成 22,不报错 —— 组件照样出来,只是尺寸不是你写的那个。
| 码 | 尺寸(pt) | 常见用途 |
|---|---|---|
11 |
68 × 60 | 一个图标 + 一个数字,再多放不下 |
12 |
68 × 158 | 竖条,一列两三行小字 |
21 |
158 × 60 | 极简单行条 |
22 |
158 × 158 | 一个主数字 + 一条形状。每个包必须有一张 |
32 |
248 × 158 | 比 22 宽一格,主数字旁边能再挂一列 |
33 |
248 × 256 | 近正方,四五行的小列表 |
42 |
338 × 158 | 一排并列的几项 / 带趋势图的横条 |
44 |
338 × 354 | 列表、网格、多块信息 |
46 |
338 × 550 | 长列表。只有鸿蒙桌面放得下 |
62 |
518 × 158 | 超宽横条。桌面都放不下,只在仪表盘里成立 |
App 内的仪表盘任何码都能放。桌面小组件放不放得下各平台有差:
| 平台 | 桌面能放的档位 |
|---|---|
| iPhone / iPad / Mac | 只有 22 / 42 / 44 |
| Android | 4×4 以内的 16 个码(11 12 13 14 21 22 23 24 31 32 33 34 41 42 43 44) |
| 鸿蒙 | 8 个码(11 21 22 32 33 42 44 46) |
| Windows 版 | 没有桌面小组件 |
要让一个组件在所有平台的桌面都可用,就写 22/42/44。别的档位不是不能用,只是那个组件在部分平台上只活在 App 内的仪表盘里 —— 这是设计选择,不是错误。各平台桌面小组件的其它差异见 numable docs capabilities。
三个组件的画法要点(同一份 RCN 换档必错位,所以档位是先定的,不是最后调的):
| 档 | 画法要点 |
|---|---|
42(338×158) |
横向分栏:左侧主数字,右侧一条趋势形状。分栏位置写成 {parent.w} 的四则运算("x": "{parent.w}*0.42"),不要把锚点塞进 $[…] 方法(check G33),也不要在宽高字段里写方法(check G32) |
44(338×354) |
上下两段:顶部一行标题 + 时间锚,下面用 op:forEach 铺 4–6 行。行高写死、行数按可用高算,别让最后一行被裁一半 |
21(158×60) |
只放得下一行:一个图标 + 一个数字 + 一个单位。不要放标题,标题已经在长按菜单和组件面板里显示过了 |
RCN 里用 {parent.w} / {parent.h} 定位,别把 158 / 338 写死 —— 换一档 layout 就整体错位。
params
"params": { "secid": "1.600519", "alias": "茅台", "mask": "0" }
- 只能是标量(字符串/数字/布尔),不能嵌对象或数组。写在这里的是默认值;用户加组件后每个组件实例各持一份自己的值。
- 默认值不本地化:params 是数据不是文案,不要给它做
i18n。要按语言变的字面文案住 RCN 的文案表(numable docs i18n)。 - 键名不要像密钥(
token/secret/api_key…),那会被闸拦下;用户自带的密钥走manifest.credentials。 - 用户怎么改这些值、怎么写回,见
numable docs params。
events
只有两个键,值有两种类型,平台按解析后的类型分派:
| 值的类型 | 怎么写 | 行为 |
|---|---|---|
| 字符串 | "/detail?secid=${secid}" · "numable://self" · "numable://self/page/item?id=${itemId}" |
当作导航串,开对应页面 |
| 结构体 | "@[file://flow/mark-today.af]" 或直接内联一个流对象 |
当作交互流跑 |
onClick= 整组件点击。numable://self开包首页;numable://self/page/<route>开某条路由(那条路由必须在router.json里真实存在)。导航串里的${}只能插顶层标量,${resp.list[0].id}这种路径插不进去 —— 先在取数流里提成顶层键。- 首页也能带参数:
"/?tab=us"或numable://self?tab=us打开首页,tab=us交给首页本身(html 首页读location.search,xpage / form 首页拿到路由参数)。只在首页新打开时送达;这个包的窗口已经开着时(Mac 独立窗口、平板浮层),首页不会重新载入,参数送不到。较早的 App 版本会丢掉首页的参数,要兼容它们就别靠 query:改成onClick跑一个交互流,先data.set写一个一次性的键再nav.open首页,首页读到后立刻清掉。
- 首页也能带参数:
onEdit= 长按组件「编辑参数」的入口。它的两种形态各有一条硬要求:- 走路由:必须是裸 path(
"/edit?ref=quote"),不能写numable://…—— 它是拿去跟router.json逐字匹配的,写成 deeplink 会打开一个空白页且不报错。 - 走流:必须是包内的
.af引用(基准xWidget/,即"@[file://flow/edit.af]"),不能用..跳出包。 - 无论哪种形态,目标必须真的能把值写回(html 页调
xbridge.updateParams,.xform的onSubmit流里调widget.updateParams,XPage 的events里调它)。只是打得开而写不回,等于用户点进去按了半天什么都没变。
- 走路由:必须是裸 path(
- 没有
onEdit的组件,用户加完就再也换不了标的,只能删了重加。凡是带params的组件都该有onEdit。
canvas
"canvas": {
"source": "@[file://rc/quote.rcn]",
"depends": [ { "flow": "@[file://flow/quote.df]", "params": { "secid": "${secid}" } } ],
"refresh": { "interval": ["3600"] }
}
source:这个组件的画法。可以内联一个 RCN 对象,通常写@[file://rc/<名>.rcn]。depends:取数流列表,可以多条,也可以没有(纯静态组件)。refresh:刷新节奏,见下。
多条 depends:按数组顺序一条接一条串行跑,输出依次合并成一份数据交给 RCN,同名键后者覆盖前者。
"canvas": {
"source": "@[file://rc/daily.rcn]",
"depends": [
{ "flow": "@[file://flow/me.df]", "params": {} },
{ "flow": "@[file://flow/todo.df]", "params": { "login": "${login}" } }
],
"refresh": { "interval": ["1800"] }
}
两条纪律:① 后一条读不到前一条的输出,入参各写各的;② 任意一条失败,整个组件就是失败态,不是「少一块」。所以别把可有可无的补充数据单开一条 depends —— 那条挂了会把主数据一起拖没。
静态组件:不取数的组件(说明组件、入口组件)连 depends 一起省掉。
{
"version": 2,
"title": "关于",
"sub": "这个包能给你什么",
"layout": 21,
"params": {},
"canvas": { "source": "@[file://rc/about.rcn]" }
}
⚠️ 省掉 refresh 不是「走默认周期」,是「只在首屏拉一次,之后不再轮询」。真要取数的组件漏写 refresh,现象是数据停在打开 App 那一刻不再动,而且不报错。
@[file://…] 的基准是 xWidget/(对 .xwidget 与 xWidget/rc/*.rcn 都是),所以写 @[file://rc/quote.rcn]、@[file://flow/quote.df],不写 xWidget/ 前缀。页面域(.xpage、page/rc/*.rcn)的基准是包根,两边不通用;基准写错的症状是解析成空 —— 取数根本不发,而且不报错。
depends 的两种形态(最常见的坑)
"depends": ["@[file://flow/quote.df]"]
裸字符串 = 传空入参。裸引用不会自动带上入参,于是流里的 ${secid} 取到空、请求打成 ?secid=,而流仍然报成功,组件安静地渲一片 --。
"depends": [ { "flow": "@[file://flow/quote.df]", "params": { "secid": "${secid}" } } ]
对象形态 = 只传显式写出来的键。要把外壳 params 交给取数流,就得一个键一个键写上去。不吃参数的组件也照样写成 { "flow": …, "params": {} },形状统一。
还有一条同源的纪律:外壳 params 不在 RCN 的渲染域里。RCN 只看得见取数流 resultFilter 透出的键。所以「不参与取数、但组件上要显示」的键(城市名、别名)也必须传进 .df、在流里落地、再透出来,RCN 才取得到 —— 否则那一格恒空或恒走兜底,同样不报错。
添加组件面板里的预览:用户在「添加组件」面板里看到的那张预览,App 会在实例参数里多给一个保留键 _preview,值为 "1",只给这一次渲染、不落盘;仪表盘、桌面小组件、分享都不给。组件要在 depends 的 params 里显式写 "_preview": "${_preview}" 才收得到,不写就和平常一样取数。典型用法是需要凭证的组件:用户还没接入时,流里看到 _preview 是 1 就给一组编造的示例数据,组件上标出「示例」,让用户看出加上之后长什么样;已经接入就照常取真数据。示例必须是编造的,不能用任何用户的真数据。
refresh
"refresh": { "interval": ["09:30-15:00@15", "3600"], "at": ["15:05"], "tz": "Asia/Shanghai" }
| 键 | 写法 |
|---|---|
interval |
数组。HH:MM-HH:MM@秒 = 该时间窗内每 N 秒一次(@ 后面是秒,不是分钟);裸数字 = 秒,作为窗外兜底。跨零点的窗直接写 21:30-05:00@60 |
at |
每日固定时点,["15:05"] |
tz |
IANA 时区名。iPhone / iPad / Android 按它算时间窗与时点;鸿蒙与 Windows 版按设备本地时区算,时区跨度大的用户在这两个平台上会看到窗口偏移 |
到点判据是「到周期了 / 错过了某个时点 / 从来没拉过」三者之一。常用取值:榜单 ["300"]、天气 ["300"]、行情 ["09:30-15:00@10", "3600"](开盘时段每 10 秒、其余每小时)。
四条容易写错的细节:
- 多个时间窗按数组顺序先匹配者胜,不是取最短那个。
["09:00-18:00@600", "09:30-15:00@60"]里第二条永远轮不到 —— 上午九点半也落在第一个窗里。把窄窗写在前面。 - 窗外兜底取的是「第一个裸数字」,后面再写裸数字不生效。一个窗都没命中、又没有裸数字,结果是完全不轮询。
at的时点错过了会补一拍:App 没开着的时候到了15:05,下次打开时判定仍然成立,会补取一次,不会跳过。- 距上一次真取数不足 3 秒的一律跳过,写多小的
interval都突破不了这个下限。
"refresh": { "interval": ["09:30-15:00@60", "21:00-23:00@300", "3600"], "at": ["15:05"] }
这段读作:开盘时段每 60 秒,晚间时段每 300 秒,其余每小时,外加收盘后 15:05 补一拍。
节奏由上游决定。 仪表盘按最早到期的那个组件醒来,不是固定节拍,所以写 10 秒就是 10 秒一跳。免费版用户的周期刷新会被放慢到每 5 分钟(窗口里的 @N 与裸秒都一样),Pro 用户按你写的节奏走;手动更新、首次加载、切语言、widget.refresh 不受这条影响。数据会变、接口扛得住,就写勤快些(行情盘中 10 秒,天气、榜单 5 分钟);上游一天才更新一次的,写快了只是反复取同一份数。要算的是「每次刷新打几个请求 × 每小时刷几次」有没有超过上游限流 —— 超了的表现是组件安静地停在旧数据,不报错。桌面小组件不跟这个数:各平台有自己的下限(iOS 15 分钟、Android 1 分钟、鸿蒙只显示 App 上次画好的图)。要「立刻更新一次」用交互流里的 widget.refresh。
规则(违反 = 返工)
| 规则 | 检查方式 | 违反时的现象 | 修法 |
|---|---|---|---|
depends 里凡是要传参的绑定都写成 {flow, params} 对象 |
check G12c |
组件渲一片 --,而 run 全绿、流报成功 |
逐键写 "k": "${k}" |
RCN 里用到的 ${x} 必须来自该组件某条 .df 的 resultFilter keys |
check G28 |
那一格渲空或恒走兜底,像「这组件本来就这样」 | 参数传进流 → 流里落地 → 透出 → RCN 取 |
onClick 的 numable://self/page/<route> 必须在 router.json 里存在;numable://self/widget/<id> 的 id 必须存在 |
check G12 |
App 里弹「页面不存在」/ 开出空的添加面板 | 跳首页写 numable://self |
onEdit 走路由必须是裸 path 且在 router.json 里;走流必须是包内存在的 .af、禁 .. |
check G12d |
点了没反应,或打开一个空白页且不报错 | 改成 /edit 这种裸 path |
onEdit 的目标必须有写回能力 |
check G12e |
打得开、按了不会有任何变化 | html 页调 xbridge.updateParams;.xform 的 onSubmit 流里调 widget.updateParams |
每包 ≥3 个组件,且至少有一个 22、一个 42 或 44 |
check --profile publish G6 |
发布被拒 | 补组件 |
i18n["en-US"].title/sub 齐全 |
check --profile publish G20 |
英文环境下组件标题、组件面板显示中文 | 补 B 表译文 |
params 键名不像密钥 |
check G18 |
密钥进明文回显面 | 走 manifest.credentials |
几何用 {parent.w} / {parent.h},不写死 158/338 |
render 层(换一档 layout 出图就看得出) | 换档后整体错位、内容出框 | 改相对锚点 |
| 刷新节奏不超过上游限流 | 人审 | 接口被限流,组件停在旧数据且不报错 | 按「每次请求数 × 每小时次数 ≤ 上游配额」调;要立刻刷用 widget.refresh |
桌面要用的组件只用 22/42/44 |
人审 | 那个组件在部分平台的桌面组件列表里根本不出现 | 换档位 |
要取数的组件必须写 canvas.refresh |
人审 | 数据停在打开 App 那一刻,之后再也不动,且不报错 | 补 "refresh": { "interval": ["1800"] } |
onEdit 写在外壳 events 里,不写进 canvas |
人审 | 长按组件没有「编辑参数」,而 check 全绿 |
挪到顶层 events |
| 窄的时间窗写在数组前面 | 人审 | 窄窗被前面的宽窗吃掉,永远轮不到 | 调整数组顺序 |
出错怎么办
| 现象 | 最可能的原因 | 先做什么 |
|---|---|---|
组件上全是 --,但 numable run 全绿 |
depends 用了裸串形态,入参没传进去 |
改 {flow, params} |
| 只有某一格空(比如城市名),别的都对 | 那个键没进 .df 的 resultFilter |
传进去、落地、透出 |
numable render 出的图整块白 |
RCN 层的问题(缺 type、尺寸字段里塞了方法…) |
见 numable docs rcn |
| 点组件没反应 / 弹「页面不存在」 | onClick 指向的路由不存在 |
对照 router.json |
| 长按「编辑参数」进去改了没用 | 目标页没有写回调用 | 见 numable docs params |
| 桌面小组件列表里找不到这个组件 | layout 不在这个平台桌面支持的档位里 |
换 22/42/44 |
组件的尺寸不是我写的那个,变成了 22 |
layout 某一位是 0(比如 20、04),越界静默回落 |
两位都写 1–9 |
| 数据只在刚打开 App 时对,之后再也不更新 | canvas.refresh 整个省掉了 = 只首屏拉一次 |
补 refresh.interval |
加了第二条 depends 之后整个组件都空了 |
新加那条失败,而任一条失败即整组件失败 | 单独跑 numable run 看是哪条挂了 |
| 改了包内容,App 里还是旧的 | manifest.version 没 +1 |
+1 重装 |