在《微信小程序開發實戰 之 「配置項」與「邏輯層」》中我們詳細闡述了小程序開發的程序和頁面各配置項與邏輯層的基礎知識。下面我們繼續解析小程序開發框架中的「視圖層」部分。學習完這兩篇文章的基礎知識,動手開發一個簡單的小程序應用已經不成問題了。
視圖層
框架中視圖層以給定的樣式展現數據並反饋事件給邏輯層。
視圖層由WXML(WeiXin Markup language)與WXSS(WeiXin Style Sheet)編寫,由組件來進行展示,組件是視圖層的基本組成單元。
微信小程序提供了視圖窗口、基礎內容、表單組件、導航、媒體、地圖、畫布、開放能力等十余類數十個組件。關於組件的種類和用法,我們可以參考小程序開發者文檔中的組件部分。后續我們一起在一些開發實例中對組件用法進行解析。歡迎關注。
於微信小程序而言,視圖層就是所有.wxml文件與.wxss文件的集合。
- .wxml文件用於組織頁面的結構;
- .wxss文件用於編寫頁面的樣式;
WXML詳解
WXML是MINA框架設計的一套類似於HTML的標簽語言,與基礎組件、事件系統一起構建頁面的結構,保存在.wxml文件中。
WXML目前具有數據綁定、列表渲染、條件渲染、模板、引用及事件綁定的能力。下面我們通過一些簡單的例子具體學習感受一下WXML的這些能力。
數據綁定
在.wxml文件中動態顯示的數據均來自對應頁面的.js文件中的Page方法的data對象。數據綁定使用Mustache(中文翻譯作“胡子”)語法(即“雙大括號{{}}”)將變量包括起來。
數據綁定有多種用法,可以簡單的用於表現數據,也可以用在組件屬性、控制屬性中,還可以進行運算、組合構成新的數據。
-
表現數據
直接用來呈現動態數據:
<!--wxml--> <view> {{content}} </view> //page.js Page({ data:{ content: 'Hello MINA !' } })
-
組件屬性
用在標簽自身的屬性值中,需要加雙引號:
<!--wxml--> <view id="res-{{id}}"> {{content}} </view> //page.js Page({ data:{ content: 'Hello MINA !', id: 0 } })
-
控制屬性
用於控制語句的條件判斷中,也需要加雙引號:
<!--wxml--> <view wx:if="{{condition}}"> </view> //page.js Page({ data:{ condition: false } })
-
簡單運算
可以在{{}}內進行簡單的運算,包括三元運算、算數運算、邏輯判斷、數據路徑運算等。
三元運算:
<!--wxml-->
<view hidden="{{flag ? true : false}}">Hidden</view>
算術運算:
<!--wxml--> <view>{{a+b}}+{{c}}+d</view> //page.js Page({ data:{ a:1, b:2, c:3 } })
//結果:view中的內容為3+3+d
邏輯判斷:
<view wx:if="{{count > 1}}"></view>
字符串運算:
<!--wxml--> <view>{{"Hello" + name}}</view> //page.js Page({ data:{ name: "World !" } })
路徑運算:
<!--wxml--> <view>{{obj.key}} {{array[0]}}</view> //page.js Page({ data:{ obj:{ key: 'Hello' }, array:['World'] } })
-
組合綁定
在Mustache內直接進行組合,構成新的對象或數組。
數組:
<!--wxml--> <view wx:for="{{[0,1,2,3,four]}}">{{item}}</view> //page.js Page({ data:{ four: 4 } }) //最終組合成數組[0,1,2,3,4]
對象:
<!--wxml--> <template is="objCombine" data="{{for:a , bar:b}}"></template> //page.js Page({ data:{ a: 1, b: 2 } }) //最終組合成的對象是{for:1 , bar:2}
也可以用「擴展運算符」“...”來展開對象:
<!--wxml--> <template is="objCombine" data="{{...obj1 , ...obj2 , e: 5}}"></template> //page.js Page({ data:{ obj1: { a: 1, b: 2 }, obj2: { c: 3, d: 4 } } }) //最終組合成的對象是{a: 1 , b: 2 , c: 3 , d: 4 , e: 5}
如果對象的key和value相同,也可以間接的表示:
<!--wxml--> <template is="objCombine" data="{{foo, bar}}"></template> //page.js Page({ data:{ foo: 'my-foo', bar: 'my-bar' } }) //最終組合成的對象是{foo:'my-foo' , bar:'my-bar'}
需要注意,上述方式可以隨意組合,但如果變量名相同,后面的對象會覆蓋前面的對象。
<!--wxml--> <template is="objCombine" data="{{...obj1 , ...obj2 , a , c: 7}}"></template> //page.js Page({ data:{ obj1: { a: 1, b: 2 }, obj2: { b: 3, c: 4 }, a: 6 } }) //最終組合成的對象是{a: 6 , b: 3 , c: 7}
條件渲染
條件語句可用於.wxml中進行條件渲染。
wx:if
我們用 wx:if = "{{condition}}" 來判斷是否需要渲染該代碼塊。如:
<view wx:if="{{condition}}">條件為真</view>
也可以用wx:elif和wx:else來添加一個else塊:
<view wx:if = "{{len > 5}}"> 1 </view> <view wx:elif = "{{len > 2}}"> 2 </view> <view wx:else > 3> 3 </view>
wx:if 是一個控制屬性,需要將它添加到一個組件標簽上。如果想一次性控制多個組件標簽該如何操作呢?我們可以借助<block/>標簽來實現這一操作,也就是把wx:if作用在<block/>標簽上。如:
<block wx:if = "{{true}}">
<view> 標簽1 </view>
<view> 標簽2 </view>
</block>
需要注意<block />並不是組件,它只是一個包裝元素,不會在頁面中做任何渲染,只接受控制屬性。
wx:if 也是惰性的,如果在初始渲染時條件為false,框架什么也不做,在條件第1次為true時才開始局部渲染。
相比之下,hidden就簡單的多,組件始終會被渲染,只需要簡單的控制顯示OR隱藏。
那么什么情況下用hidden,什么情況下用wx:if呢?兩者並沒有明確的界限。一般來說,wx:if有較高的切換消耗,而hidden有更高的初始渲染消耗。因此,如果需要頻繁切換,用hidden更好;如果運行時條件改變頻率不大,則wx:if更合適。
列表渲染
列表語句(for循環)可用於.wxml中進行列表渲染。
wx:for
在組件上使用 wx:for 控制屬性綁定一個數組,即可使用數組中各項的數據重復渲染該組件。
數組默認下標變量名為index,數組默認元素變量名為item。示例如下:
<view wx:for ="{{items}}"> {{index}}: {{item.message}} </view> //page.js Page({ data:{ items:[{ message:'foo' },{ message:'bar' }] } }) //結果顯示為
0:foo
1:bar
可以使用 wx:for-item 指定數組元素的變量名,使用 wx:for-index 指定數組下標的變更名。如:
<view wx:for ="{{items}}" wx:for-index= "idx" wx:for-item= "itemName"> {{idx}}: {{itemName.message}} </view>
wx:for也可以嵌套使用:
<view wx:for ="{{items}}" wx:for-item= "i"> <view wx:for ="{{items}}" wx:for-item= "j"> <view wx:if = "{{i <= j}}"> {{i}} * {{i}} = {{i * j}} </view> </view> </view> //page.js Page({ data:{ items:[1,2,3,4,5,6,7,8,9] } })
也可以借助<block />標簽使用wx:for來控制多個組件的渲染:
<block wx:for= "{{1,2,3}}">
<view> {{index}}: </view>
<view> {{item}} </view>
</block>
如果列表中項目的位置會發生變動,或者有新的項目添加到列表中,並且希望列表中的項目保持自己的特征和狀態(如<input />中輸入的內容,<switch />的選中狀態),需要使用wx:key來指定列表中項目的唯一的標識符。
wx:key的值有兩種形式:
- 字符串,代表在for循環的array中item的某個property,該property的值需要是列表中唯一的字符串或數字,且不能動態改變。
- 保留關鍵字 *this,代表在for循環中的item本身。這種表示需要item本身是一個唯一的字符串或者數字。例如,當數據改變觸發渲染層重新渲染的時候,會校正帶有key的組件,框架會確保它們被重新排序,而不是重新創建,以確保使組件保持自身的狀態,並且提高列表渲染時的效率。
如果不使用wx:key,會報出一個警示(warning),如果明確知道該列表是靜態的,或者不必關注其順序,可以選擇忽略。
示例代碼如下:
<!--wx-key-demo.wxml--> <switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;"> {{item.id}} </switch> <button bindtap="switch"> Switch </button> <button bindtap="addToFront"> Add to the front </button> <switch wx:for="{{numberArray}}" wx:key="*this" style="display: block;"> {{item}} </switch> <button bindtap="addNumberToFront"> Add to the front </button> //wx-key-demo.js Page({ data: { objectArray:[ {id: 5, unique: 'unique_5'}, {id: 4, unique: 'unique_4'}, {id: 3, unique: 'unique_3'}, {id: 2, unique: 'unique_2'}, {id: 1, unique: 'unique_1'}, {id: 0, unique: 'unique_0'} ], numberArray:[1,2,3,4] }, switch: function(e){ const length = this.data.objectArray.length for(let i=0; i < length; i++){ const x = Math.floor(Math.random() * length) const y = Math.floor(Math.random() * length) const temp = this.data.objectArray[x] this.data.objectArray[x] = this.data.objectArray[y] this.data.objectArray[y] = temp } this.setData({ objectArray:this.data.objectArray }) }, addToFront:function(e){ const length = this.data.objectArray.length this.data.objectArray = [{id:length, unique: 'unique_'+length}].concat(this.data.objectArray) this.setData({objectArray:this.data.objectArray}) }, addNumberToFront:function(e){ this.data.numberArray = [this.data.numberArray.length + 1].concat(this.data.numberArray) this.setData({ numberArray:this.data.numberArray }) } })
(可以小程序開發工具中預覽效果,注意將wxml片段和j片段分別保存在.wxml文件和.js文件中)
頁面布局模板
WXML支持使用模版(template)。可以在模版中定義代碼片段,然后在別的地方引用。
- 定義模版
定義模版時,使用name屬性為模版命名。 在<template />標簽內定義模版代碼片段,下面是一段電影列表頁面顯示電影評級星數的模版:
<template name="starsTemplate"> <view class="stars-container"> <view class="stars"> <block wx:for = "{{stars}}" wx:for-item="i"> <image wx:if="{{i}}" src="/images/icon/star.png"></image> <image wx:else src="/images/icon/none-star.png"></image> </block> </view> <text class="star-score">{{score}}</text> </view> </template>
- 使用模版
使用is屬性,聲明需要使用的模版,還需要將模版所需要的data傳入,例如:
<!--wxml--> <template is="starsTemplate" data="{{stars:stars,score:average}}"/>
is 屬性還可以借助 Mustache 語法,來動態決定具體需要渲染哪個模版:
<template name="fir">
<view> first </view>
</template>
<template name="sec">
<view> second </view>
</template>
<block wx:for="{{[1,2,3,4,5]}}">
<template is="{{item % 2 == 0 ? 'fir' : 'sec'}}" />
</block>
模版擁有自己的作用域,它只能使用data傳入的數據。
文件引用
WXML提供兩種文件引用的方式:import 和 include。
- import
import 可以在當前文件中使用目標文件定義的template,例如,在 item.wxml 中定義了一個叫 item 的 template:
<!-- item.wxml-- > <template name = "item"> <view>{{text}}</view> </template>
在index.wxml中引用item.wxml,就可以使用item模版:
<import src = "item.wxml" /> <template is = "item" data = "{{text: 'forbar'}}" />
import是有作用域概念的,只會引用目標文件中定義的template,而不能引用目標文件嵌套import的template。比如,C import B , B import A,在C中可以使用B定義的template,在B中可以使用A定義的template,但是C不能使用A中定義的template。
- include
include可將目標文件除模版代碼(<template />)塊的所有代碼引入,相當於拷貝到include位置。
<!-- index.wxml --> <include src = "header.wxml" /> <view> body </view> <include src = "footer.wxml" /> <!-- header.wxml --> <view> header </view> <!-- footer.wxml --> <view> footer </view>
事件綁定
事件的定義
事件是視圖層到邏輯層的通信方式,可以將用戶的行為反饋到邏輯層進行處理。事件綁定到組件上,當觸發事件時,就會執行邏輯層中對應的事件處理函數。事件對象可以攜帶額外的信息,如id、dataset、touches。
事件的使用
小程序與用戶的交互,多數是通過事件來完成的。
首先,在組件中綁定一個事件處理函數。如下所示,事件綁定的屬性是bindtap,綁定的事件名稱是tapName,當用戶點擊該組件的時候會在該頁面對應的Page中找到相應的事件處理函數tapName。
//view組件的唯一標識id值為tapTest;自定義屬性hi,其值為MINA;綁定事件tapName。 <view id = "tapTest" data-hi = "MINA" bindtap = "tapName"> Click me </view>
(bindtap=bind+tap,即綁定的是冒泡事件tap。)
其次,要在頁面.js文件的Page定義中寫上相應的事件處理函數,參數是event。如下所示:
Page({ tapName: function(event){ console.log(event) } })
如果我們將上述兩段代碼分別放入.wxml和.js文件中,編譯之后我們就可以看到控制台的console中顯示的log信息,大致如下:
{ “type": "tap", "timeStamp": 1252, "target": { "id": "tapTest", "offsetLeft": 0, "offsetTop": 0, "dataset": { "hi": "MINA" } }, "currentTarget": { "id": "tapTest", "offsetLeft": 0, "offsetTop": 0, "dataset": { "hi": "MINA" } }, "touches": [{ "pageX": 30, "pageY": 12, "clientX": 30, "clientY": 12, "screenX": 112, "screenY": 151 }], "detail": { "x": 30, "y": 12 } }
事件詳解
微信小程序中的事件分為兩種:冒泡與非冒泡。
- 冒泡事件:當一個組件上的事件被觸發后,該事件會向父節點傳遞。
- 非冒泡事件:當一個組件上的事件被觸發后,該事件不會向父節點傳遞。
WXML中的冒泡事件僅有6個:
touchstart 手指觸摸; touchmove 手指觸摸后移動; touchcancel 手指觸摸動作被中斷,如來電提醒、彈窗;
touchend 手指觸摸動作結束; tap 手指觸摸后離開; longtap 手指觸摸后,超過350ms再離開
除上述事件之外的其他組件自定義事件都是非冒泡事件。
事件綁定的寫法跟組件屬性寫法相同,都是以key、value的形式:
key以bind或catch開頭,后面緊跟事件類型,如bindtap、catchtap。
value是一個字符串,需要在對應的Page中定義同名函數, 不然在事件被觸發時會報錯。
bind和catch的區別在於,bind事件綁定不會阻止冒泡事件向上冒泡,catch事件綁定可以阻止冒泡事件向上冒泡。
例如:
<view id = "outter" bindtap = "handleTap1"> out view <view id = "middle" catchtap = "handleTap2"> middle view <view id = "inner" bindtap = "handleTap3"> inner view </view> </view> </view>
上面的代碼片段中,點擊 id 為 inner 的view組件會先后觸發 handleTap3 和 handleTap2 ,因為事件會冒泡到 id 為 middle 的組件,而 middle 組件阻止了事件冒泡,不再向上傳遞。點擊 id 為 middle 的view組件會觸發 handleTap2,點擊 id 為 outter 的view組件會觸發 handleTap1。
邏輯層的事件處理函數會收到一個事件對象,這個事件對象具有的屬性如下:
- type,說明事件的類型,value類型為String;
- timeStamp,事件生成時的時間戳,value類型為Integer;
- target,觸發事件組件的一些屬性值集合,value類型為Object;
- currentTarget,當前組件的一些屬性值集合,value類型為Object;
- touches,觸摸事件,當前停留在屏幕中觸摸點信息的數組,value類型為Array;
- changedTouches,觸摸事件,當前變化的觸摸點信息的數組,value類型為Array;
- detail,額外的信息,value類型為Object;
其中,target是指觸發事件的源組件,是一個對象,它本身也有三個屬性:
- id,事件組件的id;
- tagName,事件組件的類型;
- dataset,事件組件上,由data-開頭的自定義屬性組成的集合;
而currentTarget是事件的當前組件。與target類似也是一個對象並且同樣具有上述的3個屬性 。
target和currentTarget的區別可以參考上面的代碼片段中,點擊 inner view 時,handleTap3 收到的事件對象 target 和 currentTarget 都是inner,而 handleTap2 收到的事件對象 target 就是 inner ,currentTarget 就是 middle。
dataset 在組件中可以定義數據,這些數據將通過事件傳遞給 App Service 。dataset 書寫方式以 data- 開頭,多個單詞由連字符 “-” 連接,不能有大寫(會自動轉換成小寫)。如data-element-type,最終在 event.target.dataset 中會將連字符轉成駝峰形式:elementType。
示例代碼如下:
//bindviewtap.wxml <view data-alpha-beta = "1" data-alphaBata = "2" bindtap = "bindViewTap" > DataSet Test </view> //bindViewtap.js Page({ bindViewTap:function(event){ event.target.dataset.alphaBeta == 1 // -會轉換成駝峰寫法 event.target.dataset.alphabeta == 2 // 大寫會轉換成小寫 } })
touches是一個觸摸點的數組。每個元素為一個Touch對象,具有如下屬性:
identifier ,觸摸點的標識符;
pageX,pageY ,距離文檔左上角的距離,文檔的左上角為原點,橫向為X軸,縱向為Y軸;
clientX,clientY,距離頁面可顯示區域(屏幕除去導航條)左上角的距離,橫向為X軸,縱向為Y軸。
changedTouches 數據格式同 touches。表示有變化的觸摸點,如 touchstart 從無變有,touchmove 位置變化,touchend、touchcancel 從有變無。
WXSS詳解
wxss是一套樣式語言,用於描述wxml的組件樣式。它將決定wxml的組件應該怎么顯示。
wxss的選擇器目前支持(“.class”、“#id”、“element”、“element,element”、“::after”、“::before”),而且本地資源無法通過wxss獲取,所以wxss中的樣式都是用的網絡圖片,或者base64。這樣對於某些前端開發者而言,會有所局限。
好在wxss具有css大部分特性,同時與css相比,wxss擴展的特性有:尺寸單位、樣式導入。
1、尺寸單位
wxss新增了針對移動端屏幕的兩種尺寸單位:rpx與rem。
rpx(responsive pixel):可以根據屏幕寬度進行自適應。規定屏幕寬為750rpx。
如在iphone6上,屏幕寬度為375px,共有750個物理像素,則750rpx = 375px = 750物理像素,1rpx=0.5px=1物理像素。
設備 rpx換算px(屏幕寬度/750) px換算rpx(750/屏幕寬度)
iphone5 1rpx = 0.42px 1px = 2.34rpx
iphone6/6s 1rpx = 0.5px 1px = 2rpx
iphnoe6s Plus 1rpx = 0.552px 1px = 1.81rpx
rem(root em):規定屏幕寬度為20rem;1rem=(750/20)rpx。
因此建議開發微信小程序時設計師可以用iphon6屏幕作為視覺稿的標准。
rpx計量的最大優勢在於750設計稿不需要進行任何轉換即可適配。750設計稿是多少就是多少。非750的設計稿則需要進行一次轉換,如640的設計稿就需要進行一次換算,在640設計稿中的 1rpx = 640/750rpx ,而在wxss中並不支持算術運算符,所以小程序的視覺設計稿盡量使用750來給出。
2、導入樣式
可以使用 @import 語句來導入外聯樣式表。@import 后跟需要導入的外聯樣式表的相對路徑,並用“;”表示語句結束。示例如下:
/**common.wxss**/ .small-p { padding: 5px; } /**app.wxss**/ @import "common.wxss"; .middle-p { padding:15px; }
3、內聯樣式
內聯樣式指的是框架組件上支持使用 style、class 屬性來控制組件的樣式:
style:style接收動態的樣式,在運行時會進行解析,所以應該避免將靜態的樣式寫到 style 中,以免影響渲染速度:
<view style = "color:{{color}};" />
class:用於指定樣式規則,其屬性值是樣式規則中類選擇器名(樣式類名)的集合,樣式類名不需要帶上“.”,如“.normal-view”樣式類的使用:
<view class = "normal_view" />
4、選擇器
wxss 目前支持的選擇有:
.class, 樣例:intro,選擇所有擁有 class="intro" 的組件。
#id , 樣例:#firstname,選擇擁有 id="firstname" 的組件。
element, 樣例:view,選擇所有view組件。
element,element 樣例:view,checkbox,選擇所有文檔的 view 組件和所有的 checkbox 組件。
::after 樣例:view::after,在view組件后面插入內容。
::before 樣例:view::before,在view組件前面插入內容。
5、全局樣式和局部樣式
定義在app.wxss 中的樣式稱為全局樣式,作用於每一個頁面。在Page 的.wxss文件中定義的樣式為局部樣式,只作用在對應的頁面,並會覆蓋app.wxss中相同的選擇器。
框架組件
組件是視圖層的基本構成單元。
一個組件通常包含“開始標簽”和“結束標簽”,組件由屬性來定義和修飾,放在“開始標簽”中。組件的內容則包含在兩個標簽之內。所有的組件與屬性都需要使用小寫字符。組件代碼樣式如下:
<tagname property = "value">
Content goes here...
</tagname>
所有組件都有共同的屬性:
屬性名 | 類型 | 描述 | 注釋 |
id | String | 組件的唯一標示符 | 保持整個頁面唯一 |
class | String | 組件的樣式類 | 在對應的wxss 中定義的樣式類 |
style | String | 組件的內聯樣式 | 可以動態設置的內聯樣式 |
hidden | Boolean | 組件是否顯示 | 所有組件默認顯示 |
data-* | Any | 自定義屬性 | 組件上觸發事件時會發送給事件處理函數 |
bind*/catch* | EventHandler | 組件的事件 | 詳見本文前面的wxml事件綁定部分 |
同時每一個組件也可以有自定義的屬性(稱為特殊獨有屬性),用於該組件的功能或樣式進行修飾。自定義屬性只支持以下幾種數據類型:
Boolean、Number、String、Array、Object、EventHandler。
微信小程序為開發者提供的組件分為常用組件和高級組件兩個大類,其中常用組件包括視圖容器、基礎內容、表單、互動操作、頁面導航。高級組件包括媒體、地圖、畫布、客服會話組件。
對於這些組件的常規使用方法,我們可以參考微信官方提供的小程序開發者文檔。