开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。
本文要创建一个效果如下图的组件,由一个心形图片和右上角的数字构成。
(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对象。
在需要使用自定义组件的页面的json文件中定义:
{ "usingComponents": { "like-cmp": "/components/like/index", } }like-cmp是组件的名字。 然后在wxml文件中使用:
<like-cmp bind:like="onLike" like="{{like}}" count="{{count}}" />自定义组件中的data里的数据是私有的,不能在外部更改,只能被组件自身的wxml文件使用;而properties中的属性可以在外部更改。
(1)数据从服务器传递到页面的js文件; (2)通过setData将数据绑定到页面的wxml文件中; (3)由于使用了自定义组件,数据通过设置组件属性值的方式传递到组件的wxml中。
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 },最重要的生命周期是 created、attached、detached ,包含一个组件实例生命流程的最主要时间点。
created:在组件实例刚刚被创建时执行 attached:在组件实例进入页面节点树时执行 ready:在组件在视图层布局完成后执行 moved :在组件实例被移动到节点树另一个位置时执行 detached:在组件实例被从页面节点树移除时执行 error: 每当组件方法抛出错误时执行
属性值的改变情况可以使用 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 }) }如果组件里还有组件,即组件嵌套,则当点击页面的组件时,事件响应会从最底层的组件逐级向上传递,最后传给页面的响应函数。
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)生命周期函数不会有覆盖情况,小程序会依次执行父类的生命周期函数,再执行子类的生命周期函数。
当需要组件切换显示/隐藏时,可以使用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
如果几个组件有相同的样式,则可以通过@import的方式导入共有的样式,实现样式的复用。这是template里的做法。 例如:
@import "../common.wxss";后面一定要加封号。
父子组件间的基本通信方式有以下几种。
WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。事件:用于子组件向父组件传递数据,可以传递任意数据。如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。点击组件进行页面跳转的事件函数可以直接写在组件里,而不用写在Page页面里,这样就不用在组件和页面之间传递参数了。 例如对于组件book,其bindtap事件写在methods里:
methods: { onTap(event){ const bid = this.properties.book.id wx.navigateTo({ url:`/pages/book-detail/book-detail?bid=${bid}` }) // 降低了组件的通用性 // 非常方便 // 服务于当前的项目 项目组件 // } }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; }有时,组件希望接受外部传入的样式类,比如从页面传入样式到组件。此时可以在 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; }当一个页面加载了多个相同的组件,当点击其中一个,需要一个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, }) },