【微信小程序】开发实战 之 「视图层」WXML & WXSS 全解析
在《微信小程序开发实战 之 「配置项」与「逻辑层」》中我们详细阐述了小程序开发的程序和页面各配置项与逻辑层的基础知识。下面我们继续解析小程序开发框架中的「视图层」部分。学习完这两篇文章的基础知识,动手开发一个简单的小程序应用已经不成问题了。
视图层
框架中视图层以给定的样式展现数据并反馈事件给逻辑层。
视图层由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。
微信小程序为开发者提供的组件分为常用组件和高级组件两个大类,其中常用组件包括视图容器、基础内容、表单、互动操作、页面导航。高级组件包括媒体、地图、画布、客服会话组件。
对于这些组件的常规使用方法,我们可以参考微信官方提供的小程序开发者文档。