开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。
类似于页面,一个自定义组件由 json、wxml、wxss、js 这 4 个文件组成。
要编写一个自定义组件,首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true 可将这一组文件设为自定义组件):
{ "component": true }
也可以通过右键之间创建 componet 文件会直接创建 json、wxml、wxss、js 这 4 个文件。
同时,还要在 wxml 文件中编写组件模板,在 wxss 文件中加入组件样式。
使用已注册的自定义组件前,首先要在页面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:
{ "usingComponents": { "component-tag-name": "path/to/the/custom/component" } }
这样,在页面的 wxml 中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值。
<view> <!-- 以下是对一个自定义组件的引用 --> <component-tag-name></component-tag-name> </view>
自定义组件的 wxml 节点结构在与数据结合之后,将被插入到引用位置内。
注意,是否在页面文件中使用 usingComponents 会使得页面的 this 对象的原型稍有差异,包括:
父组件(页面)向子组件传递数据通过标签属性的方式来传递。
parent.wxml
<parent name="{{name}}" age="{{age}}"></parent>
parent.js
data:{ name: 'qw', age: 22 }
child.js
properties:{ name:{ type: String, value: 'qy' }, age: Number }
父组件向子组件传值以属性的形式,子组件以 properties 接收,并可指定数据类型 type 以及默认值 value。在 wxml 里可直接以 {{name}} 的形式使用,而在 js 中以 this.properties.name 获取。
子组件向父组件传递数据,通过事件的方式传递:
子组件向父组件传递数据使用 this.triggerEvent 方法,这个方法接受 3 个参数:
thiis.triggerEvent(‘myevent’, myEventDetail, myEventOption);
事件分为冒泡事件和非冒泡事件:
捕获阶段:捕获阶段位于冒泡阶段之前,且在捕获阶段中,事件到达节点的顺序与冒泡阶段恰好相反。需要在捕获阶段监听事件时,可以采用capture-bind、capture-catch关键字,后者将中断捕获阶段和取消冒泡阶段。
在下面的代码中,点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。
<view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2"> outer view <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4"> inner view </view> </view>
如果将上面代码中的第一个capture-bind改为capture-catch,将只触发handleTap2。
<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2"> outer view <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4"> inner view </view> </view>
child.js
//child.js methods: { changeName() { this.triggerEvent('changeName', {name: 'yj'}); } }
parent.wxml
<parent name="{{name}}" age="{{age}}" bindchangeName="changeName"></parent>
parent.js
//parent.js changeName(event) { console.log(event.detail); //{name: 'yj'} }
可在父组件里调用 this.selectComponent,获取子组件的实例对象。
调用时需要传入一个匹配选择器 selector,如:this.selectComponent(".my-component")。
selector 语法
selector类似于 CSS 的选择器,但仅支持下列语法。
// 父组件 Page({ data: {}, getChildComponent: function () { const child = this.selectComponent('.my-component'); console.log(child) } })
在上例中,父组件将会获取 class 为 my-component 的子组件实例对象,即子组件的 this 。
注意:默认情况下,小程序与插件之间、不同插件之间的组件将无法通过 selectComponent 得到组件实例(将返回 null)。如果想让一个组件在上述条件下依然能被 selectComponent 返回,可以自定义其返回结果(见下)。
需要自定义 selectComponent 返回的数据,可使用内置 behavior: wx://component-export
使用该 behavior 时,自定义组件中的 export 定义段将用于指定组件被 selectComponent 调用时的返回值。
// 自定义组件 my-component 内部 Component({ behaviors: ['wx://component-export'], export() { return { myField: 'myValue' } } })
父组件调用
const child = this.selectComponent(’#the-id’) // 等于 { myField: ‘myValue’ }
父组件获取 id 为 the-id 的子组件实例的时候,得到的是对象 { myField: ‘myValue’ } 。
slot 标签其实就是一个占位符(插槽),等到父组件调用子组件的时候再传递标签过来,最终这些被传递的标签就会替换 slot 插槽的位置。
默认情况下,一个组件的 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>
定义段 | 类型 | 是否必填 | 描述 |
---|---|---|---|
properties | Object Map | 否 | 组件的对外属性,是属性名到属性设置的映射表 |
data | Object | 否 | 组件的内部数据,和 properties 一同用于组件的模板渲染 |
observers | Object | 否 | 组件数据字段监听器,用于监听 properties 和 data 的变化 |
methods | Object | 否 | 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用 |
behaviors | String Array | 否 | 类似于mixins和traits的组件间代码复用机制 |
created | Function | 否 | 组件生命周期函数-在组件实例刚刚被创建时执行,注意此时不能调用 setData ) |
attached | Function | 否 | 组件生命周期函数-在组件实例进入页面节点树时执行) |
ready | Function | 否 | 组件生命周期函数-在组件布局完成后执行) |
moved | Function | 否 | 组件生命周期函数-在组件实例被移动到节点树另一个位置时执行) |
detached | Function | 否 | 组件生命周期函数-在组件实例被从页面节点树移除时执行) |
lifetimes | Object | 否 | 组件生命周期声明对象 |
pageLifetimes | Object | 否 | 组件所在页面的生命周期声明对象 |
定义段 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | 是 | 属性的类型 | |
optionalTypes | Array | 否 | 属性的类型(可以指定多个) 属性的类型可以为 String Number Boolean Object Array 其一,也可以为 null 表示不限制类型 多数情况下,属性最好指定一个确切的类型 |
value | 否 | 属性的初始值 | |
observer | Function | 否 | 属性值变化时的回调函数(不推荐使用,用 Component 构造器的 observers) |
Component({ properties: { min: { type: Number, value: 0 }, min: { type: Number, value: 0, observer: function(newVal, oldVal) { // 属性值变化时执行 } }, lastLeaf: { // 这个属性可以是 Number 、 String 、 Boolean 三种类型中的一种 type: Number, optionalTypes: [String, Object], value: 0 } } })
数据监听器可以用于监听和响应任何属性和数据字段的变化。
Component({ attached: function() { this.setData({ numberA: 1, numberB: 2, }) }, observers: { 'numberA, numberB': function(numberA, numberB) { // 在 numberA 或者 numberB 被设置时,执行这个函数 this.setData({ // this.data.sum 永远是 this.data.numberA 与 this.data.numberB 的和 sum: numberA + numberB }) } } })
数据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个监听器一次。
同时,监听器可以监听子数据字段。(① 使用通配符 **,监听所有子数据字段的变化;② 使用通配符 **可以监听全部 setData 。)
Component({ observers: { 'some.subfield': function(subfield) { // 使用 setData 设置 this.data.some.subfield 时触发 // (除此以外,使用 setData 设置 this.data.some 也会触发) subfield === this.data.some.subfield }, 'arr[12]': function(arr12) { // 使用 setData 设置 this.data.arr[12] 时触发 // (除此以外,使用 setData 设置 this.data.arr 也会触发) arr12 === this.data.arr[12] }, 'some.field.**': function(field) { // 使用通配符 **,监听所有子数据字段的变化 // 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发 // (除此以外,使用 setData 设置 this.data.some 也会触发) field === this.data.some.field }, '**': function() { // 每次 setData 都触发 }, } })
注意事项
behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。
每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。 每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior 。
组件实例刚刚被创建好时, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data 。 此时还不能调用 setData 。 通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。
在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。
在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发。
生命周期 | 参数 | 描述 |
---|---|---|
show | 无 | 组件所在的页面被展示时执行 |
hide | 无 | 组件所在的页面被隐藏时执行 |
resize | Object Size | 组件所在的页面尺寸变化时执行 |
Component({ pageLifetimes: { show: function() { // 页面被展示 }, hide: function() { // 页面被隐藏 }, resize: function(size) { // 页面尺寸变化 } } })
组件的的生命周期也可以在 lifetimes 字段内进行声明(这是推荐的方式,其优先级最高)。
Component({ lifetimes: { attached: function() { // 在组件实例进入页面节点树时执行 }, detached: function() { // 在组件实例被从页面节点树移除时执行 }, }, // 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容 attached: function() { // 在组件实例进入页面节点树时执行 }, detached: function() { // 在组件实例被从页面节点树移除时执行 }, // ... })
生命周期 | 参数 | 描述 |
---|---|---|
created | 无 | 在组件实例刚刚被创建时执行 |
attached | 无 | 在组件实例进入页面节点树时执行 |
ready | 无 | 在组件在视图层布局完成后执行 |
moved | 无 | 在组件实例被移动到节点树另一个位置时执行 |
detached | 无 | 在组件实例被从页面节点树移除时执行 |
error | Object Error | 每当组件方法抛出错误时执行 |
relations 定义段包含目标组件路径及其对应选项,可包含的选项见下表。
选项 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | String | 是 | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
target | String | 否 | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
<custom-ul> <custom-li> item 1 </custom-li> <custom-li> item 2 </custom-li> </custom-ul>
// path/to/custom-ul.js Component({ relations: { './custom-li-component': { type: 'child', // 关联的目标节点应为子节点 linked: function (target) { // 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后 console.log('[custom-ul] a child is linked: ', target) }, linkChanged: function (target) { // 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后 }, unlinked: function (target) { // 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后 } } }, methods: { _getAllLi: function () { // 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的 var nodes = this.getRelationNodes('./custom-li-component') console.log(nodes) } }, ready: function () { this._getAllLi() } })
// path/to/custom-li.js Component({ relations: { './custom-ul-component': { type: 'parent', // 关联的目标节点应为父节点 linked: function (target) { // 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后 console.log('child linked to ', target) }, linkChanged: function (target) { // 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后 }, unlinked: function (target) { // 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后 } } } })
注意:必须在两个组件定义中都加入relations定义,否则不会生效。
在使用如 分包异步化 或 用时注入 等特性时,自定义组件所引用的其他自定义组件,在刚开始进行渲染时可能处于不可用的状态。此时,为了使渲染过程不被阻塞,不可用的自定义组件需要一个 「占位组件」(Component placeholder)。基础库会用占位组件替代不可用组件进行渲染,在该组件可用后再将占位组件替换回该组件。
一个自定义组件的占位组件可以是另一个自定义组件、或一个内置组件。
页面或自定义组件对应的 JSON 配置中的 componentPlaceholder 字段用于指定占位组件,如:
{ "usingComponents": { "comp-a": "../comp/compA", "comp-b": "../comp/compB", "comp-c": "../comp/compC" }, "componentPlaceholder": { "comp-a": "view", "comp-b": "comp-c" } }
该配置表示:
<button ontap="onTap">显示组件</button> <comp-a wx-if="{{ visible }}"> <comp-b prop="{{ p }}">text in slot</comp-b> </comp-a>
小程序启动时 visible 为 false,那么只有 button 会被渲染;点击按钮后,this.setData({ visible: true }) 被执行,此时如果 comp-a, comp-b 均不可用,则页面将被渲染为:
<button>显示组件</button> <view> <comp-c prop="{{ p }}">text in slot</comp-c> </view>
comp-a 与 comp-b 准备完成后,页面被替换为:
<button>显示组件</button> <comp-a> <comp-b prop="{{ p }}">text in slot</comp-b> </comp-a>
注意事项