查看原文
最近摸索了一下小米 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>
如有錯誤,歡迎指出。