- 微信小程序:開發入門及案例詳解
- 李駿 邊思
- 5041字
- 2019-01-04 18:58:56
2.4.3 頁面結構文件(WXML)
WXML(WeiXin Markup Language)是框架設計的一套標記語言,用于渲染界面,WXML的渲染原理和React Native思路一致,通過一套標記語言,在不同平臺被解析為不同端的渲染文件,如圖2-9所示。

圖2-9 界面渲染示意圖
從圖中我們能看出,WXML語言最終會轉譯為宿主端對應的語言,所以WXML中所使用的標簽一定是小程序定義的標簽,不能使用自定義標簽,這樣才能保證頁面能被正確轉譯。使用微信開發者工具開發時,在WXML中編寫一些HTML標簽或自定義標簽仍然會被正常解析,這會給開發者造成一種小程序能直接支持HTML標簽的誤解。這是因為微信開發者工具內核是瀏覽器內核,同時小程序框架并沒對WXML中的標簽和WXSS中的內容進行強驗證,所以HTML和CSS能直接被解析,但這種不合法的WXML在手機端微信中是不能正常顯示的。開發過程中我們一定要拿真機進行測試,保證程序能正常運行。
WXML具有數據綁定、列表渲染、條件渲染、模板、事件等能力。
1.數據綁定
小程序中頁面渲染時,框架會將WXML文件同對應Page的data進行綁定,在頁面中我們可以直接使用data中的屬性。小程序的數據綁定使用Mustache語法(雙大括號)將變量或簡單的運算規則包起來,主要有以下幾種渲染方式。
(1)簡單綁定
簡單綁定是指我們使用Mustache語法(雙大括號)將變量包起來,在模板中直接作為字符串輸出使用,可作用于內容、組件屬性、控制屬性、關鍵字等輸出,其中關鍵字輸出是指將JavaScript中的關鍵字按其真值輸出。
示例代碼如下:
<! -- 作為內容 --> <view>{{content}}</view> <! -- 作為組件屬性 --> <view id="item-{{id}}" style="border:{{border}}">作為屬性渲染</view> <! -- 作為控制屬性 --> <view wx:if="{{showContent}}">作為屬性渲染</view> <! -- 關鍵字 --> <view>{{2}}</view> <checkbox checked="{{false}}"></checkbox> Page( { data : { border : 'solid 1px #000', id : 1, content : ’內容’, showContent : false } } );
運行效果如圖2-10所示。

圖2-10 簡單綁定渲染效果
注意:組件屬性為boolean類型時,不要直接寫checked="false",這樣checked的值是一個false的字符串,轉成boolean類型后代表為true,這種情況一定要使用關鍵字輸出:checked="{{false}}"
(2)運算
在{{}}內可以做一些簡單的運算,支持的運算有三元運算、算數運算、邏輯判斷、字符串運算,這些運算均符合JavaScript運算規則。我們利用如下示例為大家展示:
<! -- 三元表達式 --> <view>{{ showContent ? ’顯示文本’ : ’不顯示文本’}}</view> <! -- 算數運算符 --> <view>{{ num1 + num2 }} + 1 + {{ num3 }} = ? </view> <! -- 字符串運算 --> <view>{{ "name : " + name }}</view> <! -- 邏輯判斷 --> <view>{{ num3 > 0 }}</view> <! -- 數據路徑運算 --> <view>{{ myObject.age }} {{myArray[1]}}</view> Page( { data : { showContent : false, num1 : 1, num2 : 2, num3 : 3, name : 'weixin', myObject : { age : 12 }, myArray : ['arr1', 'arr2'] } } );
執行后界面如圖2-11所示。

圖2-11 運算示例
(3)組合
data中的數據可以在模板再次組合成新的數據結構,這種組合常常在數組或對象中使用。
數組組合比較簡單,可以直接將值放置到數組某個下標下:
<view>{{ [myValue, 2, 3, 'stringtype'] }}</view> Page( { data : { myValue : 0 } } );
最終頁面組合成的對象為[0, 2, 3, 'stringtype']。
對象組合有3種組合方式,這里我們以數據注入模板為例。
第一種,直接將數據作為value值進行組合:
<template is="testTemp" data="{{ name : myvalue1, age : myvalue2 }}"></template> Page( { data : { myValue1 : 'value1', myValue2 : 'value2' } } );
最終組合出的對象為{ name : 'value1', age : 'value2' }。
第二種,通過“…”將一個對象展開,把key-value值拷貝到新的結構中:
<template is="testTemp" data="{{ ...myObj1, key5 : 5, ...myObj2, key6 : 6 }}"> </template> Page( { data : { myObj1 : { key1 : 1, key2 : 2 }, myObj2 : { key3 : 3, key4 : 4 } } } );
最終組合成的對象為{ key1 : 1, key2 : 2, key5 : 5, key3 : 3 key4 : 4, key6 : 6 }
第三種,如果對象key和value相同,可以只寫key值:
<template is="testTemp" data="{{ key1, key2 }}"></template> Page( { data : { key1 :1, key2 : 2 } } );
這種寫法最后組合成的對象是{ key1 : 1, key2 : 2 }
上述3種方式可以根據項目靈活組合,要注意的是和js中的對象一樣,如果一個組合中有相同的屬性名時,后面的屬性將會覆蓋前面的屬性,如:
<template is="testTemp" data="{{…myObj, key1 : 3}}"></tamplate> Page({ data : { key1 : 1, key2 : 2 } });
示例中key1是重復的屬性,那么后面的屬性將會覆蓋前面的屬性,最終組合生成的對象為{ key1 : 3, key2 : 2 }。
2.條件渲染
(1)wx:if
除了簡單的數據綁定,我們常常會使用邏輯分支,這時候可以使用wx:if=”{{判斷條件}}”來進行條件渲染,當條件成立時渲染該代碼塊:
<view wx:if="{{showContent}}">內容</view> Page( { data : { showContent : false } } );
示例中view代碼塊將不會渲染,只有當showContent的值為true時才渲染。
和普通的編程語言一樣,WXML也支持wx:elif和wx:else,如:
<view wx:if="{{false}}">1</view> <view wx:elif="{{false}}">2</view> <view wx:else>3</view>
示例中頁面只渲染最后一個<view/>。wx:elif和wx:else必須和wx:if配合使用,否則會導致頁面解析出錯,在項目中大家一定要注意。
(2)block wx:if
wx:if是一個控制屬性,可以添置在任何組件標簽上,但如果我們需要包裝多個組件,又不想影響布局,這時就需要使用<block/>標簽將需要包裝的組件放置在里面,通過wx:if作判斷。<block/>不是一個組件,僅僅是一個包裝元素,頁面渲染過程中不做任何渲染,由屬性控制,如下所示:
<block wx:if="{{true}}"> <view>view組件</view> <image/> </block>
(3)wx:if與hidden
除了wx:if組件,也可以通過hidden屬性控制組件是否顯示,開發者難免有疑問,這兩種方式該怎樣取舍,這里我們整理了兩種方式的區別:
□wx:if控制是否渲染條件塊內的模板,當其條件值切換時,會觸發局部渲染以確保條件塊在切換時銷毀或重新渲染。wx:if是惰性的,如果在初始渲染條件為false時,框架將什么也不做,在條件第一次為真時才局部渲染。
□hidden控制組件是否顯示,組件始終會被渲染,只是簡單控制顯示與隱藏,并不會觸發重新渲染和銷毀。
綜合兩個渲染流程可以看出,由于wx:if會觸發框架局部渲染過程,在頻繁切換狀態的場景中,會產生更大的消耗,這時盡量使用hidden;在運行時條件變動不大的場景中我們使用wx:if,這樣能保證頁面有更高效的渲染,而不用把所有組件都渲染出來。
3.列表渲染
(1)wx:for
組件的wx:for控制屬性用于遍歷數組,重復渲染該組件,遍歷過程中當前項的下標變量名默認為index,數組當前項變量名默認為item,如:
<view wx:for="{{myArray}}"> {{index}}:{{item}} </view> Page( { data : { myArray : [ 'value1', 'value2' ] } } );
通過遍歷myArray,頁面渲染了兩個<view/>,結果如圖2-12所示。

圖2-12 列表渲染示例1
(2)wx:for-index和wx:for-item
index、item變量名可以通過wx:for-index、wx:for-item屬性修改,如:
<view wx:for="{{myArray}}" wx:for-index="myIndex" wx:for-item="myItem"> {{myIndex}}:{{myItem.name}} </view> Page( { data : { myArray : [ { name : 'value1' }, { name : 'value2' } ] } } );
渲染結果如圖2-13所示。

圖2-13 列表渲染示例2
普通遍歷中我們沒必要修改index、item變量名,當wx:for嵌套使用時,就有必要設置變量名,避免變量名沖突,下面我們遍歷一個二維數組:
<view wx:for="{{myArray}}" wx:for-index="myIndex" wx:for-item="myItem"> <block wx:for="{{myItem}}" wx:for-index="subIndex" wx:for-item="subItem" > {{subItem}} </block> </view> Page( { data : { myArray : [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] } } );
渲染效果如圖2-14所示。

圖2-14 列表渲染示例3
在本示例中,我們使用了<block/>標簽,和block wx:if一樣,wx:for可以直接在<block/>標簽上使用,以渲染一個包含多個節點的結構塊。
4.模板
在項目過程中,常常會遇到某些相同的結構在不同的地方反復出現,這時可以將相同的布局代碼片段放置到一個模板中,在不同的地方傳入對應的數據進行渲染,這樣能避免重復開發,提升開發效率。
(1)定義模板
定義模板非常簡單,在<template/>內定義代碼片段,設置<template/>的name屬性,指定模板名稱即可。如:
<template name="myTemplate"> <view>內容</view> <view>{{content}}</view> </template>
(2)使用模板
使用模板時,設置is屬性指向需要使用的模板,設置data屬性,將模板所需的變量傳入。模板擁有自己的作用域,只能使用data屬性傳入的數據,而不是直接使用Page中的data數據,渲染時,<template/>標簽將被模板中的代碼塊完全替換。
示例代碼如下:
<template name="myTemplate"> <view>內容</view> <view>{{content}}</view> <view>{{name}}</view> <view>{{myObj.key1}}</view> <view>{{key2}}</view> </template> <template is="myTemplate" data="{{content : ’內容’, name, myObj, ...myObj2}}"/> Page( { data : { name : 'myTemplate', myObj : { key1 : 'value1' }, myObj2 : { key2 : 'value2' } } } );
執行效果如圖2-15所示。

圖2-15 模板示例
模板可以嵌套使用,如下所示:
<template name="bTemplate"> <view>b tempalte content</view> </template> <template name="aTemplate"> <view>a template content</view> <template is="bTemplate"/> </template> <template is="aTemplate"/>
渲染結果如圖2-16所示。

圖2-16 嵌套使用模板
注意:模板is屬性支持數據綁定,在項目過程中我們可以通過屬性綁定動態決定使用哪個模板,如:
<template is="{{templateName}}" data="myData"/>
5.事件
WXML中的事件系統和HTML中DOM事件系統極其相似,也是通過在組件上設置“bind(或catch)+事件名”屬性進行事件綁定,當觸發事件時,框架會調用邏輯層中對應的事件處理函數,并將當前狀態通過參數傳遞給事件處理函數,由于小程序中沒有DOM節點概念,所以事件只能通過WXML綁定,不能通過邏輯層動態綁定。官方對WXML事件的定義如下:
□事件是視圖層到邏輯層的通訊方式。
□事件可以將用戶的行為反饋到邏輯層進行處理。
□事件可以綁定在組件上,當觸發事件時,就會執行邏輯層中對應的事件處理函數。
□事件對象可以攜帶額外信息,如id、dataset、touches。
(1)事件分類
事件分為冒泡事件和非冒泡事件:
□冒泡事件:當一個組件上的事件被觸發后,該事件會向父節點傳遞。
□非冒泡事件:當一個組件上的事件被觸發后,該事件不會向父節點傳遞。
有前端開發經驗的開發者應該對事件冒泡都有一定了解,當一個事件被觸發后,該事件會沿該組件向其父級對象傳播,從里到外依次執行,直到節點最頂層,這個是個非常有用的特性,通常用于實現事件代理,具體實現方案將在下文中具體討論。
WXML冒泡事件如下:
□touchstart:手指觸摸動作開始。
□touchmove:手指觸摸后移動。
□touchcancel:手指觸摸動作被打斷,如來電提醒、彈窗。
□touchend:手指觸摸動作結束。
□tap:手指觸摸后馬上離開。
□longtap:手指觸摸后,超過350ms再離開。
對于冒泡事件每個組件都是默認支持的,除上述事件之外的其他組件自定義事件如無特殊聲明都是非冒泡事件,如:<form/>的submit事件,<scroll-view/>的scroll事件,詳細信息請參考各組件文檔。
(2)事件綁定
在之前內容中,已經多次實現事件綁定,大家應該比較熟悉了,事件綁定的寫法和組件的屬性一樣,以key、value形式組織。
□key:以bind或catch開頭,然后跟上事件類型,字母均小寫,如:bindtap, catchtouchstart。
□value:事件函數名,對應Page中定義的同名函數。找不到同名函數會導致報錯。
綁定時bind事件綁定不會阻止冒泡事件向上冒泡,catch事件綁定會阻止冒泡事件向上冒泡。
冒泡示例如下:
<view bindtap="tap1"> view1 <view catchtap="tap2"> view2 <view bindtap="tap3"> view3 </view> </view> </view>
如上述示例中,點擊view3時會先后觸發tap3和tap2事件,由于view2通過catch阻止了tap事件冒泡,這時tap1將不會執行,點擊view2只觸發tap2,點擊view1只觸發tap1。
(3)事件對象
如果沒有特殊說明,當組件觸發事件時,邏輯層綁定該事件的事件處理函數會收到一個事件對象,如:
<view bindtap="myevent">view</view> Page( { myevent : function( e ) { console.log( e ); } } );
上述代碼中,myevent參數e便是事件對象,這和JavaScript事件綁定特別像。上述代碼執行后事件對象輸出如下:
{ "type":"tap", "timeStamp":6571, "target":{ "id":"", "offsetLeft":0, "offsetTop":0, "dataset":{ } }, "currentTarget":{ "id":"", "offsetLeft":0, "offsetTop":0, "dataset":{ } }, "detail":{ "x":15, "y":11 }, "touches":[ { "identifier":0, "pageX":15, "pageY":11, "clientX":15, "clientY":11 } ], "changedTouches":[ { "identifier":0, "pageX":15, "pageY":11, "clientX":15, "clientY":11 } ] }
事件對象屬性基本可分為三類:BaseEvent、CustomEvent、TouchEvent。
BaseEvent為基礎事件對象屬性,包括:
□type:事件類型。
□timeStamp:事件生成時的時間戳,頁面打開到觸發所經過的毫秒數。
□target:觸發事件源組件(即冒泡開始的組件)的相關屬性集合,屬性如下:
●id:事件源組件的id。
●tagName:事件源組件的類型。
●dataset:事件源組件上由data-開頭的自定義屬性組成的集合。
□currentTarget:事件綁定的當前組件的相關屬性集合,屬性如下:
●id:當前組件的id。
●tagName:當前組件的類型。
●dataset:當前組件上由data-開頭的自定義屬性組成的集合。
<canvas/>中的觸摸事件不可冒泡,所以沒有currentTarget。
dataset是組件的自定義數據,通過這種方式可以將組件的自定義屬性傳遞給邏輯層。書寫方式為:以data-開頭,多個單詞由連字符“-”連接,屬性名不能有大寫(大寫最終會被轉為小寫),最終在dataset中將連字符轉成駝峰形式,如:
<view bindtap="myevent" data-my-name="weixin" data-myAge="12"> dataset 示例 </view> Page( { myevent : function( e ) { console.log( e.currentTarget.dataset ); } } );
最后dataset打印出來為:
{ "myName" : "weixin", // 連字符被轉成駝峰 "myage" : "12" // 所有大寫字符都被轉為小寫 }
CustomEvent為自定義事件對象(繼承BaseEvent),只有一個屬性:
□detail:額外信息,通常傳遞組件特殊信息。
detail沒有統一的格式,在<form/>的submit方法中它是{"value":{}, "formId": ""},在<swiper/>的change事件中它是{"current": current},具體內容參考組件相關文檔。
TouchEvent為觸摸事件對象(繼承BaseEvent)屬性如下所示:
□touches:觸摸事件,當前停留在屏幕中的觸摸點信息的數組。
□changedTouches:觸摸事件,當前變化的觸摸點信息的數組,如從無變有(touchstart)、位置變化(touchmove)、從有變無(touchend、touchcancel)。
由于支持多點觸摸,所以touches和changedTouches都是數組格式,每個元素為一個Touch對象(canvas觸摸事件中為CanvasTouch對象)。
Touch對象相關屬性如下:
□identifier:觸摸點的標識符。
□pageX, pageY:距離文檔左上角的距離,文檔的左上角為原點,橫向為X軸,縱向為Y軸。
□clientX, clientY:距離頁面可顯示區域(屏幕除去導航條)左上角的距離,橫向為X軸,縱向為Y軸。
CanvasTouch對象相關屬性如下:
□identifier:觸摸點的標識符。
□x, y:距離Canvas左上角的距離,Canvas的左上角為原點,橫向為X軸,縱向為Y軸。
6.引用
一個WXML可以通過import或include引入其他WXML文件,兩種方式都能引入WXML文件,區別在于import引入WXML文件后只接受模板的定義,忽略模板定義之外的所有內容,而且使用過程中有作用域的概念。與import相反,include則是引入文件中除<template/>以外的代碼直接拷貝到<include/>位置,整體來說import是引入模板定義,include是引入組件。
(1)import
<import/>的src屬性是需要被引入文件的相對地址,<import/>引入會忽略引入文件中<template/>定義以外的內容,如下例中,在a.wxml引入b.wxml, b.wxml中<view/>和<template is="bTemplate"/>都被忽略,僅引入了模板的定義,在a.wxml中能使用b.wxml中定義的模板:
<import src="b.wxml"/> <template is="bTemplate" data=""/> <! -- 使用b.wxml中定義的模板 --> <view>內容</view> <! -- import引用時會被忽略 --> <template name="bTemplate"> <view>b template content</view> </template> <template is="bTemplate"/> <! -- import引用時會被忽略 -->
上述代碼中,a.wxml中的<view/>并沒有被渲染,如圖2-17所示。

圖2-17 import示例1
import引用有作用域概念,只能直接使用引入的定義模板,而不能使用間接引入的定義模板,如下例,在a.wxml中引入b.wxml, b.wxml再引入c.wxml,這樣a能直接使用b中定義的模板,b能使用c中定義的模板,但a不能使用c中的模板:
<import src="b.wxml"/> <template is="bTemplate"/> <template is="cTemplate"/> <! -- 不能直接調用c.wxml中的模板 --> <import src="c.wxml"/> <view>b content</view> <! -- import時被忽略 --> <template name="bTemplate"> <template is="cTemplate"/> <view>b tempalte content</view> </template> <template is="cTemplate"/> <! -- import時被忽略 --> <template name="cTemplate"> <view>c template content</view> </template>
渲染效果如圖2-18所示。

圖2-18 import作用域示例
(2)include
include引入會將模板定義標簽外的內容(含模板使用標簽)直接賦值替換<include/>,我們基于上個案例進行修改,大家對比一下:
<include src="b.wxml"/> <template is="bTemplate"/> <! -- 不能調用b.wxml中的模板 --> <template is="cTemplate"/> <! -- 不能調用c.wxml中的模板 --> <include src="c.wxml"/> <view>b content</view> <! -- 不會被忽略 --> <template name="bTemplate"> <template is="cTemplate"/> <! -- 不會調用c.wxml中的模板,引用時已被忽略 --> <view>b tempalte content{{name}}</view> </template> <template is="bTemplate" data="{{name}}"/> <! -- 沒有被忽略,能正常調用自己文件中的模板 --> <template name="cTemplate"> <view>c template content</view> </template> Page( { data : { name : '2' /*能將數據注入到b.wxml中*/ } } );
運行效果如圖2-19所示。

圖2-19 include引入示例
通過對比發現,import更適合引用模板定義文件,include更適合引入組件文件,在項目中大家可以根據特性靈活使用。
WXML雖然是一門新標簽語言,但大部分規則和其他前端模板語言大同小異,本節WXML規則整體可分為數據綁定,事件機制,模板語法(條件渲染、列表渲染),頁面引用(引用規則、模板),大家可以對比其他模板語言學習。
- Learning Java Functional Programming
- Mastering QGIS
- UI智能化與前端智能化:工程技術、實現方法與編程思想
- Python神經網絡項目實戰
- 高級C/C++編譯技術(典藏版)
- 用Flutter極速構建原生應用
- 小程序開發原理與實戰
- 蘋果的產品設計之道:創建優秀產品、服務和用戶體驗的七個原則
- 硬件產品設計與開發:從原型到交付
- IBM Cognos TM1 Developer's Certification guide
- App Inventor少兒趣味編程動手做
- SignalR:Real-time Application Development(Second Edition)
- Kotlin進階實戰
- Java從入門到精通(視頻實戰版)
- Android應用開發攻略