开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。
本文要创建一个效果如下图的组件,由一个心形图片和右上角的数字构成。
1、创建自定义组件
(1)首先在根目录下新建文件夹components,components下新建like文件夹,like下新建index页面;
(2)在 index.json 文件中进行自定义组件声明:
{
"component": true
}
(3)在 index.wxml 文件中编写组件模板,在 index.wxss 文件中加入组件样式,它们的写法与页面的写法类似;
编写组件样式时,需要注意以下几点:
- 组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。
- 组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。
- 子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。
- 继承样式,如 font 、 color ,会从组件外继承到组件内。
- 除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。
#a {
} /* 在组件中不能使用 */
[a] {
} /* 在组件中不能使用 */
button {
} /* 在组件中不能使用 */
.a > .b {
} /* 除非 .a 是 view 组件节点,否则不一定会生效 */
(4)在index.js 文件中,需要使用 Component 来注册组件,并提供组件的属性定义、内部数据和自定义方法:
Component({
behaviors: [],
// 属性定义(详情参见下文)
properties: {
myProperty: { // 属性名
type: String,//类型(必填)
value: ''//属性初始值(选填),默认布尔值初始为false,数字初始为0
observer: function(newVal, oldVal, changedPath){
//属性值变化时的回调函数(选填),也可以写成在methods段中定义的方法名字符串,如'_propertyChange'
//newVal是新设置的数据,oldVal是旧数据
}
},
myProperty2: String // 简化的定义方式
},
data: {}, // 私有数据,可用于模板渲染
lifetimes: {
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached() { },
moved() { },
detached() { },
},
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached() { }, // 此处attached的声明会被lifetimes字段中的声明覆盖
ready() { },
pageLifetimes: {
// 组件所在页面的生命周期函数
show() { },
hide() { },
resize() { },
},
methods: {
onMyButtonTap() {
this.setData({
// 更新属性和数据的方法与更新页面数据的方法类似
})
},
// 内部方法建议以下划线开头
_myPrivateMethod() {
// 这里将 data.A[0].B 设为 'myPrivateData'
this.setData({
'A[0].B': 'myPrivateData'
})
},
_propertyChange(newVal, oldVal) {
}
}
})
下面具体实现。
- index.wxml:
<view class="container" bind:tap="onLike">
<image src="{{like?yes_url:no_url}}" />
<text>{{count}}</text>
</view>
组件中最好不要留有无意义的间距,例如文字的行间距,设置line-height为文字大小可消除行间距。
- index.wxss:
.container{
display: flex;
flex-direction: row;
/* 必须指定宽度,否则会出现移动 */
/* width:120rpx; */
padding:10rpx;
}
.container text{
font-size:24rpx;
font-family: "PingFangSC-Thin";//苹果手机的默认字体是“苹方”,而安卓是“思源”。
color: #bbbbbb;
line-height:24rpx;//用于消除文字的上下间距
position:relative;//相对定位
bottom:10rpx;
left:6rpx;
}
.container image{
width:32rpx;
height:28rpx;
}
- index.js:
Component({
properties: {
like: Boolean,
count: Number,
readOnly:Boolean
},
data: {
yes_url: 'images/like.png',
no_url: 'images/like@dis.png'
},
methods: {
onLike: function (event) {
if(this.properties.readOnly){
return
}
let count = this.properties.count
count = this.properties.like ? count - 1 : count + 1
this.setData({
count: count,
like: !this.properties.like
})
let behavior = this.properties.like ? 'like' : 'cancel'
this.triggerEvent('like', {
behavior: behavior
}, {})
}
}
})
properties中定义的属性是需要从外部,比如服务器获取的数据;
而data中的数据是从本地加载的,或者是不需要在外部改变的;
但是最终小程序会将properties和data中的数据指向同一个JavaScript对象。
2、使用自定义组件
在需要使用自定义组件的页面的json文件中定义:
{
"usingComponents": {
"like-cmp": "/components/like/index",
}
}
like-cmp是组件的名字。
然后在wxml文件中使用:
<like-cmp bind:like="onLike" like="{{like}}" count="{{count}}" />
自定义组件中的data里的数据是私有的,不能在外部更改,只能被组件自身的wxml文件使用;而properties中的属性可以在外部更改。
3、数据传递的流程
(1)数据从服务器传递到页面的js文件;
(2)通过setData将数据绑定到页面的wxml文件中;
(3)由于使用了自定义组件,数据通过设置组件属性值的方式传递到组件的wxml中。
4、自定义事件的激活与监听
https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html
如果在外部某个页面使用自定义组件时,要给该组件添加监听事件,则该事件的返回值里没有组件的数据。这就需要在自定义组件里的methods中的点击事件中触发一个自定义事件like。
methods: {
onLike: function (event) {
if(this.properties.readOnly){
return
}
let count = this.properties.count
count = this.properties.like ? count - 1 : count + 1
this.setData({
count: count,
like: !this.properties.like
})
let behavior = this.properties.like ? 'like' : 'cancel'
this.triggerEvent('like', {
behavior: behavior
}, {})
}
}
其中triggerEvent(’’,{},{})用来触发事件behavior,三个参数指定事件名、detail对象和事件选项;
这时,在外部使用自定义组件时就能得到组件中like属性的值,:
<like-cmp bind:like="onLike" class="like" like="{{like}}" count="{{count}}" />
like的值在event.detail中:
onLike:function(event){
let like_or_cancel = event.detail.behavior
},
5、组件的生命周期函数
最重要的生命周期是 created、attached、detached ,包含一个组件实例生命流程的最主要时间点。
created:在组件实例刚刚被创建时执行
attached:在组件实例进入页面节点树时执行
ready:在组件在视图层布局完成后执行
moved :在组件实例被移动到节点树另一个位置时执行
detached:在组件实例被从页面节点树移除时执行
error: 每当组件方法抛出错误时执行
6、属性值变化时的回调函数observer
属性值的改变情况可以使用 observer 来监听。目前,在新版本基础库中不推荐使用这个字段,而是使用 Component 构造器的 observers 字段代替,它更加强大且性能更好。
数据监听器详见:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/observer.html
比如某一自定义组件要监听来自服务器的数据index,如果index为0~9,要在index前加0,否则不变:
properties: {
index:{
type: Number,
observer:function(newVal, oldVal, changedPath){
if (newVal < 10) {
this.setData({
_index: '0' + newVal
})
}
}
}
},
/**
* 组件的初始数据, data 的值也会被页面绑定, 但data的值不可以从组件外部设置
*/
data: {
_index:String//用来接收改变后的index值
}
但是,千万不要在observer函数中修改自身的属性,否则就会无限递归。
应该重新设置一个变量:_index。
也可以这样写:
properties: {
index:{
type: Number,
observer:'func'
}
}
}
},
/**
* 组件的初始数据, data 的值也会被页面绑定, 但data的值不可以从组件外部设置
*/
data: {
_index:String//用来接收改变后的index值
}
methosd: {
func(newVal, oldVal, changedPath){
if (newVal < 10) {
this.setData({
_index: '0' + newVal
})
}
事件在组件中的传递:
如果组件里还有组件,即组件嵌套,则当点击页面的组件时,事件响应会从最底层的组件逐级向上传递,最后传给页面的响应函数。
7、组建的behavior行为
behavior可以实现组件的继承机制。
定义方式和组件一样,把组件Component关键字换成Behavior,可以把几个组件共有的属性、方法等放在一个behavior里.
定义Behavior:
let classicBehavior = Behavior({
properties: {
type:String,
img:String,
content:String
},
data: {
}
})
export { classicBehavior }
继承Behavior:
properties、data、methods、生命周期函数都可以被组件继承。
在需要继承Behavior的组件里导入:
import {classicBehavior} from '../classic-beh.js'
Component({
/**
* 组件的属性列表
*/
behaviors:[classicBehavior],//若要继承多个Behavior,以逗号分隔
properties: {
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
}
})
说明:
(1)组件继承符合一般继承规则,如果子类和父类有同名属性,子类属性会覆盖父类属性。
(2)多继承时,且子类没有同名属性而几个父类之间有同名属性是,写在 behaviors:[a, b, c]括号中最后一个会覆盖其他的。
(3)生命周期函数不会有覆盖情况,小程序会依次执行父类的生命周期函数,再执行子类的生命周期函数。
8、组件的hidden属性
当需要组件切换显示/隐藏时,可以使用wx:if条件渲染,也可以给组件加hidden属性
wx:if vs hidden
因为 wx:if 之中的模板也可能包含数据绑定,所以当 wx:if 的条件值切换时,框架有一个局部渲染的过程,因为它会确保条件块在切换时销毁或重新渲染。
同时 wx:if 也是惰性的,如果在初始渲染条件为 false,框架什么也不做,在条件第一次变成真的时候才开始局部渲染。
相比之下,hidden 就简单的多,组件始终会被渲染,只是简单的控制显示与隐藏。
一般来说,wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。
因此一般使用hidden更好。
但是在自定义组件里使用hidden无效。
所以为了让自定义组件也能使用hidden,可以在组件的properties里加入hidden属性,然后在组件的wxml外部的view标签内添加hidden属性:
properties: {
hidden:flase
},
<view hidden={{hidden}} class="classic-container">
<image src="{{img}}" class="classic-img"></image>
<image class='tag' src="images/essay@tag.png" />
<text class="content">{{content}}</text>
</view>
在使用该组件时:
wx:if 写法:
<movie-cmp wx:if="{{classic.type==100}}" img="{{classic.image}}" content="{{classic.content}}" />
hidden写法:
<movie-cmp hidden="{{classic.type!=100}}" img="{{classic.image}}" content="{{classic.content}}" />
注:如果使用hidden属性,组件不会完整的执行一次生命周期,例如组件生命周期的detach()函数不会触发。
所以如果要执行detach()函数,还应该使用wx:if
9、组件中样式的复用
如果几个组件有相同的样式,则可以通过@import的方式导入共有的样式,实现样式的复用。这是template里的做法。
例如:
@import "../common.wxss";
后面一定要加封号。
10、组件间的通信
父子组件间的基本通信方式有以下几种。
- WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9
开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。 - 事件:用于子组件向父组件传递数据,可以传递任意数据。
- 如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent
方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。
11、点击组件进行页面跳转
点击组件进行页面跳转的事件函数可以直接写在组件里,而不用写在Page页面里,这样就不用在组件和页面之间传递参数了。
例如对于组件book,其bindtap事件写在methods里:
methods: {
onTap(event){
const bid = this.properties.book.id
wx.navigateTo({
url:`/pages/book-detail/book-detail?bid=${bid}`
})
// 降低了组件的通用性
// 非常方便
// 服务于当前的项目 项目组件
//
}
}
12、组件wxml的slot(插槽)
https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html
在组件的wxml中可以包含 slot 节点,用于承载组件使用者提供的wxml结构。
默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用。
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: { /* ... */ },
methods: { /* ... */ }
})
此时,可以在这个组件的wxml中使用多个slot,以不同的 name 来区分。
<!-- 组件模板 -->
<view class="wrapper">
<slot name="before"></slot>
<view>这里是组件的内部细节</view>
<slot name="after"></slot>
</view>
使用时,用 slot 属性来将节点插入到不同的slot上。
<!-- 引用组件的页面模板 -->
<view>
<component-tag-name>
<!-- 这部分内容将被放置在组件 <slot name="before"> 的位置上 -->
<view slot="before">这里是插入到组件slot name="before"中的内容</view>
<!-- 这部分内容将被放置在组件 <slot name="after"> 的位置上 -->
<view slot="after">这里是插入到组件slot name="after"中的内容</view>
</component-tag-name>
</view>
其中,slot的样式可以写在页面的wxss里。
例如,将下图的tag组件后面加上数字:
组件wxml:
<view class="container tag-class ">
<slot name="before"></slot>
<text >{{text}}</text>
<slot name="after"></slot>
</view>
页面wxml:
<v-tag tag-class="{{tool.highlight(index)}}" text="{{item.content}}">
<text class="num" slot="after">{{'+'+item.nums}}</text>
</v-tag>
页面wxss中定义slot样式:
.num {
margin-left: 10rpx;
font-size: 22rpx;
color: #aaa;
}
13、外部样式类externalClass
有时,组件希望接受外部传入的样式类,比如从页面传入样式到组件。此时可以在 Component 中用 externalClasses 定义段定义若干个外部样式类。
组件的js文件:
Component({
externalClasses: ['tag-class']
})
组件wxml:
<view class="container tag-class ">
<slot name="before"></slot>
<text >{{text}}</text>
<slot name="after"></slot>
</view>
页面wxml:
<v-tag tag-class="ex-tag" text="{{item.content}}">
页面wxss:
.ex-tag {
background-color: #fffbdd;
}
注意:在同一个节点上使用普通样式类和外部样式类时,比如container和tag-class,两个类的优先级是未定义的,因此最好避免这种情况。
可以使用!important来使外部样式强制覆盖普通样式。
页面wxss:
.ex-tag {
background-color: #fffbdd !important;
}
14、区分同一页面下的多个相同组件的点击事件
当一个页面加载了多个相同的组件,当点击其中一个,需要一个id来判断用户点击了哪个组件。
在页面的XML中,给每个组件加入一个data-id:
<van-button data-id="{{item._id}}" size="small" type='primary' plain bind:click='viewItem'>详情</van-button>
这样在每个组件的点击事件中就可以取出该id,从而知道用户点击了哪个组件的按钮:
viewItem:function(event){
//console.log(event);
var id = event.currentTarget.dataset.id;
wx.navigateTo({
url: '../bookDetail/bookDetail?id='+id,
})
},