4、小程序进阶一
事件处理
事件类型
事件对象
事件传参
事件冒泡和捕获
事件捕获
事件冒泡
阻止冒泡或捕获
组件化
创建组件
组件页面注册
组件全局注册
页面及组件样式
样式隔离
生命周期
组件所在页面的生命周期
Component函数
组件通信
数据传递
传递样式
传递事件
组件插槽
单插槽
多插槽
事件类型
事件对象
事件传参
事件冒泡和捕获
事件捕获
事件冒泡
阻止冒泡或捕获
组件化
创建组件
组件页面注册
组件全局注册
页面及组件样式
样式隔离
生命周期
组件所在页面的生命周期
Component函数
组件通信
数据传递
传递样式
传递事件
组件插槽
单插槽
多插槽
事件处理
小程序内的事件处理和JavaScript内的事件处理基本上没有什么太大的差异的,只不过有一些微小的变化,在小程序内,是经常需要和用户进行某种交互的,比如点击街面上某个按钮,或者某个区域,或者滑动了某个区域,那么这个时候,如果我们希望捕获到这些事件,就必须采取事件监听的方式来获取;
事件可以绑定在组件上,当事件发生时,就会支持逻辑层中对应的事件处理函数,同时,事件发生时,默认会给事件处理函数传递一个事件对象,在这个事件对象内,默认会携带一些信息;
事件通过bind/catch属性来将事件绑定在组件上,和普通的属性写法上一致的,以key/value的形式,从基础库1.5.0版本开始,可以在bind和catch后面加上一个冒号,同时,在当前页面Page构造器定义对应的事件处理函数,当用户触发了指定的事件,就会立即调用对应的事件处理函数进行相应的处理,并在在调用时,默认会传递一个事件对象给事件处理函数,所以每一个事件处理函数默认都可以接受一个事件对象的参数;
事件类型
对于事件类型来讲,其实在此之前学习过的一些组件,默认就有有一些自有的事件,比如input有bindinput、bindblur等事件,比如scroll-view有bindscrolltowpper、bindscrolltolower等事件,这些组件自身的事件属于组件专有事件,具体的还请查看官方文档;
那么在此处主要讨论公共事件,在小程序内,默认有七个公共事件,如下列表;
事件类型
|
触发条件
|
touchstart
|
手指触摸动作开始,即点击不松开
|
touchmove
|
手指触摸后移动,即点击不松开拖动
|
touchcancel
|
手指触摸被打断,如来电提醒,弹窗等
|
touchend
|
手指触摸动作结束,即松开
|
tap
|
手指触摸后马上离开,即点击
|
longpress
|
手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发该事件,tab事件将不被触发
|
longtap
|
手指触摸后,超过350ms再离开(推荐使用longpress)
|
事件对象
当某个事件触发时会产生一个事件对象,然后会直接将这个事件对象传递给回调函数,然后开发者,可以根据事件对象中的一些数据作出相应的处理,那么这个事件对象里面的属性其实是非常多的,常用如下;
type:字符串类型,表示此次事件的类型;
timeStamp:数字类型,表示事件生成时的时间戳;
traget:对象类型,表示触发事件的组件上的一些属性集合,可能存在冒泡,或者嵌套元素;
currentTarget:对象类型,当前组件的属性集合,可能存在冒泡,或者嵌套元素;
mark:对象类型,表示事件标记的数据;
这里唯一需要注意一点,就是target和currentTarget,如果存在一个嵌套组件,我们在外层组件监听对应的事件,内层组件并未监听对应的事件,但是如果我们点击内层组件,一样会触发事,那么此时,target就是内层组件,而currentTarget就是外层组件,所以currentTarget代表,触发事件的组件,而target表示发生事件的组件,如下示例;
<!-- pages/page/page.wxml -->
<view id="1" class="outer" bind:tap="EventFunction">
<view id="2" class="inner"></view>
</view>
/* pages/page/page.wxss */
.outer {
display: flex;
justify-content: center;
align-items: center;
background-color: red;
width: 300rpx;
height: 300rpx;
margin: 10px;
}
.inner {
background-color: black;
width: 100rpx;
height: 100rpx;
}
// pages/page/page.js
Page({
EventFunction(event) {
console.log(event.target)
console.log(event.currentTarget)
}
})
当点击内层组件时,结果如下;
{id: "2", offsetLeft: 43, offsetTop: 43, dataset: {…}}
{id: "1", offsetLeft: 0, offsetTop: 0, dataset: {…}}
- 注意:
所以,从结果来看,如果我们通过data-var_name给事件函数传参,那么推荐使用event.currentTarget来获取;
事件传参
对于小程序当全局类型当事件传参,有点独特,它是通过组件属性的方式来传递的,即通过data-var_name的形式给事件处理函数传参的,data-为固定格式,后面的参数为标识符,但是需要注意,它并非直接将这个数据通过实参的形式传递给事件处理函数,而是将这个数据存放在event对象内,默认会存放在target或者currentTarget的dataset对象中,但是由于上面说的target和currentTarget的问题,所以建议直接从currentTarget中的dataset对象获取传递过来的数据;
<!-- pages/page/page.wxml -->
<view class="outer" bind:tap="EventFunction" data-number="{{1}}" />
// pages/page/page.js
Page({
EventFunction(event) {
console.log(event.target.dataset)
console.log(event.currentTarget.dataset)
}
})
# 结果
{number: 1}
{number: 1}
事件冒泡和捕获
如果我们的组件之间存在嵌套关系,且嵌套间的组件都存在一个共同的事件,那么就可能会产生事件冒泡或者事件捕获,从外层向内层传递,称之为事件捕获,从内层向外层传递称之为事件冒泡;
事件捕获
事件捕获就是,从外层向内层延伸,层层触发对应的事件,那么对于事件捕获来讲,它的事件类型有点特殊,如果我们希望实现事件捕获,那么在组件上定义事件属性时,需要以capture-开头,如点击事件,则为capture-bindtab,如下;
<!-- pages/page/page.wxml -->
<view class="view1" capture-bind:tap="view1CaptureEvent">
<view class="view2" capture-bind:tap="view2CaptureEvent">
<view class="view3" capture-bind:tap="view3CaptureEvent"></view>
</view>
</view>
/* pages/page/page.wxss */
.view1 {
display: flex;
justify-content: center;
align-items: center;
background-color: red;
width: 600rpx;
height: 600rpx;
}
.view2 {
display: flex;
justify-content: center;
align-items: center;
background-color: blue;
width: 400rpx;
height: 400rpx;
}
.view3 {
background-color: green;
width: 200rpx;
height: 200rpx;
}
// pages/page/page.js
Page({
view1CaptureEvent() {
console.log('view1CaptureEvent')
},
view2CaptureEvent() {
console.log('view2CaptureEvent')
},
view3CaptureEvent() {
console.log('view3CaptureEvent')
}
})
当我们点击最内层的组件时,就会触发事件捕获,从外层向内层层层触发点击时,直到被点击的组件为止,如下,点击最内层的结果;
view1CaptureEvent
view2CaptureEvent
view3CaptureEvent
事件冒泡
事件冒泡就是,从内层向外层延伸,层层触发对应的事件,小程序在默认情况下就是事件冒泡的,所以,对于事件冒泡来讲,针对事件的定义,我们无需以capture-开头,如下;
<!-- pages/page/page.wxml -->
<view class="view1" bind:tap="view1CaptureEvent">
<view class="view2" bind:tap="view2CaptureEvent">
<view class="view3" bind:tap="view3CaptureEvent"></view>
</view>
</view>
/* pages/page/page.wxss */
.view1 {
display: flex;
justify-content: center;
align-items: center;
background-color: red;
width: 600rpx;
height: 600rpx;
}
.view2 {
display: flex;
justify-content: center;
align-items: center;
background-color: blue;
width: 400rpx;
height: 400rpx;
}
.view3 {
background-color: green;
width: 200rpx;
height: 200rpx;
}
// pages/page/page.js
Page({
view1CaptureEvent() {
console.log('view1CaptureEvent')
},
view2CaptureEvent() {
console.log('view2CaptureEvent')
},
view3CaptureEvent() {
console.log('view3CaptureEvent')
}
})
当我们点击最内层的组件时,就会触发事件冒泡,从外层向内层层层触发点击时,直到冒泡到最外层的组件为止,如下,点击最内层的结果;
view3CaptureEvent
view2CaptureEvent
view1CaptureEvent
阻止冒泡或捕获
如果我们,我们希望阻止事件捕获或者冒泡向外或者向内继续传递,那么我们可以使用catch来实现,其实很简单,就直接把事件监听属性名的bind替换为catch即可;
<!-- pages/page/page.wxml -->
<!-- 阻止事件冒泡 -->
<view class="view1" bind:tap="view1CaptureEvent">
<view class="view2" catch:tap="view2CaptureEvent">
<view class="view3" bind:tap="view3CaptureEvent"></view>
</view>
</view>
<!-- pages/page/page.wxml -->
<!-- 阻止事件捕获 -->
<view class="view1" capture-bind:tap="view1CaptureEvent">
<view class="view2" capture-catch:tap="view2CaptureEvent">
<view class="view3" capture-bind:tap="view3CaptureEvent"></view>
</view>
</view>
组件化
在小程序的开发中,我们使用的一些view、button、image等组件,都是小程序的内置组件,但是除了内置组件之外,我们其实也可以自定义组件,当我们想要开发一个较为复杂的页面时,如果希望将所有的逻辑全部都在这一个页面中处理完,会变得非常复杂,且代码可读性低下,不利于后续的维护和扩展;
但如果将这么一个较为复杂但页面,拆分成一个一个的小的功能模块,每个功能模块完成特定的功能,然后将其进行整合,这样整个页面的维护和扩展就会变得非常的方便,这就是组件化开发的思想,不仅可以将一个较大的程序实现解耦,还可以实现功能复用,极大的加快了开发进度,小程序在刚刚推出时是不支持组件化的,但是在基础库1.6.3之后,小程序开始支持自定义组件开发;
创建组件
小程序的组件和Vue的组件非常的相似,在小程序内,组件其实和页面的结构是一摸一样的,每一个组件内一样可以有最基础的四个文件,即wxml、wxss、js和json文件,每个组件都可以有自己独特的逻辑、样式和配置;
那么怎么区分组件和非组件呢,这就涉及到一个配置,在每个组件下面的json文件里面的的对象内最少有一个配置,即component,当它的值为true时,表示当前当页面为一个组件页面,这样,就区分开了组件和非组件之间的区别;
那么为了能够将组件和页面能够划分开来,以免造成混乱,所以我们一般将组件定义在项目根目录下面的一个components的目录下,然后以组件的名称创建一个文件夹,在该文件夹内创建相应的组件内容,如下示例;
[cce@doorta ~]# tree /usr/local/Project/wechat
├── app.js
├── app.json
├── app.wxss
├── components
│ └── section-info # section-info组件
│ ├── section-info.js
│ ├── section-info.json
│ ├── section-info.wxml
│ └── section-info.wxss
├── pages
│ └── index
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── project.config.json
├── project.private.config.json
└── sitemap.json
[cce@doorta ~]# cat /usr/local/Project/wechat/components/section-info/section-info.json
{
"component": true, # 设置当前页面为组件
"usingComponents": {}
}
组件页面注册
那么当我们的组件定义好了之后,想要使用这个组件,那么我们必须将这个组件注册到页面当中,注意,这里不是注册到小程序当中,而是注册到页面当中,也就是说,哪个页面想要使用哪个组件,那么就必须在这个页面里面注册指定的组件;
那么对于注册组件,就是在页面下面的json文件里面的对象内加入一个usingComponents的对象,key为组件的名称(可以不必和组件原名一致),value为组件的路径,组件路径并非组件的根目录,而是根目录下面的组件原名,如下示例;
[cce@doorta ~]# cat /usr/local/Project/wechat/pages/index/index.json
{
"usingComponents": {
"section-info": "/components/section-info/section-info"
}
}
那么当注册完成之后,对于组件的使用,也非常简单,和内置组件的使用方法一致,可以写单标签也可以写成双标签,如下示例;
<!--pages/index/index.wxml-->
<section-info/>
- 注意:
当自定义组件当中,也可以引用别的自定义组件,使用方法和上述一致,同时,在定义组件名时,官方不允许组件名以wx-开头;
组件全局注册
当一个组件,可能在很多个页面都要被引用时,我们其实也可以直接将这个组件在全局进行注册,直接在项目目录下面的app.json文件内的对象加入usingComponents对象即可,配置方法和页面的配置方法一致,至此,我们就可以在全局使用这个组件,所有页面都可以直接引用,如下示例;
[cce@doorta ~]# cat /usr/local/Project/wechat/app.json
{
"pages": [
"pages/index/index"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Weixin",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"usingComponents": { # 全局注册
"section-info": "/components/section-info/section-info"
}
}
页面及组件样式
对于页面和组件的样式我们在开发的过程中,要严格的注意它们之间的影响,主要有几点,第一,在组件内使用class作为样式选择器,值对组件内的wxml内的组件生效,对于引用的Page页面不生效;
第二,在组件内,其实我们可以使用ID选择器、属性选择器和标签选择器,但是极不推荐使用这三类选择器,它会直接影响Page页面中组件的样式,所以在小程序内,推荐使用class作为样式选择器;
第三,在页面内,如果我们使用标签选择器作为样式选择器,那么它会对这个页面所引用的组件产生影响,这一点要格外注意;
样式隔离
上面说了三点对于组件和页面之间的样式影响的注意问题,那么其实对于组件的页面的样式是否需要隔离,这个是可以配置的,在每一个组件内都有一个和页面一样的js文件,该文件主要是用来编写组件的一些逻辑的,在这个js文件内有一个Component的函数,它类似页面的Page函数,同样的传入一个对象,在这个对象内,我们可以加入一个options的对象,这个对象里面有一个属性,即styleIsolation,该属性就可以用来配置组件和样式是否隔离,它的取值常用的有三个,如下;
isolated:默认值,表示启用隔离,自自定义组件内外,使用class指定的样式将不会相互影响;
apply-shared:表示页面的wxss样式将影响自定义组件,但自定义组件中的wxss中指定的样式将不会影响页面;
shared:表示页面的wxss样式将影响自定义组件,同时自定义组件的wxss样式也将影响页面;
- 总结:
综上所述,在小程序内,不推荐使用ID选择器、标签选择器和属性选择器,推荐使用class类选择器,即使我们声明了样式隔离,也没什么太大的用处,所以对于样式隔离这个配置,了解即可;
生命周期
组件的生命周期指的是组件自身的一些钩子函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时,会被自动触发,其中最重要的生命周期有三个,即created、attached、detached,这三个生命周期的钩子函数,包含了一个组件实例的生命周期流程最主要的时间点;
组件的生命周期本身是可以直接写在组件的js文件内的Component函数里面的参数对象内的,但是自小程序基础库版本2.2.3起,组件的生命周期也可以在Component函数里面的参数对象内的lifetimes对象内进行声明,这也是目前推荐的编写方式,其优先级最高;
created:组件被创建,可以用来做网络请求初始化数据等;
attached:组件被添加到组件树当中,可以获取DOM信息;
ready:当组件在是涂层布局完成之后执行;
moved:当组件实例被移动到节点树另一个位置时执行;
detached:组件从组件树当中移除,可以做一些垃圾回收操作;
error:当组件方法抛出异常时执行;
组件所在页面的生命周期
在小程序内,组件除了有自己的生命周期之外,还有当组件加载到页面之后的一个生命周期,这些生命周期一共有三个,它们都在Component函数里面的参数对象内的pageLifetimes对象内进行声明,没什么太大的实际用途,了解即可;
hide:组件所在页面被展示时执行;
show:组件所在页面被隐藏时执行;
resize:组件所在页面尺寸发生变化时执行
Component函数
在小程序全局有一个App函数,在页面有一个Page函数,那么在组件中,也有一个Component实例,同样的,调用Component函数时,可以指定当前组件的属性、数据、方法等,那么一个Component实例的对象参数内,主要有几项,即properties、data、methods、options、externalClasses、observers、pageLifetunes和lefetimes,它们都是对象类型,此处仅做介绍不做过多的演示,如下;
properties:定义组件通信数据;
data:定时页面数据;
methods:自定义方法;
options:额外的配置选项;
externalClasses:引用外部样式;
observers:属性和数据监听;
pageLifetunes:组件在页面时的生命周期;
lefetimes:组件自身的生命周期;
组件通信
很多情况下,页面展示的一些数据、样式等数据,并不一定是在页面内写死的,那么对于这一点,组件也是一样的,那么当一个组件被引用到一个页面时,由于组件内的数据需要进一步渲染,那么怎么从页面将数据传递到组件呢,其实这一点就涉及到组件通信的问题;
其实对于小程序的组件通信和Vue的组件通信非常相似,但在小程序内,不仅可以向组件传递数据,还可以向组件传递样式、标签以及自定义事件;
数据传递
对于组件的数据,一般来源可以有两种,第一种是在js文件里面Component函数参数对象内的data对象,和页面是一样的,只不过页面是Page函数而组件是Component函数,第二种,就是由页面来传递,那么对于页面传递数据到组件,和Vue非常相似,我们只需要在引用组件时,在组件之上加入要传递的数据即可,以属性的方式加入到组件之上,属性名为标识符,值为数据;
那么对于组件来讲,我们需要在组件内的js文件里面的Component函数参数对象内加入一个properties对象,然后将需要接受的数据作为key即可,该key必须和组件之上的标识符一致,它的值是一个对象,该对象内,可以加入两个属性,第一个属性type,代表接受数据的类型,对于type的类型,可以有String、Number、Boolean、Object、Array和null类型,第二个属性value,表示默认值,如下示例;
<!--components/section-info/section-info.wxml-->
<text>{{title}}</text>
// components/section-info/section-info.js
Component({
properties: {
title: {
type: String,
value: "title默认值"
}
}
})
<!--pages/index/index.wxml-->
<section-info title="页面传递过来的title值"/>
传递样式
在小程序内,页面可以除了可以向组件传递数据之外,还可以向组件传递样式,但是由于这种东西其实用得不是非常多,所以在此不做过多的赘述,无太大的实际用途;
传递事件
在小程序内,组件向外传递事件,其实也和Vue非常的相似,假设,我们希望点击组件内的一个按钮,触发页面当中的一个事件处理函数,那么就涉及到自定义事件,那么这个时候,首先,我们需要在组件内部监听这个事件的点击,然后将这个事件,在组件的事件处理函数中,传递出去;
那么对于组件向外传递事件,需要用到一个this.triggerEvent的函数,该函数接受两个参数,第一个参数为事件名称,第二个为传递给页面事件处理函数的参数,那么在页面这边,我们就需要在组件上监听这个自定义事件了,使用bind:事件名称来监听指定的事件,从而触发对应的事件处理函数;
那么对于组件的事件就不再向页面那种编写模式了,在组件中,事件处理函数,我们需要写在methods里面,这其实就是Vue的Options API,如下示例;
<!--components/section-info/section-info.wxml-->
<button size="mini" type="primary" bind:tap="btnclick">触发事件</button>
// components/section-info/section-info.js
Component({
methods: {
btnclick() {
this.triggerEvent("custom_click")
}
}
})
<!--pages/index/index.wxml-->
<section-info title="页面传递过来的title值" bind:custom_click="click"/>
// pages/index/index.js
Page({
click(){
console.log(1)
}
})
那么在组件的事件处理函数中,调用this.triggerEvent函数来触发自定义事件,也支持参数的传递,建议用一个对象的形式传递到页面,那么在页面这一侧,我们可以使用event.detail属性拿到组件传递过来的参数,如下示例;
// components/section-info/section-info.js
Component({
methods: {
btnclick() {
this.triggerEvent("custom_click",{"name":"cce"})
}
}
})
// pages/index/index.js
Page({
click(e){
console.log(e.detail)
}
})
# 结果如下
{name: "cce"}
组件插槽
在开发中,我们会经常复用一个一个可复用的组件,同时,我们也可以通过properties实现间实现数据传递,让组件根据不同的数据展示不同的内容,但是这里有一个问题,就是组件就是一套模版,它里面的结构基本都是写死的、固定的;
比如,某些情况希望组件只显示一个<button>元素, 但又因为其他原因,希望组件只显示一个<img>元素,虽然这种需求我们可以通过现有的知识去实现,但是可能比较鸡肋,那么在这种场景下,小程序框架提供了和Vue框架一样插槽功能,让调用者(父组件)来决定,组件显示什么内容;
插槽(slot)是小程序为组件的封装者提供的一种可编程的能力,允许开发者在封装组件时,把不确定的、希望由开发者指定的部分定义为插槽,从更加简单的方式来理解,其实我们可以把插槽认为是组件封装期间,预留的占位符;
单插槽
这里我们需要明确一个问题,插槽是预留在组件当中的一个占位符,所以插槽是依托于组件的,所以如果想要使用使用插槽,首先我们必须创建一个组件,然后在组件内预留一个插槽占位符,这个插槽占位符其实和Vue是一样的,在小程序内被称之为slot组件,Vue里面称之为slot元素,但是需要注意的是,小程序内的插槽是不支持默认值的,所以当没有插入任何东西时,插槽就不会进行渲染;
那么在我们的组件里面定义了插槽之后,想要在页面中使用也非常简单,直接将想要插入的内容,用自定义组件包裹起来即可,这个内容将会直接替换组件内的<slot>组件,如下示例;
<!--pages/index/index.wxml-->
<view>
<section_info>
<button size="mini" type="primary">触发事件</button>
</section_info>
</view>
{
"usingComponents": {
"section_info": "/components/section-info/section-info"
}
}
<!--components/section-info/section-info.wxml-->
<view>
<slot></slot>
</view>
多插槽
上述是单插槽的使用,那么对于一个复杂的组件中,可能存在多个插槽,那么对于小程序来讲,如果想要使用多个插槽,我们需要给每一个插槽加上一个属性,即使name属性,给每一个插值命个名,然后在页面中引用这个组件时,在插入的组件上指定该组件插入到哪一个插槽内,对于这个指定的方式也非常简单,直接在内容的组件上加入一个slot属性即可,其值为需要插入的插槽的name值;
同时,小程序的默认配置是不支持多插槽的,所以如果我们希望使用多个插槽,还需要在组件的js文件内的Component函数的对象参数里面的options对象加入multipleSlots属性,并设置该属性值为true,这样,我们才能够使用多插槽,如下示例;
<!--components/section-info/section-info.wxml-->
<view>
<slot name="s1"></slot>
<slot name="s2"></slot>
</view>
// components/section-info/section-info.js
Component({
options: {
multipleSlots: true
}
})
<!--pages/index/index.wxml-->
<view>
<section_info>
<button slot="s1" size="mini" type="primary">插槽1</button>
<button slot="s2" size="mini" type="primary">插槽2</button>
</section_info>
</view>