← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

小米vela快应用编写

编写、修改、调试或评审小米 Vela 快应用(Xiaomi Vela JS 应用)代码时使用

personAuthor: chbr114hubModelScope

编写小米 Vela 快应用

小米 Vela 快应用运行在小米手环、手表等可穿戴设备上。页面用 .ux 编写,由原生组件渲染,产物是 .rpk 包。它看起来像 HTML/CSS/JS,但不是浏览器,不要用写网页的直觉去套。

0. 开始前必问的四件事

写任何代码之前先确认(缺失就问用户,不要猜):

| 必问项 | 为什么重要 | | --- | --- | | 目标设备与屏幕形状(圆屏 / 矩形 / 胶囊) | 直接决定画布比例与安全区,见 §5 | | 具体机型(手环 8 Pro/9/9 Pro/10?Watch S3/S4/S5?) | 手环全线不支持 fetch 网络接口,先定机型再定数据方案,见 §6 | | 设计稿基准宽度 | 决定 config.designWidth,默认 480 | | 要用哪些系统能力(网络 / 存储 / 定位 / 传感器…) | 每用一个接口都要在 manifest.json 的 features 里声明,见 §6 |

若用户只说"做个手环快应用",用 assets/starter/ 骨架起步,并按 assets/starter 说明改包名。

1. 标准工作流

  1. 搭骨架 — 新项目直接用脚本生成,不要手搓目录:

    python <skill目录>/scripts/new_project.py ./my-app --package com.company.demo --name "我的应用" --design-width 480
    

    或复制 assets/starter/ 后手动改 package、name、icon、router。

  2. 定页面清单 — 一个页面 = src/pages/<PageName>/<PageName>.ux,且必须在 router.pages 里注册,否则编译时被跳过;目录名、key、ux 文件名保持一致。

  3. 写 template — 先只写结构(div / text / image / input),套公共 class 做布局。

  4. 写 style — Flexbox + 480 基准尺寸;尺寸直接抄设计稿标注。

  5. 写 script — 先给 private 填静态假数据让页面立起来,再接接口。

  6. 接数据 — onReady 里请求,成功后赋值给 this.xxx,失败/加载态都要处理。

  7. 自检 — 跑 scripts/check_ux.py,再对照 §8 清单。

2. 项目骨架

├── package.json
├── quickapp.config.js        # 可选:cli 编译参数
├── sign/                     # 签名:certificate.pem / private.pem
└── src/
    ├── manifest.json         # 项目配置:包名/版本/features/路由
    ├── app.ux                # 应用入口:全局生命周期、全局数据与方法
    ├── pages/
    │   └── index/index.ux
    ├── common/               # 跨页面共享的 components / images / scripts
    └── i18n/                 # defaults.json / zh-CN.json / en-US.json

src/ 名称固定不可改。build/、dist/ 是构建产物,不要手写。路径规则见 §7。

3. .ux 三段式与铁律

一个 .ux 页面 = <template> + <style> + <script>,三者顺序固定。

样式也可以拆成同名的 .css 用 <style src="./x.css"></style> 引入(此时该 ux 里不再写内联 <style> 内容);脚本同理可拆成 .js。官方文档也展示了 detail.ux + detail.css + detail.js 的全拆分结构,并提示全拆分后 ux 中不再包含 template 标签——实践上建议保留 template 在 ux 中,只把 style/script 拆出去。

<template>
  <div class="page" @swipe="onSwipe">
    <text class="title">{{title}}</text>
    <input class="btn" type="button" value="查看详情" onclick="goDetail">
  </div>
</template>

<style>
  .page { flex-direction: column; align-items: center; justify-content: center; }
  .btn { width: 400px; height: 60px; border-radius: 30px; background-color: #09ba07; color: #ffffff; }
</style>

<script>
  import router from '@system.router'

  export default {
    private: { title: '示例' },
    onSwipe(e) { if (e.direction === 'up') this.goDetail() },
    goDetail() { router.push({ uri: '/detail' }) }
  }
</script>

必须遵守(违反即报错或白屏)

  • <template> 只能有 1 个根节点,且根节点不能是 <block>。
  • 文本必须包在 <text> 里,裸文本不会渲染。
  • if / elif / else 必须是相邻的兄弟节点,否则编译不过。show="{{cond}}" 等价于 visible: none,组件仍在 VDOM 中。
  • for 的 tid 指定的字段必须存在且唯一,否则可能运行异常或性能劣化;tid 不支持表达式。
  • JS 环境不是 Node 环境:import fs from 'fs' 这类会失败。只 import @system.* 模块和项目内相对路径文件。
  • app.ux 里的 /**manifest**/ 注释标识不能删,编译后它会带出 manifest 配置。

页面数据对象:private / protected / public

可声明的块有 data / public / protected / private / computed,public/protected/private 不能与 data 同时使用;属性名不能以 $ 或 _ 开头,也不要用 for / if / show / tid 等保留字。

被 router 传参、外部拉起时能不能覆盖,取决于声明在哪:

| 声明块 | 页面内部跳转传参可覆盖 | 应用外部拉起传参可覆盖 | 用途 | | --- | --- | --- | --- | | private | 否 | 否 | 页面自有数据,默认都用它 | | protected | 是 | 否 | 需要接收其他页面传参时用 | | public | 是 | 是 | 需要被外部拉起时覆盖时用 |

接收方与传参方写法:

// 传参(uri 用页面在 router.pages 中配置的 path)
router.push({ uri: '/detail', params: { id: 123 } })
// 接收
export default { protected: { id: 0 }, onInit() { console.info(this.id) } }

生命周期

页面:onInit(ViewModel 数据就绪,可读数据、发起请求)、onReady(模板编译完成,可 this.$element(id) 取节点)、onShow / onHide、onDestroy(在这里取消订阅)、onBackPress(返回 true 表示自行处理,否则系统返回上一页)、onRefresh(query)(singleTask 页面被再次打开)、onConfigurationChanged(evt)。 应用:onCreate、onShow、onHide、onDestroy、onError(e)。

全局数据与方法写在 app.ux,页面里通过下表访问:

| 常用成员 | 用途 | | --- | --- | | this.$app.$def.x | app.ux 暴露的属性 / 方法 | | this.$app.$data.x | manifest.json 中 config.data 的数据 | | this.$element(id) | 取模板节点,需在 onReady 之后 | | this.$valid | 页面是否仍存在 | | this.$canIUse(cap) | API 3+,能力探测,如 '@system.router.push'、'scroll.attr.scroll-x' | | this.$t(path) / $tc(path, n) | 多语言 |

页面销毁后 setTimeout 之类绑定在该页的异步回调不会再执行。订阅类接口(定位、传感器等)务必在 onDestroy 里 unsubscribe。

4. manifest.json

{
  "package": "com.company.module",
  "name": "应用名",
  "icon": "/common/images/icon.png",
  "versionName": "1.0",
  "versionCode": 1,
  "minAPILevel": 1,
  "deviceTypeList": ["watch"],
  "features": [{ "name": "system.router" }, { "name": "system.fetch" }],
  "config": { "designWidth": 480, "logLevel": "info" },
  "display": { "backgroundColor": "#000000" },
  "permissions": [],
  "router": {
    "entry": "index",
    "pages": {
      "index": { "component": "index", "path": "/" },
      "detail": { "component": "detail", "path": "/detail", "launchMode": "singleTask" }
    }
  }
}

硬性要求:

  • package 不得与原生应用包名重复,推荐 com.company.module。
  • name 6 个汉字以内,须与应用商店名称一致;icon 提供 192x192。
  • versionCode 从 1 自增,每次重新上传包都要 +1。
  • 页面目录名 = router.pages 的 key = ux 文件名,三者保持一致最省心(pages/detail/detail.ux ↔ key detail ↔ component: "detail")。path 必须唯一,缺省为 /<页面名称>;router.push 的 uri 传的是这个 path。
  • deviceTypeList 目前只支持 watch。
  • 用到新版 API 特性时,把 minAPILevel 提到对应版本号。
  • 定位 / 设备信息需在 permissions 声明 hapjs.permission.LOCATION / hapjs.permission.DEVICE_INFO。

5. 样式、尺寸与屏幕适配

Flexbox 布局,不是 Web 的 block/float 布局。默认 flex-direction: row,竖排要显式写 column。

尺寸基准:所有与大小相关的样式(width、font-size 等)以 designWidth(默认 480px)为基准,按实际屏幕宽度等比缩放(类似 web 的 rem)。换算公式 设计稿1px / 设计稿基准宽度 = 框架样式1px / designWidth——例:设计稿宽 640 时,把 designWidth 设 640 就能直抄;保持 480 则 100px 要写成 75px。dp 单位(API 3+)= 物理分辨率 / DPR。

盒模型固定 border-box(width 已含 padding 和 border),不支持 content-box、不能写 box-sizing。

选择器只支持四种:.class、#id、tag、, 分组。优先级 inline > #id > .class > tag。 以下全部不支持:后代 .a .b、子 .a > .b、兄弟 .a + .b / .a ~ .b、伪类 :hover / :first-child / :nth-child()、伪元素 ::before、[attr=value]、通配 *、交集 .a.b。需要层级样式就多拆一层 div 并挂 class。

支持 <style src="./x.css">、<style lang="less"> / lang="sass" 预编译与 @import。

手环/手表屏幕实参(设计时务必对照):

| 设备 | 形状 | 分辨率 | 建议长宽比 | | --- | --- | --- | --- | | 小米手环 8 Pro / 9 Pro | 矩形 | 336x480 | 0.7 | | 小米手环 9 | 胶囊 | 192x490 | 0.39 | | 小米手环 10 | 胶囊 | 212x520 | 0.39 | | 小米 Watch S1 Pro / S5 | 圆形 | 480x480 | 1 | | 小米 Watch H1 / S3 / S4 | 圆形 | 466x466 | 1 |

形状长宽比分档:圆屏 W/H = 1;矩形 0.5 ≤ W/H < 1;胶囊 0.3 < W/H < 0.5。

适配要点:

  • 圆屏、胶囊屏有弧形边缘安全区,主体内容、文本、可点区域必须落在安全区内,否则边缘文字被切、按钮点不到。
  • 主色背景与圆角配合形状:圆屏用 border-radius 做成圆形画布;矩形/胶囊注意上下留白。
  • 触控目标要够大,屏幕上主要操作控件用 <input type="button">;注意 input 默认尺寸是手机尺寸(128x70、37.5px 字号),手表上几乎都要显式覆盖。
  • 长列表放进 <list>/<list-item> 或 <scroll>,不要用一长串 div 硬堆;list / scroll 必须显式设高度(或横向时的宽度)才会滚动。
  • 尺寸差异大的机型用媒体查询区分(见下)。

媒体查询(多屏适配的主力工具,需 API Level 2+,min-width/height/aspect-ratio/device-type 需 3+;aiot-toolkit ≥ 1.1.3):

/* 按屏幕形状适配:circle 圆屏 / rect 矩形 / pill-shaped 胶囊 */
@media (shape: circle) or (shape: pill-shaped) {
  .box { padding-left: 30px; padding-right: 30px; }
}

/* 限定设备类型:watch 手表 / band 手环 / smartspeaker */
@media (device-type: band) {
  .title { font-size: 26px; }
}

/* 按宽度区间,level 3 写法;查询不带单位,数值单位为 dp */
@media (min-width: 160) and (max-width: 200) {
  .box { background-color: yellow; }
}

dp = 物理分辨率 / DPR。常用机型的水平 dp 值:Watch S1 Pro / S5 = 240,Watch H1 / S3 / S4 = 233,REDMI Watch 5 = 216,手环 8 Pro / 9 Pro = 168,手环 10 = 106,手环 9 = 96——手环 dp 比手表小一半多,这是同一套样式在两类设备上表现差异巨大的根因。

媒体查询支持机型:手环 9 / 9 Pro、手环 10、Watch S3 / S4 / S5、REDMI Watch 5 / 6;不支持手环 8 Pro、Redmi Watch 4、S1 Pro。给这些机型写代码时别依赖媒体查询,改用 flex 自适应。

6. 接口与数据

每用一个接口,通常要做两件事:在 manifest.json 的 features 里声明,再在页面 import。声明名 = import 模块名去掉前导 @(多段的如 system.bluetooth.ble)。

例外:@system.app、@system.router、@system.configuration 文档明确写了无需声明,其余接口不声明就用不了。

"features": [{ "name": "system.router" }, { "name": "system.fetch" }]
import router from '@system.router'
import fetch from '@system.fetch'

常用:

| 用途 | 模块 | 备注 | | --- | --- | --- | | 页面跳转 | @system.router | push / replace / back,配 params | | 网络请求 | @system.fetch | fetch.fetch({url}).then(res => res.data),res.data 是字符串需自行 JSON.parse | | 键值存储 | @system.storage | 仅 get / set / delete / clear;key 用普通字符串 | | 文件读写 | @system.file | 按 URI 读写,分区 internal://cache(缓存,可被清理)、internal://files(永久)、internal://mass(大文件,不保证一直可用)、internal://tmp(只读、重启失效) | | 设备信息 | @system.device | 需权限 | | 定位 / 传感器 / 振动 / 亮度 / 电量 / 蓝牙 / 音频 | @system.* | 详见 references/features.md |

⚠️ 先看机型能力,再决定要不要联网

手环全线与部分手表不支持网络接口,这是手环快应用最容易踩的坑:

| 接口 | 支持 | 不支持 | | --- | --- | --- | | fetch / request / network / geolocation / crypto | Watch S3 / S4 / S5、REDMI Watch 5 / 6 | 小米手环 8 Pro / 9 / 9 Pro / 10 全线、Redmi Watch 4、S1 Pro | | event / battery | 手环 10、S4、REDMI Watch 5 / 6、S5 | 手环 8 Pro / 9 / 9 Pro、S3、Redmi Watch 4 | | sensor 压强 | Watch S3、手环 9 Pro、手环 10、S4、S5 | 手环 8 Pro、手环 9 | | sensor 加速度 | 手环 9 / 9 Pro、手环 10、S5 | 手环 8 Pro、S3、S4 | | vibrator.vibrate | 全系支持 | — | | vibrator.start/stop | 仅 Watch S5 | 手环全线 |

所以:面向手环开发时,先确认目标机型是否联网。不支持则设计成离线应用或依赖 interconnect(设备通信,且要求快应用包名与签名和手机端 App 一致)。完整机型矩阵见 references/features.md §3。

数据流范式:onInit 里读缓存先渲染 → onReady 请求网络 → 成功覆盖 this.xxx,失败保留旧数据或显示错误态。必须处理加载中、失败、空数据三种状态,手环屏幕小,优先用短文案。接口回调的 fail 里要处理错误码 203(设备不支持)、201/207(权限)。

7. 资源路径

| 用途 | 写法 | | --- | --- | | 应用内绝对路径 | /common/images/a.png | | 相对路径 | ./a.png、../common/a.png | | import 代码文件 | 相对路径:../common/utils.js | | CSS 里引用资源 | url(/common/abc.png) |

关键坑:a.css 被 b.ux 导入且不在同一目录时,a.css 内部引用的资源必须写成绝对路径,因为编译时被导入文件会被复制到导入文件所在目录,相对路径会失效。URI 允许字符 0-9a-zA-Z_-./%:,不能出现 ..。

8. 自检清单

先跑脚本(零依赖 Python 3,能覆盖下面绝大部分条目):

python scripts/check_ux.py <项目根目录>          # 有 ERROR 返回退出码 1
python scripts/check_ux.py <项目根目录> --strict  # 警告也视为失败

它会检查:根节点数量与 <block>、裸文本、非法选择器、features 声明、页面注册与命名、manifest 字段、资源引用、订阅未取消。它是辅助,不能替代下面的人工判断。

交付前逐条确认:

  • [ ] <template> 只有一个根节点,根节点不是 <block>;所有文本在 <text> 内。
  • [ ] 所有 if/elif/else 是相邻兄弟节点。
  • [ ] 每个 for 的 tid 字段存在且唯一。
  • [ ] 每个 @system.* 模块都在 manifest.features 中声明了。
  • [ ] 每个页面都在 router.pages 注册,目录名/key/ux 文件名一致,path 不重复。
  • [ ] 样式只用 .class / #id / tag / 分组选择器,无 :hover、*、后代选择器。
  • [ ] 尺寸基于 480(或已改 designWidth),未硬编码真实像素屏宽。
  • [ ] 圆屏 / 胶囊屏下内容落在安全区内,主控件足够大可点。
  • [ ] 页面有加载中 / 失败 / 空数据处理;onDestroy 里已取消订阅。
  • [ ] 所用接口在目标机型上受支持(手环无网络接口,见 §6)。
  • [ ] 首页渲染完成时间(FMP)不超过 2000ms。
  • [ ] versionCode 已自增,name ≤ 6 汉字,icon 为 192x192。
  • [ ] 对照 references/practices.md 的验收标准与最佳实践。

9. 构建、调试与打包

AIoT-toolkit(命令行,可脱离 IDE):

| 命令 | 作用 | | --- | --- | | npm create aiot | 创建项目 | | aiot start | 直接运行(首次会提示创建模拟器) | | aiot build | 构建,生成 .debug.rpk 到 dist/ | | aiot release | release 模式构建,生成 .release.rpk | | aiot getConnectedDevices | 列出已连接设备 |

编译参数只在 build / server / release 生效,命令行传或写进项目根 quickapp.config.js 的 cli 字段:

module.exports = { cli: { devtool: 'source-map', 'enable-custom-component': true } }

常用参数:--devtool(sourcemap)、--enable-jsc(JS 转 jsc 提速)、--enable-protobuf(二进制打包提速)、--enable-custom-component(用自定义组件必须开)。

官方提示:自定义组件有独立 ViewModel、存在内存开销,手表手环等轻量设备上不建议使用。手环应用优先用 div + class 拆分;确需使用时再开 --enable-custom-component。组件写法见 references/syntax.md §5。

release 打包前需要签名文件 sign/private.pem 与 sign/certificate.pem:

openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem

AIoT-IDE 中:新建项目走「文件 → 新建项目 → watch → 创建」;banner 栏可打包 / 调试 / 管理模拟器;调试面板有 DOM 树、Console、断点。

发布:官方验收页给出的唯一硬性指标是首页渲染完成时间 FMP ≤ 2000ms。其余发布关卡(后台运行需求合理性、证书一致性且 release 证书不可变、异常场景覆盖、息屏重亮屏重复 onShow)与最佳实践阈值(list 首次 >10 条要分页且每页 ≤20、大图 >100kb 要 loading+缓存、在线图 ≤200kb、不轮询 getApkStatus())见 references/practices.md §1 与 §6。

10. 常见错误速查

| 症状 | 原因 | | --- | --- | | 页面空白、模板不渲染 | <template> 多个根节点;或根节点用了 <block> | | 文字不显示 | 文本没放进 <text> | | 编译报样式错 | 用了不支持的选择器(:hover / * / 后代 / .a.b) | | import 的模块调用不生效 | 没在 manifest.features 声明 | | 页面传参拿不到 | 参数声明在 private,应改 protected(外部拉起用 public) | | 条件渲染编译失败 | if/elif/else 之间插了别的节点 | | 列表复用错乱 | tid 字段不唯一或不存在 | | 图片不显示 | 被导入的 css/ux 里用了相对路径引用图片,改绝对路径 | | 自定义组件不生效 | 构建未加 --enable-custom-component | | 切页面数据不刷新 | 页面已存在,singleTask 下参数需在 onRefresh(query) 里手动赋给 this | | 联网功能在真机不工作 | 目标机型不支持(手环全线无 fetch),先查 §6 机型表 | | 列表/滚动区域空白 | list / scroll 没有显式设 width / height | | 按钮在手表上偏小 | input 默认尺寸是手机尺寸,必须显式覆盖 width / height / font-size |

参考资料

按需读取,不要一次全读:

| 文件 | 内容 | | --- | --- | | references/components.md | 组件、通用属性/样式/事件/方法速查,含手环小屏约束 | | references/features.md | 全部 @system.* 接口签名、features 声明对照、机型支持矩阵 | | references/syntax.md | template/style/script 语法、自定义组件、媒体查询与页面切换 | | references/practices.md | 性能优化、启动模式、i18n、后台运行、验收标准、常见坑、APILevel 差异 | | scripts/new_project.py | 生成新项目骨架(自动改写 package / name / designWidth) | | scripts/check_ux.py | 静态自检脚本,零依赖 Python 3 | | assets/starter/ | 可直接复制运行的最小项目骨架 |

官方文档:https://iot.mi.com/vela/quickapp/zh/guide/