WXML(WeiXin Markup Language)是框架设计的一套标准语言,用于渲染界面,WXML的渲染原理和React Native思路一致,通过一套标记语言,在不同平台被解析为不用端的渲染文件,如图:
使用微信开发者工具时,在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
}
})
运行效果如下:
注意:组件属性为boolean类型时,不要直接写checked ="false",这样checked的值时一个false的字符串,转成boolean类型后代表为true,所以这种情况一定要使用关键字输出:checked="{{false}}"
2)运算
在{{}}内可以做一些简单的运算,支持的运算有三元运算、算术运算、逻辑判断、字符串运算,这些运算均符合JavaScript运算规则,示例如下:
Page({
data: {
showContent:false,
num1:1,
num2:2,
num3:3,
name:'weixin',
myObject:{
age:12
},
myArray:['arr1','arr2']
}
});
<view>{{showContent ?'显示文本':'不显示文本'}}</view>
<view>{{num1+num2}}+1+{{num3}}=?</view>
<view>{{"name:"+name}}</view>
<view>{{num3>0}}</view>
<view>{{myObject.age}}{{myArray[1]}}</view>
执行结果如下:
3)组合
data中的数据可以在模板再次组合成新的数据结构,这种组合常常在数组或对象中使用,数组组合比较简单,可以直接将值放置到数组某个下标下:
<view>{{[myValue,2,3,'stringtype']}}</view>
Page(
{
data:{
myValue:0
}
}
)
输出结果为:
对象组合有3种组合方式,这里我们以数据注入模板为例
第一种,直接将数据作为value值进行组合:
<template is="testTemp" data="{{name:myvalue1,age:myvalue2}}"></template>
Page(
{
data: {
myValue1: 'value1',
myValue2: 'value2'
}
}
)
最终组合成的对象为:{name:'vaue1',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}
如果一个组合中有相同的属性名时,后面的属性将会覆盖前面的属性,如:
<template is="testTemp" data="{{...myObj,key1:3}}"></template>
Page({
data:{
key1:1,
key2:2
}
});
最后的组合对象为:{key1:3,key2:2}
2.条件渲染
1)wx:if
除了简单的数据绑定,我们常常会使用逻辑分支,这时候可以使用wx:if="{{判断条件}}"来进行条件渲染,当条件成立时渲染该块代码:
<view wx:if="{{showContent}}">内容</view>
Page({
data:{
showContent:true /*若未false则不显示Content*/
}
});
执行结果如下:
和普通的编程语言一样,WML也支持wx:elif和wx:else,如:
<view wx:if="{{false}}">1</view>
<view wx:elif="{{false}}">2</view>
<view wx:else>3</view>
2)block wx:if
wx:if是一个控制属性,可以添置在任何组件标签上,但如果我们需要包装多个组件,又不想影响布局,这时就需要使用<block/>标签将需要包装的组件放置在里面,通过wx:if做判断。<block/>不是一个组件,仅仅是一个包装元素,页面渲染过程中不做任何渲染,由属性控制,如下所示:
<block wx:if="{{true}}">
<view>view组件</view>
<image></image>
</block>
3)wx:if与hidden
除了wx:if组件,也有了通过hidden属性控制组件是否显示,开发者难免有疑问,这两种方式该怎样取舍,这里我们整理了两种方式的区别:
- wx:if控制是否渲染条件块内的模板,当其条件值切换时,会触发布局渲染以确保条件快在切换时销毁或重新渲染。wx:if是惰性的,如果在初始渲染条件为false时,框架将生命也不做,在条件第一次为真时才局部渲染
- hidden控制组件是否显示,组件始终会被渲染,只是简单控制显示与隐藏,并不会触发重新渲染和销毁。
在频繁切换状态的场景中,会产生更大的消耗,这时尽量使用hidden;
在运行时条件变动不大的场景中我们使用wx:if,这样能保证页面有更高效的渲染,而不用把所有组件渲染出来。
3.列表渲染
1)wx:for
组件的wx:for控制属性用于遍历数组,重复渲染该组件,遍历过程中当前项的下标变量名默认为index,数组当前项变量名默认为item,如:
<view wx:for="{{myArray}}">
{{index}}:{{item}}
</view>
Page({
data:{
myArray:['value1','value2']
}
});
执行结果如下:
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'}
]
}
});
执行结果如下:
普遍遍历中我们没必要修改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]
]
}
});
执行结果如下:
在本示例中,我们使用了<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'
}
}
});
执行结果如下:
模板可以嵌套使用:
<template name="bTemplate">
<view>b tempalte content</view>
</template>
<template name="aTemplate">
<view>a template content</view>
<template is="bTemplate"/>
</template>
<template is="aTemplate"/>
执行结果如下:
注意:模板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 bindlongtap='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事件绑定特别像,上述代码执行后事件对象输出如下:
事件对象属性基本可以分为三类:BaseEvent、CustomEvent、TouchEvent
BaseEvent为基础事件对象属性,包括:
- type:事件类型
- timeStamp:事件生成时的时间戳,页面打开到触发所经过的毫秒数
- target:触发事件源组件(即冒泡开始的组件)的相关属性集合,属性如下:
- id:事件源组件的id
- tagName:事件源组件的类型
- dataset:事件源组件上由data-开头的自定义属性组成的集合
- currentTarget:事件绑定的当前组件的相关属性集合,属性如下:
- id:事件源组件的id
- tagName:当前组件的类型
- dataset:当前组件上由data-开头的自定义属性组成的集合
<canvas/>中的触摸事件不可冒泡,所以没有currentTarget
dataset是组件的自定义数据,通过这种方式可以将组件的自定义属性传递给逻辑层。书写方式为:以data-开头,多个单词由连字符“-”连接,属性名不能有大写(大写最终会被转为小写),最终在dataset中将连字符专程驼峰形式如:
Page({
myevent:function(e){
console.log(e.currentTarget.dataset);
}
});
<view bindtap='myevent' data-my-name="weixin" data-myAge="12">
dataset示例
</view>
执行结果如下:
CustomEvent为自定义事件对象(继承BaseEvent),只有一个属性:
- detail:额外信息,通常传递数组特殊信息
detail没有统一格式,在<form/>的submit方法中它是{"vaue":{},"formid":""},在<swiper/>的change事件中它是{"current":current},具体内容参考组件相关文档。
TouchEvent为触摸事件对象(继承BaseEvent)属性如下所示:
- touches:触摸事件,当前停留在屏幕中的触摸点信息的数组
- changedTouched:触摸事件,当前变化的触摸点信息的数组,如从无变有(touchstart)、位置变化(touchmove)、从有变无(touchend、touchcancel)
由于支持多点触摸,所有touched和changedTouched都是数组格式,每个元素为一个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文件后只接受模板定义,忽略模板定义之外的所有内容,而且使用过程中有作用域的概念,与improt相反,include则是引入文件中除<templaet/>以外的代码直接拷贝到<include/>位置,整体来说Import是引入模板定义,include是引入组件
1)import
<import/>的src属性是需要被引入文件的相对地址,<import/>引入会忽略引入文件中<template/>以外的内容,如下:
<import src="b.wxml"/>
<template is="bTemplate" data=""/> <!--使用b.wxml中定义的模板-->
<view>内容</view> <!--import引用时会被忽略-->
<temmplate name="bTemplate">
<view>b Template content</view>
</template>
<template is="bTemplate"/> <!--import引用时会被忽略-->
import引用有作用域概念,只能直接使用引入的模板,而不能使用间接引入的定义模板,如下例:
<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 conteng</view>
</template>
<template is="cTemplate"/> <!--import时被忽略-->
<template name="cTemplate">
<view>c template content</view>
</template>
2)include
include引入会将模板定义标签外的内容(含模板使用标签)直接赋值替换<include/>,我们基于上个案例进行修改,大家可以对比一下:
<include src="b.wxml"/>
<template is="bTemplate"/> <!--不能调用b.wxml中的模板-->
<template is="cTemplate"/> <!--不能调用c.wxml中的模板-->
<import src="c.wxml"/>
<view>b content</view> <!--不会被忽略-->
<template name="bTemplate">
<template is="cTemplate"/> <!--不会调用c.wxml中的模板,引用时已经被忽略-->
<view>b tempalte conteng{{name}}</view>
</template>
<template is="bTemplate" data="{{name}}"/> <!--没有被忽略,能正常调用自己文件中的模板-->
<template name="cTemplate">
<view>c template content</view>
</template>
Page({
data:{
name:2 /**将数据注入到b.wxml中 */
}
})
通过对比发现,Import更适合引用模板定义文件,include更适合引入组件文件,在项目中大家可以根据特性灵活使用。