编写小米 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. 标准工作流
-
搭骨架 — 新项目直接用脚本生成,不要手搓目录:
python <skill目录>/scripts/new_project.py ./my-app --package com.company.demo --name "我的应用" --design-width 480或复制
assets/starter/后手动改package、name、icon、router。 -
定页面清单 — 一个页面 =
src/pages/<PageName>/<PageName>.ux,且必须在router.pages里注册,否则编译时被跳过;目录名、key、ux 文件名保持一致。 -
写 template — 先只写结构(
div/text/image/input),套公共 class 做布局。 -
写 style — Flexbox + 480 基准尺寸;尺寸直接抄设计稿标注。
-
写 script — 先给
private填静态假数据让页面立起来,再接接口。 -
接数据 —
onReady里请求,成功后赋值给this.xxx,失败/加载态都要处理。 -
自检 — 跑
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。name6 个汉字以内,须与应用商店名称一致;icon提供 192x192。versionCode从 1 自增,每次重新上传包都要 +1。- 页面目录名 =
router.pages的 key = ux 文件名,三者保持一致最省心(pages/detail/detail.ux↔ keydetail↔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/ | 可直接复制运行的最小项目骨架 |
Scan to join WeChat group