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

df —— 取数流(.df)

读者:做工具的用户,和替他干活的 AI。两者读同一份。

它是什么

.df 是一个组件或一个页面取数据的那一步:打网络、抽字段、算派生量,最后把要给渲染层的键透出去。它只做取数与加工,没有 UI、没有导航、没有用户参与——因为它跑在渲染链路上,可能发生在用户看不见的时候(桌面小组件定时刷新、仪表盘首帧)。

需要用户手指参与的动作(弹确认、打开页面、写回参数、震一下)属于另一种文件 .af。两者是同一套引擎的两个能力档:

.df 取数流 .af 交互流
用在哪 .xwidget 的 canvas.depends、XPage 节点的 depends、横幅 flow events.*(点组件、点节点、表单提交)
能用什么 白名单子集(见下) 全部,含 ui.* / nav.* / widget.*
触发者 平台(定时、首帧、下拉) 用户

写错文件类型的症状很具体:H5 里用 runDataFlow 调一个 .af,App 里回 -6 flow not found。.af 的写法与 action 全表见 numable docs af。

最小可用示例

一条真的会打网络、会失败、会透出的流:

{
  "version": 1,
  "actions": [
    {
      "id": "resp",
      "action": "request",
      "params": {
        "url": "https://hn.algolia.com/api/v1/search?tags=front_page",
        "method": "GET",
        "formatType": "json",
        "timeout": "8000"
      }
    },
    {
      "op": "set",
      "props": { "key": "_b", "value": "1" },
      "_note": "屏障:request/concurrent 之后紧邻的节点读不到结果,隔一个空 set 再用"
    },
    {
      "op": "if",
      "props": { "val": "$[if::(gt::(length::(${resp.hits}),0),0,1)]" },
      "items": [ { "action": "error", "params": { "errorMsg": "取不到榜单" } } ],
      "_note": "主干字段取不到 → 让流失败,平台回落上一次成功的数据;绝不渲一份空数据上去"
    },
    { "op": "set", "props": { "key": "t0", "value": "${resp.hits[0].title}" } },
    { "op": "set", "props": { "key": "p0", "value": "${resp.hits[0].points}" } },
    { "op": "set", "props": { "key": "id0", "value": "${resp.hits[0].objectID}" } },
    { "op": "set", "props": { "key": "at", "value": "$[formatDate::(${@time.nowMs},HH:mm)]" } },
    { "op": "set", "props": { "key": "hasT0", "value": "$[if::(gt::(length::(${t0}),0),1,0)]" } },
    { "action": "resultFilter", "params": { "keys": ["t0", "p0", "id0", "at", "hasT0"] } }
  ]
}

跑它:

numable run <包> --flow top1          # 名字 = 组件名(.xwidget 文件名),页面流用文件名(如 rates)或 page/rates

怎么写

文件结构

顶层两个必填键:{ "version": 1, "actions": [ … ] }。可选 i18n(本文件自己的文案表,与 .af 同形,正文用 ${@i18n.key} 取,见下「按语言取数与出文案」)。任何位置都可以加 _note 字符串作注释,下划线开头的键运行时一律忽略。

步骤的三种形状

形状 写法 说明
action 步 { "id": "resp", "action": "request", "params": {…} } 干活的一步。id = 它的结果键
operation 步 { "op": "set", "props": { "key": "x", "value": … } } 控制流与赋值,走 op 键,子节点放 items
复合步 { "action": "concurrent", "items": [ … ] } 写在 action 位,不是 op 位

concurrent / sequential 写成 {"op": "concurrent"} 会被整块静默跳过 —— 现象是那一段 3 毫秒就「成功」了、字段全空。

并发怎么写

两条互不依赖的请求并排跑,总耗时按最慢的那条算:

{ "action": "concurrent", "items": [
    { "id": "a", "action": "request", "params": { "url": "https://api.example.com/a", "formatType": "json" } },
    { "id": "b", "action": "request", "params": { "url": "https://api.example.com/b", "formatType": "json" } } ] }

紧接着放一个屏障,再开始用 ${a.…} / ${b.…}:

{ "op": "set", "props": { "key": "_b", "value": "1" } }

id 就是结果键

带 id 的一步跑完,结果以 id 为键写进数据域,后面用 ${id.字段} 取。两条规矩:

能用的 action(白名单,超出即失败)

类 action
网络与解析 request · clearCookie · htmlParse · xmlParse
本包存储 data.get · data.set · data.remove · data.has · data.keys · data.getAll · data.merge · data.clear
流程 cancel · error · finish · sleep · log · resultFilter
复合 sequential · concurrent(写在 action 位)
operation(走 op 键) if · for · forEach · set · remove · include(⚠️ 见下)

明确禁止:toast / showLoading / hideLoading,以及全部 App 注入 action(ui.* / nav.* / xpage.* / widget.* / installBundle)。这些只能出现在 .af 里,numable check 会拦(E)。

表达式里的方法($[calc::(…)] 这类)是另一张表,全表见 numable docs methods。写方法名前先查那张表:未注册的方法一律静默求空,流照样报成功,组件上那个符号后面的数字就没了。

request 的全部参数

参数 类型 说明
url string 必填。可以插 ${}。host 必须在 manifest.network 里
method string GET(缺省)/ POST / PUT / PATCH / DELETE
queryParams object 问号后面的参数,写成 {"page":"1"},不用自己拼串和转义。每个值都写成字符串,数字也加引号(check G52)
header object 自定义请求头,值一律字符串(check G52)。别把密钥写这儿,走 credential
formData array multipart 表单上传,只在 method 是 POST 时生效。填了它就别再填 body
body string 请求体,只吃字符串。要发 JSON 就在这里写 JSON 串
formatType string string(缺省,拿原文)/ json / xml / base64(拿字节的 base64 串)/ tsv / csv(表格转对象数组)。写了这六个以外的值不会报错,而是「先试 JSON、再试 XML、都不成给原文」—— 大小写不敏感("JSON" 照样命中),但拼成 "text" 这类不认识的值就走那条猜测链 —— 多半也能跑出结果,代价是你失去了「这个接口到底给的是不是 JSON」这条判据,见下
columns array 只对 tsv / csv 生效:只保留这几列(按表头名),顺序照你写的;表头里没有的列输出空串。也接受逗号分隔的字符串。不写、写空数组或空串 = 保留全部列
credential string 凭证声明 id,字面量,见下
timeout string 毫秒,写成字符串 "8000",常用 5000–10000。不写就一直等,慢接口会拖住整个组件的首帧。写成数字在 iOS 与网页上照跑、在鸿蒙上整条请求不发(check G52)

GET:参数走 queryParams

{ "id": "resp", "action": "request", "params": {
    "url": "https://api.example.com/v1/quote",
    "method": "GET",
    "queryParams": { "symbol": "${sym}", "range": "1d" },
    "formatType": "json",
    "timeout": "8000" } }

POST:body 与 formData 二选一

{ "id": "resp", "action": "request", "params": {
    "url": "https://api.example.com/graphql",
    "method": "POST",
    "header": { "Content-Type": "application/json" },
    "body": "{\"query\":\"{ viewer { login } }\"}",
    "formatType": "json",
    "timeout": "8000" } }

formatType 的六个值

值 拿到什么 什么时候用
string(缺省) 原文字符串 接着交给 htmlParse / xmlParse,或自己 split:: 切
json 解析后的对象/数组,${resp.a.b[0].c} 直接取 接口返回 JSON
xml { 根标签: {…} }:属性挂成 @属性名 键、节点文本挂成 value 键、同名兄弟自动变数组 结构浅的 XML
base64 对原始字节做 base64 得到的字符串 取二进制(图片字节)用,不是给文本用
tsv / csv 对象数组:首行是表头,每行一个对象,值一律是字符串 接口返回制表符 / 逗号分隔的表格(报表下载这类)

凭证引用

{ "id": "me", "action": "request", "params": {
    "url": "https://api.github.com/user", "formatType": "json", "credential": "gh" } }

credential 的值必须是 manifest.credentials 里声明过的 id 字面量,不能是 ${} 表达式 —— 动态值在运行时会被静默透传成匿名请求,而你看到的是 401 或空数据。声明格式见 numable docs layout,本机测试的密钥放 .numable/params/_credentials.json。

流里永远不出现密钥本身:注入发生在 App 的网络边界。也不要把凭证或它的派生物放进 resultFilter —— 透出去就进了渲染缓存和分享截图;要让 RCN 知道「绑没绑」,透一个 hasToken 旗标。

抓网页:htmlParse / xmlParse

接口拿不到就抓页面。先用 formatType: "string" 取原文,再交给 htmlParse:

{ "id": "page", "action": "htmlParse", "params": {
    "content": "${resp}",
    "rules": [
      { "key": "title", "selector": "h1.site-title", "defaultValue": "" },
      { "key": "posts", "selector": "ul.list > li", "isArray": "true",
        "items": [
          { "key": "name", "selector": "a.t" },
          { "key": "url", "selector": "a.t", "extractFrom": "href", "prefix": "https://example.com" },
          { "key": "hot", "selector": "span.n", "regex": "(\\d+)", "defaultValue": "0" }
        ] } ] } }

rules 是数组,不是对象——写成 {"title": "h1"} 这样的对象,一个 key 都取不出来,而且不报错。每一项就是一个字段:

字段 必填 说明
key 是 结果里的字段名。没有 key 的规则整条被跳过
selector 是 以 /、./、( 开头当 XPath,其余一律当 CSS 选择器
isArray "true" = 命中的节点全要,结果是数组;缺省只取第一个
extractFrom text(缺省,取文本并去空白)/ html(取内部 HTML)/ 其它任意值都当属性名(href、src、data-id)
regex 对取到的串再筛一次,要第 1 个捕获组(没写捕获组就是整个匹配);匹配不上 = 这个值算空
prefix / suffix 拼在取到的值前后,补相对链接的域名用
defaultValue 没命中节点、或值为空时用它
items 子规则数组,把每个命中节点再拆成对象。写了 items,同一条规则上的 extractFrom / regex / prefix / suffix 全部不生效

三个坑:

结构化 op:set

{ "op": "set", "props": { "key": "rows", "value": ["${v1}", "${v2}", "${v3}"] } }

value 可以是数组或对象字面量,结构保留、叶子逐个求值。要交给 sum:: / max:: 的数组必须这样造 —— 把 [...] 写进方法参数串里不成立,那个统计会静默消失。

op:if 与两种循环

{ "op": "if", "props": { "val": "$[eq::(${fresh},0)]" }, "items": [ … ] }

op:if 的条件键是 props.val。写成 cond 不会报错:条件恒假、分支体静默不执行、流照样报成功。

op:if 只用来分派动作,不要用它算值:分支里 op:set 出来的键出了分支就取不到。算值用表达式里的嵌套 if::。

固定次数用 op:for:

{ "op": "for", "props": { "count": "5", "index": "i" },
  "items": [ { "op": "set", "props": { "key": "y_${i}", "value": "$[calc::(48+${i}*48)]" } } ] }

遍历集合用 op:forEach:

{ "op": "forEach", "props": { "items": "${resp.list}", "key": "it", "index": "i" },
  "items": [ { "op": "set", "props": { "key": "n_${i}", "value": "${it.name}" } } ] }
op:for op:forEach
跑几轮 count 轮(取整;≤0 一轮都不跑) items 的元素个数
循环变量 只有序号 key = 当前元素,index = 序号
序号变量名 index,不写时叫 __index 同左

resultFilter 决定渲染层看得见什么

{ "action": "resultFilter", "params": { "keys": ["t0", "p0", "id0", "at", "hasT0"] } }

只有 keys 里的键会交给 RCN / XPage。漏一个,组件上那格就空、且不报错(闸 G28 会拿 RCN 里的 ${} 跟这份 keys 对一遍)。两条附带纪律:

data.*:本包持久化

读写本包私有的沙盒(用户的自选列表、上次选中项、首屏缓存),跨启动保留。八个 action 各一行:

{ "id": "saved",   "action": "data.get",    "params": { "key": "cities", "default": [ { "name": "上海" } ] } },
{ "id": "hasCity", "action": "data.has",    "params": { "key": "cities" } },
{ "id": "allKeys", "action": "data.keys",   "params": {} },
{ "id": "all",     "action": "data.getAll", "params": {} },
{ "action": "data.set",    "params": { "key": "cityIdx", "value": "2" } },
{ "action": "data.merge",  "params": { "key": "prefs", "value": { "unit": "c", "sort": "hot" } } },
{ "action": "data.remove", "params": { "key": "c.wx.31_121" } },
{ "action": "data.clear",  "params": {} }
action 参数 结果
data.get key · default 存的值;没存过时给 default
data.set key · value 回显刚写进去的值
data.has key true / false
data.keys — 本包全部键名的数组
data.getAll — 本包全部键值的对象
data.merge key · value(对象) 无结果
data.remove key 无结果
data.clear — 无结果

要点:

首屏缓存(页面用)与 ${_cache} 开关

页面消费的 .df 应该做三态,否则每次进页面都白等一轮网络:

  1. 有缓存且新鲜 → 直接用,连接都不建立;
  2. 有缓存但过期(stale)→ 也先渲出来,同时透一个 _stale 旗标让页面后台校新;
  3. 没缓存 / 太旧 → 老实取数。
{ "op": "set", "props": { "key": "cacheIn", "value": "${_cache}" } },
{ "op": "set", "props": { "key": "useCache", "value": "$[parseNumber::(${cacheIn},0)]" } },
{ "op": "set", "props": { "key": "ckey", "value": "$[connect::(c.wx.,${lat},_,${lon})]" } },
{ "id": "hit", "action": "data.get", "params": { "key": "${ckey}", "default": {} } }

三条硬要求:

必守的六条

  1. 屏障:request / concurrent / data.get 之后紧邻的那一步读不到结果(晚一拍),循环体的第一个节点同样如此(它读的是进循环那一刻的快照)。中间插一个 {"op":"set","props":{"key":"_b","value":"1"}} 再用。也不要在 op:if 的嵌套域里读并发分支的 id —— 求值域对不上,取到的是空。
  2. 取数失败要让流失败。主干字段取不到就走 error,平台会回落上一次成功的数据;报成功再渲一份空的,会把好数据覆盖掉。判据钉在「少了它这个组件就没意义」的那个字段上;「集合为空」是成功,可选支路挂了不抛。
  3. 判空一律立显式旗标:hasX = $[if::(gt::(length::(${原始串}),0),1,0)],渲染层只看 hasX。不要用 eq:: 判空 —— 它两边能转数字就按数字比,eq::("00","") 和 eq::(空,0) 都为真。length:: 只钉在原始字符串字段上,对自己算出来的数字它恒为 0。
  4. 方法参数里的 ${} 看不见 flow 入参,只看得见流里已落地的键(带 id 的 action 结果、op:set 的键)。所以入参进流的第一件事是落地:{"op":"set","props":{"key":"k","value":"$[findNotEmpty::(${pair},btcusd)]"}}。
  5. op:set 引用的键必须排在它前面。引擎按顺序跑、没有依赖图,顺序错了就是分支恒落 else,而代码看着全对。
  6. 时间锚要防空。实时数字得让人知道是什么时候的数;formatDate:: 喂空串会渲成 1970 年。先兜哨兵:$[if::(eq::(findNotEmpty::(${ts},__none__),__none__),,formatDate::(${ts},HH:mm))]。

按语言取数与出文案

.df 里可以直接读 ${@app.language}(也可读 @device.language / @time.locale),按语言取数是合法且期望的写法(新闻标题、城市名、商店描述)。切换语言时,仪表盘会对每个包做一次 widget.refresh(清取数计时 + 真取数),按语言取的数据会跟着换。

.df 与 .af 完全同构:.df 也可以有自己的顶层 i18n 表({ locale: { key: 文案 } } 扁平表),正文写 ${@i18n.key},取数流入参里注入的是宿主页表 ⊕ 本文件表(加 @app)。所以「和昨天差不多」「今日在高低区间的 59%」这类成品文案可以直接在数据层出:

{
  "version": 1,
  "actions": [
    { "id": "resp", "action": "request", "params": { "url": "https://api.example.com/today?lang=${@app.language}", "formatType": "json" } },
    { "op": "set", "props": { "key": "_b", "value": "1" } },
    { "op": "set", "props": { "key": "delta", "value": "${resp.delta}" } },
    { "op": "set", "props": { "key": "summary", "value": "$[if::(gt::(${delta},0),${@i18n.up},${@i18n.flat})]" } },
    { "action": "resultFilter", "params": { "keys": ["delta", "summary"] } }
  ],
  "i18n": {
    "zh-CN": { "up": "比昨天高", "flat": "和昨天差不多" },
    "en-US": { "up": "Higher than yesterday", "flat": "About the same as yesterday" }
  }
}

一句话记住:任何 flow 文件都可以有顶层 i18n 表,文案写 ${@i18n.key},语言分支读 ${@app.language}。「今天」这类纯文案放取数层还是渲染层由你自决 —— 渲染层拼则同一份数据在两门语言下可复用,取数层出则渲染层少一次拼接。numable check 对 .df 的表做与 .af 相同的检查(G8:引用的 key 必须在必需门里、各门 key 集一致、无未使用 key)。

数据缓存的键含语言:切完语言,每个组件在新语言下没有落盘数据 —— 先出骨架,取到再显示;离线切语言就没有该语言的数据可显示(走空态 / 错误态)。这是有意取舍。主题(明暗)不同:它是纯渲染参数,切主题只重渲、不取数,数据缓存也不分明暗。文案表的完整规则见 numable docs i18n。

规则(违反 = 返工)

规则 检查方式 违反时的现象 修法
组件的 .df 里有 request 或 concurrent 就必须有 error 出口 check G26 取数失败时流报成功,空数据覆盖掉好数据 主干字段判空 → error
concurrent 之后紧邻的节点不得引用并发分支的 id check G4b 那些字段全空,流仍成功 插一个 op:set _b 屏障
op:if 的条件键必须是 props.val check G30 条件恒假、分支静默不执行、流仍成功 改成 props.val
parseDate 的 pattern 去掉 token 后不能剩字母 check G4c 手机上整条解析失败返 null,只有 render 出的图正常 先 subString:: 切掉 T/Z 再解析
request.credential 必须是已声明的字面量 declId check G18b 运行时静默降级成匿名请求,拿到 401 或空数据 写 manifest.credentials 里的 id
请求的 host 与 manifest.network 恰好相等 check G3 真机被网络守卫静默拦掉,或安装面板列出用不到的域名 不多不少
表达式里不得双单位后缀(14.0ptpt)、$[…] 不得再嵌 $[ check G28 整个组件渲不出来且不报错 内层写裸方法名 if::(…)
RCN 用到的 ${x} 必须在某条 .df 的 resultFilter keys 里 check G28 那一格渲空或恒走兜底 补进 keys
页面消费的 .df 有 request 就该有 data.get 缓存 check --profile publish G23 每次进页面白等一轮网络 做三态;刻意不做就写 _lintCache: "exempt" 加 _note 说明理由
组件消费的 .df,缓存写回必须受 ${_cache} 控制且默认关 check --profile publish G23 组件的刷新链整条空转,数据永远停在上次 页面传 _cache:1,.xwidget 不传
缓存写回的值不得含凭证;缓存键不得含下标 check --profile publish G23 密钥进缓存;A 的数据渲给 B 只透旗标;键用稳定标识
.df 里引用的 ${@i18n.key} 必须在本文件(或宿主页)表的必需门里存在 check G8 那一句文案缺一段,不报错 补 key;刻意为空写 i18nEmptyOk
未注册的方法名一个都不用 run 层(那个键静默求空) 组件上「+%」这类符号还在、数字没了 先查 numable docs methods
concurrent / sequential 写在 action 位 run 层(那段 3ms 就成功、字段全空) 整块静默跳过 改成 "action": "concurrent"
sleep 的参数名是 timestamp,值写成字符串 "1500" run 层(根本不等)· check G52(写成数字) 节流/退避没生效;写成数字时鸿蒙上这一步不执行 改名;数字加引号
htmlParse / xmlParse 的 rules 写成对象 run 层(结果对象是空的) 一个字段都取不出来,流仍成功 改成数组 [{"key":…,"selector":…}]
formData 只在 POST 下写 run 层(对方 400) 表单没发出去、流没有异常 换 POST,或改用 body
不用 op:include 人审 那一段一个节点都不跑,流仍成功 复制那几步,或拆成独立 .df
并发分支里不放「允许失败」的支路 run 层(其它分支一起没结果) 一条挂掉,整组都没数据 把它挪出 concurrent 串行跑

出错怎么办

现象 最可能的原因 先做什么
run 报成功但所有字段空 取值路径写错一层 numable run <包> --full 看真实响应形状
紧跟 request 的那个键是 null 结果晚一拍 插屏障 op:set
循环「只挑第一个」挑成了最后一个 每轮第一个节点读进循环前的快照,守卫旗标恒为初值 循环体首行插屏障 op:set _lb
某个统计键凭空消失 方法名不存在,或把 [...] 写进了方法参数串 查方法表;数组用结构化 set 造
分支怎么都不执行,流却成功 op:if 写了 cond 改 props.val
组件上出现假的 0、0 条、1970-01-01 空值被聚合/格式化成了合法值 先判有无再算;时间锚套哨兵
只有某个平台整组件 -- xmlParse 各平台引擎不同;或 parseDate 的 pattern 带字面量 改 formatType:"string" + split::;pattern 只留纯 token
页面下拉刷新数字纹丝不动 又命中了缓存 onRefresh 里先 data.remove 缓存键再 reloadPage;键要与流里写回的逐字一致
组件刷新了但数据是旧的 组件的 .df 里缓存没受 ${_cache} 控制 加开关,默认关
请求 401 / 拿到匿名数据 credential 没声明或写成了表达式 对照 manifest.credentials
htmlParse 结果是空对象 rules 写成了对象,或选择器只在脚本渲染后才成立 改成数组;看那个页面的接口
formatType: "json" 之后整个 resp 不存在 对方返回的不是合法 JSON,解析失败给了 null 先改 string 打印原文

相关

numable docs methods · numable docs builtins · numable docs af