微信小程序:开发入门及案例详解
上QQ阅读APP看书,第一时间看更新

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规则整体可分为数据绑定,事件机制,模板语法(条件渲染、列表渲染),页面引用(引用规则、模板),大家可以对比其他模板语言学习。