查看原文
最近摸索了一下小米 vela 快应用的编写,写个教程顺便做个归纳 蹭个 hyperOS 的热度。
本文章使用初学者 毕竟我也是。
概述#
在介绍之前,需要了解一个 vela app 基本的架构。
项目结构#
基本架构:
├── manifest.json
├── app.ux
├── pages
│ ├── index
| | ├── index.ux
| | ├── index.css
| | └── index.js
│ └── detail
| ├── detail.ux
| ├── detail.css
| └── detail.js
├── i18n
| ├── defaults.json
| ├── zh-CN.json
| └── en-US.json
└── common
├── style.css
├── utils.js
└── logo.png
其中,
-
manifest.json
记录 app 的基本信息例如以下属性依次是:
package: 包名,自己命名。
name: app 名称,会在手表上显示。
versionName: 版本名称,自己命名。
minPlatformVersion: 最低 api 版本。
icon: 图标文件路径
{% hideToggle 资源和文件访问规则 %}
资源和文件访问规则
应用资源路径分为绝对路径和相对路径,以 "/" 开头的路径表示绝对路径,比如 /common/a.png,不以 "/" 开头的路径是相对路径,比如 a.png 和 ../common/a.png 等。
应用资源文件分为代码文件和资源文件,代码文件是指 .js/.css/.ux 等包含代码的文件,其他文件则是资源文件,这类文件一般只当作数据来使用,比如图片、视频等。
- 在代码文件中,导入其他代码文件时,使用相对路径,比如:../common/component.ux;
- 在代码文件中,引用资源文件 (如:图片、视频等) 时,一般情况下使用相对路径,比如: ./abc.png;
- 当代码文件需要被导入时,如果导入文件与被导入文件在同一个目录,被导入文件引用资源文件时可以使用相对路径,但如果不在同一目录,必须使用绝对路径,因为被导入文件编译时会被复制到导入文件中,编译后目录会发生变化。比如 a.css 文件被 b.ux 导入,如果 a.css 与 b.ux 在同一个目录,a.css 引用资源文件时可以写相对路径:abc.png,如果不在同一个目录,必须写绝对路径:/common/abc.png,再比如当 a.ux 文件被 b.ux 文件导入时,如果 a.ux 与 b.ux 在同一个目录,a.ux 引用资源文件时可以写相对路径:a.png,如果不在同一个目录,a.ux 引用资源必须写绝对路径:/common/abc.png;
- 在 CSS 中,与前端开发一致,使用 url (PATH) 的方式访问资源文件,如:url (/common/abc.png)。
{% endhideToggle %}
features: 调用的接口声明,有些敏感接口只有声明才可使用。
designWidth: 设计稿的尺寸,具体看页面样式与布局。
router: app 中每个页面的定义。
{ "package": "com.genkaim.muyu", "name": "电子木鱼", "versionName": "1.1.0", "versionCode": 4, "minPlatformVersion": 1000, "icon": "/common/logo.png", "deviceTypeList": [ "watch" ], "features": [ { "name": "system.storage" }, { "name": "system.file" }, { "name": "system.prompt" }, { "name": "system.vibrator" } ], "config": { "logLevel": "log", "designWidth": 600 }, "router": { "entry": "pages/index", "pages": { "pages/index": { "component": "index" }, "pages/settings": { "component": "settings" }, "pages/confirm": { "component": "confirm" }, "pages/about": { "component": "about" } } } }
-
app.ux
为 app 基本的 js 语法例如生命周期中,app 的 onCreate、onDestroy 在这调用在下面的代码中,会在 app 打开时和退出时分别 log “onCreate” 和 "onDestroy"。
<script> export default { data: { a: 1 }, onCreate() { console.log("onCreate") }, onDestroy() { console.log("onDestroy") } } </script>
-
pages
下每个文件夹为当前页面的资源文件,包含.ux
等等。 -
i18n
语言文件。 -
common
存放公用资源文件,比如 logo。
生命周期#
这里借用一下官方的图。
简单来说,app 生命周期就是当打开 app 时,一次进行下面步骤,页面 onShow 前,先经历 onInit 和 onReady 两个阶段(这在 export default 中是具体的两个函数)。
当 app 退出时,为 onDestroy。
ux
文件的基本认识#
下面是一个简单的.ux
文件的内容:
<template>
<div class="page">
<text class="title">确定要清除所有功德吗</text>
<input class="yes" type="button" value="确认" onclick="clearData" />
<input class="no" type="button" value="取消" onclick="goBack" />
</div>
</template>
<script>
import prompt from '@system.prompt'
export default {
goBack(event) {
router.back()
},
clearData(event) {
router.back()
}
}
</script>
<style>
.yes, .no {
width: 500px;
height: 150px;
color: white;
font-size: 40px;
font-weight: bold;
margin-top: 60px;
}
.page {
position: absolute;
background-color: black;
display: flex;
flex-direction: column;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
.back {
position: absolute;
left: 0px;
top: 10px;
background-color: black;
color: white;
font-size: 80px;
font-weight: bold;
background-image: url('/common/icon-back.png');
}
.yes {
margin-top: 50px;
background-color: rgb(44, 44, 44);
}
.no {
margin-top: 30px;
}
.title {
color: white;
font-size: 50px;
font-weight: bold;
top: 5px;
}
</style>
- template 标签: 类似于 HTML 的语言,vela 提供多种组件。例如这里出现两种不同的组件:
text 和 input,分别会被渲染为文本块和按钮。
其中:
1.1 class 为自己定义的类名 type 为组件类型,这里为按钮。
1.2 button 标签的 value 值为按钮上显示的文字。
1.3 onclick 为事件绑定,意思为满足” 点按 “这一条件时,会调用指定函数,函数在 export default 定义。
为什么要在 template 下再包一层 div 呢?
因为 template 下只允许存在一个根节点,必须先创建一个节点,内部再放并列节点。
- script 脚本: 支持 ES5 / ES6 语法,这里的 export default 中,引用了一个模块来实现返回的功能。
- **style 层叠样式表:** 用选择器选择并设置属性即可。
简单示例:电子木鱼基本功能实现#
基本功能:#
-
页面元素:电子木鱼(图片),显示功德数值功能,” 功德 + 1“动画。
-
实现点按木鱼计数功能。
页面资源:单击下载
页面布局#
- 放置木鱼图片可以用 img 组件或者 css 实现,这里使用的前者。
因为需要实现点按反馈的功能,所以需要绑定 onclick 事件。
<img class="muyu" src="muyu.png" onclick="rpPlusPlus()" />
- 计数功能可以通过 text 组件显示,这里注意,动态的数据(变量)可以通过大括号的方式显示,例如下面的代码可以实现显示功德计数(变量在下文)。
<text class="cnt" >功德 {{localCnt}}</text>
- 而动画的实现则是通过 text 组件 +if 条件渲染+ 绑定动画(下文)实现的。
if 条件渲染指 if 内表达式的值为 true 时,会渲染当前元素。
<text if="{{plus}}" class="showPlus">功德+1</text>
脚本#
- 数据处理:
在上文中,需要几个变量:localCnt (int) 用于计数功德,plus (bool) 用于指示消息的显示。
定义变量需要这么写:
其中,
属性 | 类型 | 描述 |
---|---|---|
public | Object | 页面级组件的数据模型,影响传入数据的覆盖机制:public 内定义的属性允许被传入的数据覆盖,如果外部传入数据的某个属性未被声明,在 public 中不会新增这个属性 |
protected | Object | 页面级组件的数据模型,影响传入数据的覆盖机制:protected 内定义的属性,允许被应用内部页面请求传递的数据覆盖,不允许被应用外部请求传递的数据覆盖 |
private | Object | 页面级组件的数据模型,影响传入数据的覆盖机制:private 内定义的属性不允许被覆盖 |
<script>
export default {
public: {
localCnt: 0,
plus: false
}
}
</script>
- 在上文中,需要定义 rpPlusPlus 函数,实现功德 + 1
其中:
通过 this.Name 访问当前页面数据对象。
并设置 plus 为 true,在 400ms 后设置为 false,即让其显示 400ms,因为它绑定了动画(下文),看 起来的效果就是文字向上移动然后消失。
rpPlusPlus(event) {
this.plus = true;
this.localCnt++;
setTimeout(() => {
this.plus = false;
}, 400);
}
css#
- 绑定动画:
上文已经定义了 class 为 showPlus,所以通过.showPlus
选择元素:
.showPlus {
position: absolute;/*定位*/
right: 70px;
top: 210px;
color: white;/*字体颜色*/
font-weight: bold;/*字体粗细*/
animation-name: moveUp;/*字体绑定动画*/
animation-delay: 0s;/*动画延迟时间*/
animation-duration: 200ms;/*动画持续时间*/
animation-iteration-count: 1;/*动画持续次数*/
}
@keyframes moveUp {/*定义动画*/
0% {
transform: translateY(0);
}
99% {
transform: translateY(-40px);
}
100% {
visibility: hidden;
}
}
- cnt、page 和 muyu 元素:
.muyu {
position: absolute;/*定位*/
height: 350px;
top: 250px;
background-color: transparent;/*背景颜色*/
width: 450px;/*大小*/
}
.cnt {
position: absolute;
bottom: 100px;/*定位*/
font-size: 70px;/*字体大小*/
color: white;/*字体颜色*/
}
.page {
background-color: black;/*背景颜色*/
display: flex;
flex-direction: column;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 100%; /* 确保容器占满整个视窗 */
}
完整代码:
<template>
<div class="page">
<img class="muyu" src="muyu.png" onclick="rpPlusPlus()" />
<text if="{{plus}}" class="showPlus">功德+1</text>
<text class="cnt" >功德 {{localCnt}}</text>
</div>
</template>
<script>
export default {
public: {
localCnt: 0,
plus: false
},
rpPlusPlus(event) {
this.plus = true;
this.localCnt++;
setTimeout(() => {
this.plus = false;
}, 400);
}
}
</script>
<style>
.page {
background-color: black;/*背景颜色*/
display: flex;
flex-direction: column;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 100%; /* 确保容器占满整个视窗 */
}
.showPlus {
position: absolute;/*定位*/
right: 70px;
top: 210px;
color: white;/*字体颜色*/
font-weight: bold;/*字体粗细*/
animation-name: moveUp;/*字体绑定动画*/
animation-delay: 0s;/*动画延迟时间*/
animation-duration: 200ms;/*动画持续时间*/
animation-iteration-count: 1;/*动画持续次数*/
}
@keyframes moveUp {/*定义动画*/
0% {
transform: translateY(0);
}
99% {
transform: translateY(-40px);
}
100% {
visibility: hidden;
}
}
.muyu {
position: absolute;/*定位*/
height: 350px;
top: 250px;
background-color: transparent;/*背景颜色*/
width: 450px;/*大小*/
}
.cnt {
font-size: 70px;/*字体大小*/
bottom: 100px;/*定位*/
position: absolute;
color: white;
}
</style>
如有错误,欢迎指出。