本节书摘来自华章出版社《微信小程序:开发入门及案例详解》一 书中的第2章,第2.4节,作者李骏 边思,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.4 框架页面文件
小程序中一个框架页面包含4个文件,同一框架页面的这4个文件必须具有相同的路径与文件名,进入小程序时或页面跳转时,小程序会根据app.json配置的路径找到对应的资源进行渲染。
◇ .js文件:页面逻辑文件,必要项。
◇ .wxml文件:页面结构文件,必要项。
◇ .wxss文件:页面样式文件。
◇ .json文件:页面配置文件。
与框架主体文件相比框架页面文件多了一种页面结构文件,其余3个文件和框架主体文件的功能类同,下面我们一一讲解每个文件作用。
2.4.1 页面配置文件
我们首先讲解页面配置文件,页面配置文件和框架配置文件一样,是一个json文件,与框架配置文件不同的是,页面配置文件是非必要存在的,同时页面配置文件的配置项只有window,控制当前页面的窗口表现,window的属性和app.json一致。渲染页面时,页面中的window配置项会覆盖app.json中的相同配置项。
由于页面的.json只能配置window相关属性,编写时只需直接写出属性,不用写window这个键,如下所示:
{
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "我的页面",
"backgroundColor": "#efefef",
"backgroundTextStyle": "light"
}
2.4.2 页面逻辑文件(JavaScript)
页面逻辑文件主要功能有:设置初始化数据,注册当前页面生命周期函数,注册事件处理函数等。小程序的逻辑层文件是JavaScript文件,所有的逻辑文件,包括app.js,最终将会打包成一个js文件,在小程序启动时运行,直到小程序销毁,类似于ServiceWorker,所以逻辑层也称为App Service。
在小程序中,每个逻辑文件有独立的作用域,并具备模块化能力。由于JavaScript逻辑文件是运行在纯JavaScript引擎中而并非运行在浏览器中,一些浏览器提供的特有对象,如document、window等,在小程序中都无法使用,同理,一些基于document、window的框架如:jQuery、Zepto都不能在小程序中使用,同时我们不能通过操作DOM改变页面,这时需要我们将面向DOM操作的编程思路转化为数据绑定和事件响应。
1.注册页面
在页面逻辑文件中需要通过Page()函数注册页面,指定页面的初始数据、生命周期函数、事件处理函数等,参数为一个Object对象,其属性如下:
◆ data:页面的初始数据,数据格式必须是可转成JSON格式的对象类型。当页面第一次渲染时,data会以JSON的形式由逻辑层传至渲染层,渲染层可以通过WXML对数据进行绑定。
◆ onLoad:生命周期函数,页面加载时触发。一个页面只会调用一次,接受页面参数,可以获取wx.navigateTo、wx.redirectTo以及中的query参数。
◆ onShow:生命周期函数,页面显示时触发。每次打开页面都会调用一次。
◆ onReady:生命周期函数,页面初次渲染完成时触发。一个页面生命周期中只会调用一次,代表当前页面已经准备妥当,可以和视图层进行交互。一些对界面的设置,操作需要在页面准备妥当后调用,如wx.setNavigationBarTitle需要在onReady之后设置,详细可以参考后面的“页面生命周期”小节。
◆ onHide:生命周期函数,页面隐藏时触发。
◆ onUnload:生命周期函数,页面卸载时触发。
◆ onPullDownRefresh:页面相关时间处理函数,用户下拉时触发。使用时需要将app.json配置中window的enablePullDownRefresh属性设置为true。当处理完数据刷新后,可以调用wx.stopPullDownRefresh方法停止当前页面的下拉刷新。
◆ onReachBottom:页面上拉触底事件的处理函数。
◆ 其他:开发者可以添加任意的函数或数据到Object参数中,可以用this访问这些函数和数据。示例代码如代码清单2-2所示:
代码清单2-2 页面逻辑文件
// 获取app实例
var app = getApp();
Page( {
data : { // 页面初始化数据
count : 0
},
onLoad : function() {
// 页面加载时执行
},
onShow : function() {
// 页面打开时执行
console.log( app.globalData );
},
onReady : function() {
// 页面初次渲染完成执行,一个页面只会调用一次
},
onHide : function() {
// 页面隐藏时执行
},
onUnload : function() {
// 页面卸载时执行
},
onPullDownRefresh : function() {
// 下拉刷新时执行
},
onReachBottom : function() {
// 下拉触底时执行
},
// 自定义函数,可与渲染层中的组件进行事件绑定
countClick : function() {
// 触发视图层重新渲染
this.setData( {
count : this.data.count + 1
} );
},
// 自定义数据
customData : {
name : '微信'
}
} );
页面的生命周期函数比小程序的生命周期函数略微复杂一点,弄懂其执行顺序能避免在不恰当的生命周期函数中调用还未创建的对象或方法,小程序框架以栈的形式维护了当前的所有页面,当发生路由切换时,页面栈和生命周期函数的关系如下:
◆ 小程序初始化:默认页面入栈,依次触发默认页面onLoad、onShow、onReady方法。
◆ 打开新页面:新页面入栈,依次触发新页面onLoad、onShow、onReady方法。
◆ 页面重定向:当前页面出栈并卸载,触发当前页面onUnload方法,新页面入栈,触发新页面onLoad、onShow、onReady方法。
◆ 页面返回:页面不断出栈并卸载,触发当前弹出页面onUnload方法,直到返回目标页面,新页面入栈,触发新页面onShow方法。
◆ Tab切换:当前页面出栈但不卸载,仅触发onHide方法,新页面入栈,如果当前页面是新加载的,触发onLoad、onShow、onReady方法,如果当前页面已加载过,仅触发onShow方法。
◆ 程序从前台到后台:触发当前页面onHide方法,触发App onHide方法。
◆ 程序从后台到前台:触发小程序onShow方法,触发页面onShow方法。
整体来说,如果页面在生命周期中,只会触发onShow和onLoad方法,只有加载和卸载时才会触发onLoad、onReady和onUnload方法,而触发页面卸载只有页面返回和页面重定向两种操作。页面生命周期函数的执行顺序不用死记硬背,开发过程中可以开启app.json中的debug模式,路由切换时,小程序、页面的生命周期函数执行顺序都会在控制台以info形式输出,在后面小节中我们将对页面生命周期作简单介绍。
2. 获取当前页面栈
有注册就有获取,getCurrentPages()函数便是用于获取当前页面栈的实例,页面栈以数组形式按栈顺序给出,第一个元素为首页,最后一个元素为当前页面。不要尝试修改页面栈,这会导致路由以及页面状态错误。
示例代码如下:
/* 获取页面栈 */
var pages = getCurrentPages();
/* 获取当前页面对象 */
var currentPage = pages[pages.length - 1];
3. 事件处理函数
页面对象中注册的函数可以和视图层中的组件进行绑定,当达到触发条件时,就会执行Page中定义的相应事件,这类自定义函数统称为事件处理函数。小程序中组件的事件分为通用事件和特殊事件,事件类型请参考后面2.4.3节中“事件”小节。
示例代码如下:
<view bindtap= "myevent ">点击执行逻辑层事件</view>
Page( {
myevent : function() {
console.log( '点击了view' );
}
} );
4. 触发视图层渲染
页面首次加载时,框架会结合初始化数据渲染页面,在逻辑层中则需主动调用Page.
prototype.setData()方法,而不能直接修改Page的data值,这样不仅无法触发视图层渲染,还会造成数据不一致。当 Page.prototype.setData()被调用时,会将数据从逻辑层发送到视图层触发视图层重绘,同时会修改Page的data值。setData()接受一个Object对象参数,方法会自动将this.data中的key对应的值变成Object参数中key对应的值。当Object参数key对应的值和this.data中key对应的值一致时,将不会触发视图层渲染。在项目中我们一定要保证视图层和逻辑层的数据一致。
Object参数的key值非常灵活,可以按数据路径的形式给出,如array[5].info、obj.key.subkey,并且这样使用时,不需要在this.data中预先定义。
示例代码如下:
<view>{{text}}</view>
<button bindtap="changeText">修改普通数据</button>
<view>{{object.subObject.objectText}}</view>
<button bindtap="changeObjectText">修改对象数据</button>
<view>{{array[0].arrayText}}</view>
<button bindtap="changeArrayText">修改数组数据</button>
<view>{{newField.newFieldText}}</view>
<button bindtap="addNewData">添加新字段</button>
Page( {
data : {
text : 'normal data',
object : {
subObject : {
objectText : 'object data'
}
},
array : [
{ arrayText : 'array data' }
]
},
changeText : function() {
this.setData( {
/* 普通索引 */
text : 'new normal data'
} );
},
changeObjectText : function() {
this.setData( {
/* 按路径索引 */
'object.subObject.objectText' : 'new object data'
} );
},
changeArrayText : function() {
this.setData( {
/* 按路径索引 */
'array[0].arrayText' : 'new array data'
} );
},
addNewData : function() {
this.setData( {
/* 修改一个已绑定,但未在data中定义的数据 */
'newField.newFieldText' : 'add new data'
} );
}
} );
5. 页面生命周期
页面的生命周期整体关系着页面视图层线程和页面逻辑层线程,注册页面时,Object参数中很多属性都是生命周期函数,这些函数的调用和页面生命息息相关,程序视图层线程和逻辑层线程关系如图2-8所示。
如图2-8,线程启动后视图层和逻辑层相互监听,当逻辑层线程触发onLoad、onShow方法后会把初始数据data传送给视图层线程,视图层完成第一次渲染后触发逻辑层onReady方法,代表页面已经准备妥当,之后我们便可通过setData方法主动触发视图层渲染。当页面被调往后台时,触发onHide方法,这时逻辑层线程并没有销毁,我们仍然可以通过代码控制视图层渲染,只是可能不会在界面上表现出来。当页面从后台回到前台时,触发onShow方法,最后当页面销毁时,触发onUnload方法。整体来看onLoad、onReady和onUnload方法在生命周期中只会调用一次,生命周期内显示、隐藏页面都是触发onShow和onHide方法,在路由方式中,只有页面重定向和页面返回会结束当前页面生命周期,当进入一个已加载的页面时只会触发onShow方法,不会触发onLoad和onReady方法。
2.4.3 页面结构文件(WXML)
WXML(WeiXin Markup Language)是框架设计的一套标记语言,用于渲染界面,WXML的渲染原理和React Native思路一致,通过一套标记语言,在不同平台被解析为不同端的渲染文件,如图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)运算
在{{}}内可以做一些简单的运算,支持的运算有三元运算、算数运算、逻辑判断、字符串运算,这些运算均符合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所示。
(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>
示例中页面只渲染最后一个。wx:elif和wx:else必须和wx:if配合使用,否则会导致页面解析出错,在项目中大家一定要注意。
(2)block wx:if
wx:if是一个控制属性,可以添置在任何组件标签上,但如果我们需要包装多个组件,又不想影响布局,这时就需要使用标签将需要包装的组件放置在里面,通过wx:if作判断。不是一个组件,仅仅是一个包装元素,页面渲染过程中不做任何渲染,由属性控制,如下所示:
<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,页面渲染了两个,结果如图2-12所示。
(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所示。
普通遍历中我们没必要修改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所示。
在本示例中,我们使用了<block/>
标签,和block wx:if一样,wx:for可以直接在<block/>
标签上使用,以渲染一个包含多个节点的结构块。
4. 模板
在项目过程中,常常会遇到某些相同的结构在不同的地方反复出现,这时可以将相同的布局代码片段放置到一个模板中,在不同的地方传入对应的数据进行渲染,这样能避免重复开发,提升开发效率。
(1)定义模板
定义模板非常简单,在内定义代码片段,设置的name属性,指定模板名称即可。如:
<template name="myTemplate">
<view>内容</view>
<view>{{content}}</view>
</template>
(2)使用模板
使用模板时,设置is属性指向需要使用的模板,设置data属性,将模板所需的变量传入。模板拥有自己的作用域,只能使用data属性传入的数据,而不是直接使用Page中的data数据,渲染时,标签将被模板中的代码块完全替换。
示例代码如下:
<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所示。
模板可以嵌套使用,如下所示:
<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再离开。
对于冒泡事件每个组件都是默认支持的,除上述事件之外的其他组件自定义事件如无特殊声明都是非冒泡事件,如:
的submit事件,的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/>
以外的代码直接拷贝到位置,整体来说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中的并没有被渲染,如图2-17所示。
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)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所示。
通过对比发现,import更适合引用模板定义文件,include更适合引入组件文件,在项目中大家可以根据特性灵活使用。
WXML虽然是一门新标签语言,但大部分规则和其他前端模板语言大同小异,本节WXML规则整体可分为数据绑定,事件机制,模板语法(条件渲染、列表渲染),页面引用(引用规则、模板),大家可以对比其他模板语言学习。
2.4.4 页面样式文件(WXSS)
WXSS(WeiXin Style Sheets)是基于CSS拓展的样式语言,用于描述WXML的组件样式,决定WXML的组件该怎么显示,它具有CSS的大部分特性,在CSS基础上WXSS拓展了尺寸单位、样式导入特性,对CSS选择器属性上做了部分兼容,目前官方文档没有给出WXSS具体具备CSS哪些特性,在开发过程中不能想当然地使用CSS属性,一定要使用iOS和Android真机进行调试,本小节主要讲述WXSS和CSS的不同点,后续布局章节会讲解CSS盒子模型布局相关属性,其余CSS属性大家可以参考W3C规范,在WXSS中我们甚至能使用一些兼容性写法,不过在开发过程中我们一定要开启开发者工具中“开启上传代码时样式文件自动补全”功能,这样小程序会自动补全其余一些样式的兼容性写法,保证在不同终端达到统一视觉效果。
1. 尺寸单位
CSS中原有尺寸单位在不同尺寸屏幕中不能完美实现元素按比例缩放,WXSS在此基础上拓展了两种尺寸单位:rpx(responsive pixel)和rem(root em),这两种尺寸单位都是相对单位,最终会被换算成px,使用rpx和rem布局页面能让页面界面在不同尺寸屏幕中按比例缩放。
(1)rpx
在渲染过程中rpx会按比例转化为px,WXSS规定屏幕宽度为750rpx,如在iPhone6中,屏幕宽度为375px,即750rpx=375px,那么在iPhone6中1rpx=0.5px。
(2)rem
同rpx一样,WXSS规定屏幕宽度为20rem,同样在iPhone6中,屏幕宽度为375px,即20rem=375px,所以在iPhone6中1rem=18.75px。
以常规设备为例,这些尺寸换算如表2-1所示。
在设计界面时,如果要实现尺寸自适应,设计师可以用iPhone6作为视觉标准。由于rpx和rem最终要被换算为px,在某些情况下可能会存在除不尽的情况,会导致界面中产生毛刺,这种情况大家要多留意、多测试以尽量避免这种情况。
2. 选择器
CSS选择器用于选择需要添加样式的元素,WXSS实现了CSS的部分选择器,使用规则和CSS一样,熟悉CSS的同学会很快上手,参见表2-2。
WXSS和CSS代码结构一样,一段样式前面是选择器,后面是以大括号括起来的样式组合,每个样式以分号结束,如下所示:
选择器 { 样式1; 样式2; }
选择器使用规则和CSS也是一致的,如下所示:
/* 选择所有class含有myClass的组件,并设置边框 */
.myClass { border : solid 1px #000; }
/* 选择所有view组件且class含有myClass的组件,并设置边框 */
view.myClass { border : solid 1px #000; }
/* 选择所有view组件中子节点class含有myClass的组件,并设置边框 */
view .myClass { border : solid 1px #000; }
/*
选所有class含有myContent组件中所有checkbox组件和radiobox组件,并设置它们的边框
*/
.myContent checkbox,
.myContent radiobox { boder : solid 1px #000; }
/* 选择所有view组件且class含有myClass的组件,在其后面插入新内容,内容为new content*/
view.myClass::after { content : 'new content' }
3.内联样式
同HTML一样,样式除了写在WXSS文件中,也可以通过设置style、class属性控制样式,一般静态样式可以统一写到class中,style样式会在运行时解析,如非特别需要,尽量避免将静态样式写入style,以免影响渲染速度,例如:
<!-- 通过style动态设置样式 -->
<view style="border:solid 1px #000;background-color:{{color}}"></view>
<!-- 通过class选择器设置样式 -->
<view class="myClassName1 myClassName"></view>
4. 样式导入
通常在项目中为了便于管理会将WXSS按职责拆分为多个文件,这时便需要@import语句在当前WXSS文件中导入其他WXSS文件,@import后写入需要导入WXSS文件的相对路径,用“;”表示语句结束,例如:
.common-view { border : solid 1px #000; }
@import "common.wxss";
.page-container { padding : 10px; }
至此,小程序框架页面相关的4个文件已介绍完成,大家对每个文件的功能、内容应该都有了一定了解,在本章最后一节中,我们将探讨小程序的模块化。