wxml
模板和 wxss
样式。组件模板的写法与页面模板相同。组件模板与组件数据结合后生成的节点树,将被插入到组件的引用位置上。
在组件模板中可以提供一个
节点,用于承载组件引用时提供的子节点。
代码示例:
<view class="wrapper">
<view>这里是组件的内部节点view>
<slot>slot>
view>
<view>
<component-tag-name>
<view>这里是插入到组件slot中的内容view>
component-tag-name>
view>
json
文件中显式定义,否则会被当作一个无意义的节点。除此以外,节点名也可以被声明为抽象节点。代码示例:
<view>
<component-tag-name prop-a="{
{dataFieldA}}" prop-b="{
{dataFieldB}}">
<view>这里是插入到组件slot中的内容view>
component-tag-name>
view>
在以上例子中,组件的属性 propA
和 propB
将收到页面传递的数据。页面可以通过 setData
来改变绑定的数据字段。
注意:这样的数据绑定只能传递 JSON 兼容数据。自基础库版本 2.0.9 开始,还可以在数据中包含函数(但这些函数不能在 WXML 中直接调用,只能传递给子组件)。
在组件的 wxml 中可以包含 slot
节点,用于承载组件使用者提供的 wxml 结构。
默认情况下,一个组件的 wxml 中只能有一个 slot 。需要使用多 slot 时,可以在组件 js 中声明启用。
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: {
/* ... */ },
methods: {
/* ... */ }
})
name
来区分。
<view class="wrapper">
<slot name="before">slot>
<view>这里是组件的内部细节view>
<slot name="after">slot>
view>
slot
属性来将节点插入到不同的 slot 上。
<view>
<component-tag-name>
<view slot="before">这里是插入到组件slot name="before"中的内容view>
<view slot="after">这里是插入到组件slot name="after"中的内容view>
component-tag-name>
view>
组件对应 wxss
文件的样式,只对组件wxml内的节点生效。编写组件样式时,需要注意以下几点:
#a
)、属性选择器([a]
)和标签名选择器,请改用class选择器。.a .b
)在一些极端情况下会有非预期的表现,如遇,请避免使用。.a>.b
)只能用于 view
组件与其子节点之间,用于其他组件可能导致非预期的情况。font
、 color
,会从组件外继承到组件内。app.wxss
中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。#a {
} /* 在组件中不能使用 */
[a] {
} /* 在组件中不能使用 */
button {
} /* 在组件中不能使用 */
.a > .b {
} /* 除非 .a 是 view 组件节点,否则不一定会生效 */
:host
选择器(需要包含基础库 1.7.2 或更高版本的开发者工具支持)。代码示例:
/* 组件 custom-component.wxss */
:host {
color: yellow;
}
这段文本是黄色的
默认情况下,自定义组件的样式只受到自定义组件 wxss 的影响。除非以下两种情况:
app.wxss
或页面的 wxss
中使用了标签名选择器(或一些其他特殊选择器)来直接指定样式,这些选择器会影响到页面和全部组件。通常情况下这是不推荐的做法。styleIsolation
。Component({
options: {
styleIsolation: 'isolated'
}
})
styleIsolation
选项从基础库版本 2.6.5 开始支持。它支持以下取值:
isolated
表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值);apply-shared
表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面;shared
表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared
或 shared
的自定义组件。(这个选项在插件中不可用。)使用后两者时,请务必注意组件间样式的相互影响。
如果这个 Component 构造器用于构造页面 ,则默认值为 shared
,且还有以下几个额外的样式隔离选项可用:
page-isolated
表示在这个页面禁用 app.wxss ,同时,页面的 wxss 不会影响到其他自定义组件;page-apply-shared
表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式不会影响到其他自定义组件,但设为 shared
的自定义组件会影响到页面;page-shared
表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式会影响到其他设为 apply-shared
或 shared
的自定义组件,也会受到设为 shared
的自定义组件的影响。从小程序基础库版本 2.10.1 开始,也可以在页面或自定义组件的 json 文件中配置 styleIsolation
(这样就不需在 js 文件的 options
中再配置)。例如:
{
"styleIsolation": "isolated"
}
此外,小程序基础库版本 2.2.3 以上支持 addGlobalClass
选项,即在 Component
的 options
中设置 addGlobalClass: true
。 这个选项等价于设置 styleIsolation: apply-shared
,但设置了 styleIsolation
选项后这个选项会失效。
代码示例:
/* 组件 custom-component.js */
Component({
options: {
addGlobalClass: true,
}
})
<!-- 组件 custom-component.wxml -->
<text class="red-text">这段文本的颜色由 `app.wxss` 和页面 `wxss` 中的样式定义来决定</text>
/* app.wxss */
.red-text {
color: red;
}
基础库 1.9.90 开始支持,低版本需做兼容处理。
有时,组件希望接受外部传入的样式类。此时可以在 Component
中用 externalClasses
定义段定义若干个外部样式类。
这个特性可以用于实现类似于 view
组件的 hover-class
属性:页面可以提供一个样式类,赋予 view
的 hover-class
,这个样式类本身写在页面中而非 view
组件的实现中。
注意:在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况。
代码示例:
/* 组件 custom-component.js */
Component({
externalClasses: ['my-class']
})
<!-- 组件 custom-component.wxml -->
<custom-component class="my-class">这段文本的颜色由组件外的 class 决定</custom-component>
代码示例:
<custom-component my-class="red-text" />
<custom-component my-class="large-text" />
<custom-component my-class="red-text large-text" />
.red-text {
color: red;
}
.large-text {
font-size: 1.5em;
}
基础库 2.9.2 开始支持,低版本需做兼容处理。
isolated
,组件仍然可以在局部引用组件所在页面的样式或父组件的样式。例如,如果在页面 wxss 中定义了:
.blue-text {
color: blue;
}
~
来引用这个类的样式:<view class="~blue-text"> 这段文本是蓝色的 view>
如果在一个组件的父组件 wxss 中定义了:
.red-text {
color: red;
}
^
来引用这个类的样式:<view class="^red-text"> 这段文本是红色的 view>
^
来引用祖先组件中的样式。注意:如果组件是比较独立、通用的组件,请优先使用外部样式类的方式,而非直接引用父组件或页面的样式。
基础库 2.11.2 开始支持,低版本需做兼容处理。
class
style
、动画、 flex 布局等,就如同普通的 view 组件节点一样。
<view style="display: flex">
<custom-component style="color: blue; flex: 1">蓝色、满宽的custom-component>
view>
但有些时候,自定义组件并不希望这个节点本身可以设置样式、响应 flex 布局等,而是希望自定义组件内部的第一层节点能够响应 flex 布局或者样式由自定义组件本身完全决定。
这种情况下,可以将这个自定义组件设置为“虚拟的”:
Component({
options: {
virtualHost: true
},
properties: {
style: {
// 定义 style 属性可以拿到 style 属性上设置的值
type: String,
}
},
externalClasses: ['class'], // 可以将 class 设为 externalClasses
})
这样,可以将 flex 放入自定义组件内:
<view style="display: flex">
<custom-component style="color: blue">不是蓝色的custom-component>
view>
<view style="flex: 1">
满宽的
<slot>slot>
view>
需要注意的是,自定义组件节点上的 class
style
和动画将不再生效,但仍可以:
properties
属性来获取 style 上设置的值;externalClasses
外部样式类使得自定义组件 wxml 可以使用 class 值。Component
构造器可用于定义组件,调用 Component
构造器时可以指定组件的属性、数据、方法等。
创建自定义组件,接受一个 Object
类型的参数。
参数
Object object
定义段 | 类型 | 是否必填 | 描述 | 最低版本 |
---|---|---|---|---|
properties | Object Map | 否 | 组件的对外属性,是属性名到属性设置的映射表 | |
data | Object | 否 | 组件的内部数据,和 properties 一同用于组件的模板渲染 |
|
observers | Object | 否 | 组件数据字段监听器,用于监听 properties 和 data 的变化,参见 数据监听器 | 2.6.1 |
methods | Object | 否 | 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 组件间通信与事件 | |
behaviors | String Array | 否 | 类似于mixins和traits的组件间代码复用机制,参见 behaviors | |
created | Function | 否 | 组件生命周期函数-在组件实例刚刚被创建时执行,注意此时不能调用 setData ) |
|
attached | Function | 否 | 组件生命周期函数-在组件实例进入页面节点树时执行) | |
ready | Function | 否 | 组件生命周期函数-在组件布局完成后执行) | |
moved | Function | 否 | 组件生命周期函数-在组件实例被移动到节点树另一个位置时执行) | |
detached | Function | 否 | 组件生命周期函数-在组件实例被从页面节点树移除时执行) | |
relations | Object | 否 | 组件间关系定义,参见 组件间关系 | |
externalClasses | String Array | 否 | 组件接受的外部样式类,参见 外部样式类 | |
options | Object Map | 否 | 一些选项(文档中介绍相关特性时会涉及具体的选项设置,这里暂不列举) | |
lifetimes | Object | 否 | 组件生命周期声明对象,参见 组件生命周期 | 2.2.3 |
pageLifetimes | Object | 否 | 组件所在页面的生命周期声明对象,参见 组件生命周期 | 2.2.3 |
definitionFilter | Function | 否 | 定义段过滤器,用于自定义组件扩展,参见 自定义组件扩展 | 2.2.3 |
observer
中通过 this
访问。组件包含一些通用属性和方法。属性名 | 类型 | 描述 |
---|---|---|
is | String | 组件的文件路径 |
id | String | 节点id |
dataset | String | 节点dataset |
data | Object | 组件数据,包括内部数据和属性值 |
properties | Object | 组件数据,包括内部数据和属性值(与 data 一致) |
方法名 | 参数 | 描述 | 最低版本 |
---|---|---|---|
setData | Object newData |
设置data并执行视图层渲染 | |
hasBehavior | Object behavior |
检查组件是否具有 behavior (检查时会递归检查被直接或间接引入的所有behavior) |
|
triggerEvent | String name , Object detail , Object options |
触发事件,参见 组件间通信与事件 | |
createSelectorQuery | 创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内 | ||
createIntersectionObserver | 创建一个 IntersectionObserver 对象,选择器选取范围为这个组件实例内 | ||
createMediaQueryObserver | 创建一个 MediaQueryObserver 对象 | 2.11.1 | |
selectComponent | String selector |
使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象(会被 wx://component-export 影响) |
|
selectAllComponents | String selector |
使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组(会被 wx://component-export 影响) |
|
selectOwnerComponent | 选取当前组件节点所在的组件实例(即组件的引用者),返回它的组件实例对象(会被 wx://component-export 影响) |
2.8.2 | |
getRelationNodes | String relationKey |
获取这个关系所对应的所有关联节点,参见 组件间关系 | |
groupSetData | Function callback |
立刻执行 callback ,其中的多个 setData 之间不会触发界面绘制(只有某些特殊场景中需要,如用于在不同组件同时 setData 时进行界面绘制同步) |
2.4.0 |
getTabBar | 返回当前页面的 custom-tab-bar 的组件实例,详见自定义 tabBar | 2.6.2 | |
getPageId | 返回页面标识符(一个字符串),可以用来判断几个自定义组件实例是不是在同一个页面内 | 2.7.1 | |
animate | String selector , Array keyframes , Number duration , Function callback |
执行关键帧动画,详见动画 | 2.9.0 |
clearAnimation | String selector , Object options , Function callback |
清除关键帧动画,详见动画 | 2.9.0 |
setUpdatePerformanceListener | Object options , Function listener |
清除关键帧动画,详见动画 | 2.12.0 |
代码示例:
Component({
behaviors: [],
// 属性定义(详情参见下文)
properties: {
myProperty: {
// 属性名
type: String,
value: ''
},
myProperty2: String // 简化的定义方式
},
data: {
}, // 私有数据,可用于模板渲染
lifetimes: {
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () {
},
moved: function () {
},
detached: function () {
},
},
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () {
}, // 此处attached的声明会被lifetimes字段中的声明覆盖
ready: function() {
},
pageLifetimes: {
// 组件所在页面的生命周期函数
show: function () {
},
hide: function () {
},
resize: function () {
},
},
methods: {
onMyButtonTap: function(){
this.setData({
// 更新属性和数据的方法与更新页面数据的方法类似
})
},
// 内部方法建议以下划线开头
_myPrivateMethod: function(){
// 这里将 data.A[0].B 设为 'myPrivateData'
this.setData({
'A[0].B': 'myPrivateData'
})
},
_propertyChange: function(newVal, oldVal) {
}
}
})
注意:在 properties
定义段中,属性名采用驼峰写法(propertyName
);在 wxml
中,指定属性值时则对应使用连字符写法(component-tag-name property-name="attr value"
),应用于数据绑定时采用驼峰写法(attr=""
)。
properties 定义
定义段 | 类型 | 是否必填 | 描述 | 最低版本 |
---|---|---|---|---|
type | 是 | 属性的类型 | ||
optionalTypes | Array | 否 | 属性的类型(可以指定多个) | 2.6.5 |
value | 否 | 属性的初始值 | ||
observer | Function | 否 | 属性值变化时的回调函数 |
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
}
}
})
属性的类型可以为 String
Number
Boolean
Object
Array
其一,也可以为 null
表示不限制类型。
多数情况下,属性最好指定一个确切的类型。这样,在 WXML 中以字面量指定属性值时,值可以获得一个确切的类型,如:
<custom-comp min="1" max="5" />
Number
类型, min
和 max
会被赋值为 1
和 5
,而非 "1"
和 "5"
,即:this.data.min === 1 // true
this.data.max === 5 // true
Bug & Tips:
this.data
可以获取内部数据和属性值;但直接修改它不会将变更应用到界面上,应使用 setData
修改。this
访问到。dataXyz
这样的形式,因为在 WXML 中, data-xyz=""
会被作为节点 dataset 来处理,而不是组件属性。bug
: 位于 slot 中的自定义组件没有触发 pageLifetimes
中声明的页面生命周期,此问题在 2.5.2 中修复。bug
: 对于 type 为 Object 或 Array 的属性,如果通过该组件自身的 this.setData 来改变属性值的一个子字段,则依旧会触发属性 observer ,且 observer 接收到的 newVal
是变化的那个子字段的值, oldVal
为空, changedPath
包含子字段的字段名相关信息;目前推荐使用 observers
定义段代替。事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component
构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含 usingComponents
定义段。
此时,组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123¶mB=xyz
,如果声明有属性 paramA
或 paramB
,则它们会被赋值为 123
或 xyz
。
页面的生命周期方法(即 on
开头的方法),应写在 methods
定义段中。
代码示例:
{
"usingComponents": {
}
}
Component({
properties: {
paramA: Number,
paramB: String,
},
methods: {
onLoad: function() {
this.data.paramA // 页面参数 paramA 的值
this.data.paramB // 页面参数 paramB 的值
}
}
})
Component
构造器来构造页面的一个好处是可以使用 behaviors
来提取所有页面中公用的代码段。例如,在所有页面被创建和销毁时都要执行同一段代码,就可以把这段代码提取到 behaviors
中。
代码示例:
// page-common-behavior.js
module.exports = Behavior({
attached: function() {
// 页面创建时执行
console.info('Page loaded!')
},
detached: function() {
// 页面销毁时执行
console.info('Page unloaded!')
}
})
// 页面 A
var pageCommonBehavior = require('./page-common-behavior')
Component({
behaviors: [pageCommonBehavior],
data: {
/* ... */ },
methods: {
/* ... */ },
})
// 页面 B
var pageCommonBehavior = require('./page-common-behavior')
Component({
behaviors: [pageCommonBehavior],
data: {
/* ... */ },
methods: {
/* ... */ },
})
组件间的基本通信方式有以下几种。
this.selectComponent
方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。事件系统是组件间通信的主要方式之一。自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件。关于事件的基本概念和用法,参见 事件 。
监听自定义组件事件的方法与监听基础组件事件的方法完全一致:
代码示例:
<component-tag-name bindmyevent="onMyEvent" />
<component-tag-name bind:myevent="onMyEvent" />
Page({
onMyEvent: function(e){
e.detail // 自定义组件触发事件时提供的detail对象
}
})
triggerEvent
方法,指定事件名、detail对象和事件选项:代码示例:
<button bindtap="onTap">点击这个按钮将触发“myevent”事件button>
Component({
properties: {},
methods: {
onTap: function(){
var myEventDetail = {} // detail对象,提供给事件监听函数
var myEventOption = {} // 触发事件的选项
this.triggerEvent('myevent', myEventDetail, myEventOption)
}
}
})
选项名 | 类型 | 是否必填 | 默认值 | 描述 |
---|---|---|---|---|
bubbles | Boolean | 否 | false | 事件是否冒泡 |
composed | Boolean | 否 | false | 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部 |
capturePhase | Boolean | 否 | false | 事件是否拥有捕获阶段 |
代码示例:
// 页面 page.wxml
<another-component bindcustomevent="pageEventListener1">
<my-component bindcustomevent="pageEventListener2">my-component>
another-component>
// 组件 another-component.wxml
<view bindcustomevent="anotherEventListener">
<slot />
view>
// 组件 my-component.wxml
<view bindcustomevent="myEventListener">
<slot />
view>
// 组件 my-component.js
Component({
methods: {
onTap: function(){
this.triggerEvent('customevent', {}) // 只会触发 pageEventListener2
this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1
this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1
}
}
})
可在父组件里调用 this.selectComponent
,获取子组件的实例对象。(插件的自定义组件将返回 null
)
调用时需要传入一个匹配选择器 selector
,如:this.selectComponent(".my-component")
。
selector
详细语法可查看 selector 语法参考文档。
代码示例:
// 父组件
Page({
data: {
},
getChildComponent: function () {
const child = this.selectComponent('.my-component');
console.log(child)
}
})
在上例中,父组件将会获取 class
为 my-component
的子组件实例对象,即子组件的 this
。
若需要自定义 selectComponent
返回的数据,可使用内置 behavior
: wx://component-export
从基础库版本 2.2.3 开始提供支持。
使自定义组件中支持 export
定义段,这个定义段可以用于指定组件被 selectComponent
调用时的返回值。
代码示例:
// 自定义组件 my-component 内部
Component({
behaviors: ['wx://component-export'],
export() {
return {
myField: 'myValue' }
}
})
<!-- 使用自定义组件时 -->
<my-component id="the-id" />
// 父组件调用
const child = this.selectComponent('#the-id') // 等于 { myField: 'myValue' }
id
为 the-id
的子组件实例的时候,得到的是对象 { myField: 'myValue' }
。组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发。
其中,最重要的生命周期是 created
attached
detached
,包含一个组件实例生命流程的最主要时间点。
created
生命周期被触发。此时,组件数据 this.data
就是在 Component
构造器中定义的数据 data
。 此时还不能调用 setData
。 通常情况下,这个生命周期只应该用于给组件 this
添加一些自定义属性字段。attached
生命周期被触发。此时, this.data
已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。detached
生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached
会被触发。生命周期方法可以直接定义在 Component
构造器的第一级参数中。
自小程序基础库版本 2.2.3 起,组件的的生命周期也可以在 lifetimes
字段内进行声明(这是推荐的方式,其优先级最高)。
代码示例:
Component({
lifetimes: {
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
},
// 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
// ...
})
在 behaviors 中也可以编写生命周期方法,同时不会与其他 behaviors 中的同名生命周期相互覆盖。但要注意,如果一个组件多次直接或间接引用同一个 behavior ,这个 behavior 中的生命周期函数在一个执行时机内只会执行一次。
可用的全部生命周期如下表所示。
生命周期 | 参数 | 描述 | 最低版本 |
---|---|---|---|
created | 无 | 在组件实例刚刚被创建时执行 | 1.6.3 |
attached | 无 | 在组件实例进入页面节点树时执行 | 1.6.3 |
ready | 无 | 在组件在视图层布局完成后执行 | 1.6.3 |
moved | 无 | 在组件实例被移动到节点树另一个位置时执行 | 1.6.3 |
detached | 无 | 在组件实例被从页面节点树移除时执行 | 1.6.3 |
error | Object Error |
每当组件方法抛出错误时执行 | 2.4.1 |
pageLifetimes
定义段中定义。其中可用的生命周期包括:生命周期 | 参数 | 描述 | 最低版本 |
---|---|---|---|
show | 无 | 组件所在的页面被展示时执行 | 2.2.3 |
hide | 无 | 组件所在的页面被隐藏时执行 | 2.2.3 |
resize | Object Size |
组件所在的页面尺寸变化时执行 | 2.4.0 |
代码示例:
Component({
pageLifetimes: {
show: function() {
// 页面被展示
},
hide: function() {
// 页面被隐藏
},
resize: function(size) {
// 页面尺寸变化
}
}
})
behaviors
是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。
每个 behavior
可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。 每个组件可以引用多个 behavior
,behavior
也可以引用其它 behavior
。
注册一个 behavior
,接受一个 Object
类型的参数。
参数
Object object
定义段 | 类型 | 是否必填 | 描述 | 最低版本 |
---|---|---|---|---|
properties | Object Map | 否 | 同组件的属性 | |
data | Object | 否 | 同组件的数据 | |
methods | Object | 否 | 同自定义组件的方法 | |
behaviors | String Array | 否 | 引入其它的 behavior |
|
created | Function | 否 | 生命周期函数 | |
attached | Function | 否 | 生命周期函数 | |
ready | Function | 否 | 生命周期函数 | |
moved | Function | 否 | 生命周期函数 | |
detached | Function | 否 | 生命周期函数 |
代码示例:
// my-behavior.js
module.exports = Behavior({
behaviors: [],
properties: {
myBehaviorProperty: {
type: String
}
},
data: {
myBehaviorData: {
}
},
attached: function(){
},
methods: {
myBehaviorMethod: function(){
}
}
})
组件引用时,在 behaviors
定义段中将它们逐个列出即可。
代码示例:
// my-component.js
var myBehavior = require('my-behavior')
Component({
behaviors: [myBehavior],
properties: {
myProperty: {
type: String
}
},
data: {
myData: 'my-component-data'
},
created: function () {
console.log('[my-component] created')
},
attached: function () {
console.log('[my-component] attached')
},
ready: function () {
console.log('[my-component] ready')
},
methods: {
myMethod: function () {
console.log('[my-component] log by myMethod')
},
}
})
在上例中, my-component
组件定义中加入了 my-behavior
,
而 my-behavior
结构为:
myBehaviorProperty
myBehaviorData
myBehaviorMethod
attached
、created
、ready
这将使 my-component
最终结构为:
myBehaviorProperty
、myProperty
myBehaviorData
、myData
myBehaviorMethod
、myMethod
attached
、created
、ready
当组件触发生命周期时,上例生命周期函数执行顺序为:
[my-behavior] created
[my-component] created
[my-behavior] attached
[my-component] attached
[my-behavior] ready
[my-component] ready
组件和它引用的 behavior
中可以包含同名的字段,对这些字段的处理方法如下:
behavior
中的同名属性或方法;behaviors
字段中定义靠后的 behavior
的属性或方法会覆盖靠前的同名属性或方法;behavior
的情况,则规则为:父 behavior
覆盖 子 behavior
中的同名属性或方法。组件
> 父 behavior
> 子 behavior
、 靠后的 behavior
> 靠前的 behavior
。(优先级高的覆盖优先级低的,最大的为优先级最高)behavior
优先于组件执行;子 behavior
优先于 父 behavior
执行;靠前的 behavior
优先于 靠后的 behavior
执行;behavior
被一个组件多次引用,它定义的生命周期函数只会被执行一次。behavior
来获得内置组件的一些行为。Component({
behaviors: ['wx://form-field']
})
在上例中, wx://form-field
代表一个内置 behavior
,它使得这个自定义组件有类似于表单控件的行为。
内置 behavior
往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type
或添加 observer
。
wx://form-field
属性名 | 类型 | 描述 | 最低版本 |
---|---|---|---|
name | String | 在表单中的字段名 | 1.6.7 |
value | 任意 | 在表单中的字段值 | 1.6.7 |
代码示例:
// custom-form-field.js
Component({
behaviors: ['wx://form-field'],
data: {
value: ''
},
methods: {
onChange: function (e) {
this.setData({
value: e.detail.value,
})
}
}
})
wx://form-field-group
从基础库版本 2.10.2 开始提供支持。
代码示例:
<form bindsubmit="submit">
<custom-comp>custom-comp>
<button form-type="submit">submitbutton>
form>
组件 custom-comp
自身结构如下:
<input name="name" />
<switch name="student" />
如果组件 custom-comp
配置有:
Component({
behaviors: ['wx://form-field-group']
})
此时,表单的 submit
事件的 value
中将包含 name
和 student
两个字段。
wx://form-field-button
从基础库版本 2.10.3 开始提供支持。
代码示例: 在开发者工具中预览效果
<form bindsubmit="submit">
<input name="name" placeholder="请输入名字">input>
<custom-comp>custom-comp>
form>
组件 custom-comp
自身结构如下:
<button form-type="submit">submitbutton>
如果组件 custom-comp
配置有:
Component({
behaviors: ['wx://form-field-button']
})
此时点击组件内的 button ,将触发 form 的 submit 事件。
wx://component-export
从基础库版本 2.2.3 开始提供支持。
使自定义组件支持 export
定义段。这个定义段可以用于指定组件被 selectComponent
调用时的返回值。
详细用法以及代码示例可见:selectComponent 参考文档
<custom-ul>
<custom-li> item 1 custom-li>
<custom-li> item 2 custom-li>
custom-ul>
custom-ul
和 custom-li
都是自定义组件,它们有相互间的关系,相互间的通信往往比较复杂。此时在组件定义时加入 relations
定义段,可以解决这样的问题。示例:// path/to/custom-ul.js
Component({
relations: {
'./custom-li': {
type: 'child', // 关联的目标节点应为子节点
linked: function(target) {
// 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
},
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('path/to/custom-li')
}
},
ready: function(){
this._getAllLi()
}
})
// path/to/custom-li.js
Component({
relations: {
'./custom-ul': {
type: 'parent', // 关联的目标节点应为父节点
linked: function(target) {
// 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
},
linkChanged: function(target) {
// 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
},
unlinked: function(target) {
// 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
}
}
}
})
注意:必须在两个组件定义中都加入relations定义,否则不会生效。
<custom-form>
<view>
input
<custom-input>custom-input>
view>
<custom-submit> submit custom-submit>
custom-form>
custom-form
组件想要关联 custom-input
和 custom-submit
两个组件。此时,如果这两个组件都有同一个behavior:// path/to/custom-form-controls.js
module.exports = Behavior({
// ...
})
// path/to/custom-input.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
// path/to/custom-submit.js
var customFormControls = require('./custom-form-controls')
Component({
behaviors: [customFormControls],
relations: {
'./custom-form': {
type: 'ancestor', // 关联的目标节点应为祖先节点
}
}
})
relations
关系定义中,可使用这个behavior来代替组件路径作为关联的目标节点:// path/to/custom-form.js
var customFormControls = require('./custom-form-controls')
Component({
relations: {
'customFormControls': {
type: 'descendant', // 关联的目标节点应为子孙节点
target: customFormControls
}
}
})
relations
定义段包含目标组件路径及其对应选项,可包含的选项见下表。选项 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | String | 是 | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
target | String | 否 | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
**数据监听器可以用于监听和响应任何属性和数据字段的变化。**从小程序基础库版本 2.6.1 开始支持。
有时,在一些数据字段被 setData 设置时,需要执行一些操作。
例如, this.data.sum
永远是 this.data.numberA
与 this.data.numberB
的和。此时,可以使用数据监听器进行如下实现。
Component({
attached: function() {
this.setData({
numberA: 1,
numberB: 2,
})
},
observers: {
'numberA, numberB': function(numberA, numberB) {
// 在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
}
}
})
数据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 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]
},
}
})
**
。Component({
observers: {
'some.field.**': function(field) {
// 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发
// (除此以外,使用 setData 设置 this.data.some 也会触发)
field === this.data.some.field
},
},
attached: function() {
// 这样会触发上面的 observer
this.setData({
'some.field': {
/* ... */ }
})
// 这样也会触发上面的 observer
this.setData({
'some.field.xxx': {
/* ... */ }
})
// 这样还是会触发上面的 observer
this.setData({
'some': {
/* ... */ }
})
}
})
**
可以监听全部 setData 。Component({
observers: {
'**': function() {
// 每次 setData 都触发
},
},
})
Bugs & Tips:
有些情况下,某些 data
中的字段(包括 setData
设置的字段)既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。
此时,可以指定这样的数据字段为“纯数据字段”,它们将仅仅被记录在 this.data
中,而不参与任何界面渲染过程,这样有助于提升页面更新性能。
指定“纯数据字段”的方法是在 Component
构造器的 options
定义段中指定 pureDataPattern
为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。
代码示例:
Component({
options: {
pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
a: true, // 普通数据字段
_b: true, // 纯数据字段
},
methods: {
myMethod() {
this.data._b // 纯数据字段可以在 this.data 中获取
this.setData({
c: true, // 普通数据字段
_d: true, // 纯数据字段
})
}
}
})
<view wx:if="{
{a}}"> 这行会被展示 view>
<view wx:if="{
{_b}}"> 这行不会被展示 view>
属性也可以被指定为纯数据字段(遵循 pureDataPattern
的正则表达式)。
属性中的纯数据字段可以像普通属性一样接收外部传入的属性值,但不能将它直接用于组件自身的 WXML 中。
代码示例:
Component({
options: {
pureDataPattern: /^_/
},
properties: {
a: Boolean,
_b: {
type: Boolean,
observer() {
// 不要这样做!这个 observer 永远不会被触发
}
},
}
})
注意:属性中的纯数据字段的属性 observer 永远不会触发!如果想要监听属性值变化,使用数据监听器代替。
pureDataPattern
(这样就不需在 js 文件的 options
中再配置)。此时,其值应当写成字符串形式:{
"pureDataPattern": "^_"
}
数据监听器可以用于监听纯数据字段(与普通数据字段一样)。这样,可以通过监听、响应纯数据字段的变化来改变界面。
下面的示例是一个将 JavaScript 时间戳转换为可读时间的自定义组件。
代码示例:
Component({
options: {
pureDataPattern: /^timestamp$/ // 将 timestamp 属性指定为纯数据字段
},
properties: {
timestamp: Number,
},
observers: {
timestamp: function () {
// timestamp 被设置时,将它展示为可读时间字符串
var timeString = new Date(this.data.timestamp).toLocaleString()
this.setData({
timeString: timeString
})
}
}
})
<view>{
{
timeString}}</view>
有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。
例如,我们现在来实现一个“选框组”(selectable-group)组件,它其中可以放置单选框(custom-radio)或者复选框(custom-checkbox)。这个组件的 wxml 可以这样编写:
代码示例:
<view wx:for="{
{labels}}">
<label>
<selectable disabled="{
{false}}">selectable>
{
{item}}
label>
view>
usingComponents
字段中声明的组件,而是一个抽象节点。它需要在 componentGenerics
字段中声明:{
"componentGenerics": {
"selectable": true
}
}
<selectable-group generic:selectable="custom-radio" />
<selectable-group generic:selectable="custom-checkbox" />
注意:上述的 custom-radio
和 custom-checkbox
需要包含在这个 wxml 对应 json 文件的 usingComponents
定义段中。
{
"usingComponents": {
"custom-radio": "path/to/custom/radio",
"custom-checkbox": "path/to/custom/checkbox"
}
}
componentGenerics
字段中指定:{
"componentGenerics": {
"selectable": {
"default": "path/to/default/component"
}
}
}
Tips:
generic:xxx="yyy"
中,值 yyy
只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。**为了更好定制自定义组件的功能,可以使用自定义组件扩展机制。**从小程序基础库版本 2.2.3 开始支持。
// behavior.js
module.exports = Behavior({
definitionFilter(defFields) {
defFields.data.from = 'behavior'
},
})
// component.js
Component({
data: {
from: 'component'
},
behaviors: [require('behavior.js')],
ready() {
console.log(this.data.from) // 此处会发现输出 behavior 而不是 component
}
})
data
定义段里的内容。Behavior()
构造器提供了新的定义段 definitionFilter
,用于支持自定义组件扩展。definitionFilter
是一个函数,在被调用时会注入两个参数definitionFilter
函数列表以下举个例子来说明:
// behavior3.js
module.exports = Behavior({
definitionFilter(defFields, definitionFilterArr) {
},
})
// behavior2.js
module.exports = Behavior({
behaviors: [require('behavior3.js')],
definitionFilter(defFields, definitionFilterArr) {
// definitionFilterArr[0](defFields)
},
})
// behavior1.js
module.exports = Behavior({
behaviors: [require('behavior2.js')],
definitionFilter(defFields, definitionFilterArr) {
},
})
// component.js
Component({
behaviors: [require('behavior1.js')],
})
definitionFilter
定义段。那么按照声明的顺序会有如下事情发生:definitionFilter
函数,其中 defFields
参数是 behavior2 的定义段, definitionFilterArr
参数即为空数组,因为 behavior3 没有使用其他的 behavior 。definitionFilter
函数,其中 defFields
参数是 behavior1 的定义段, definitionFilterArr
参数是一个长度为1的数组,definitionFilterArr[0]
即为 behavior3 的 definitionFilter
函数,因为 behavior2 使用了 behavior3。用户在此处可以自行决定在进行 behavior1 的声明时要不要调用 behavior3 的 definitionFilter
函数,如果需要调用,在此处补充代码 definitionFilterArr[0](defFields)
即可,definitionFilterArr
参数会由基础库补充传入。definitionFilter
函数。definitionFilter
函数可以理解为当 A 使用了 B 时,A 声明就会调用 B 的 definitionFilter
函数并传入 A 的定义对象让 B 去过滤。此时如果 B 还使用了 C 和 D ,那么 B 可以自行决定要不要调用 C 和 D 的 definitionFilter
函数去过滤 A 的定义对象。代码示例:
// behavior.js
module.exports = Behavior({
lifetimes: {
created() {
this._originalSetData = this.setData // 原始 setData
this.setData = this._setData // 封装后的 setData
}
},
definitionFilter(defFields) {
const computed = defFields.computed || {
}
const computedKeys = Object.keys(computed)
const computedCache = {
}
// 计算 computed
const calcComputed = (scope, insertToData) => {
const needUpdate = {
}
const data = defFields.data = defFields.data || {
}
for (let key of computedKeys) {
const value = computed[key].call(scope) // 计算新值
if (computedCache[key] !== value) needUpdate[key] = computedCache[key] = value
if (insertToData) data[key] = needUpdate[key] // 直接插入到 data 中,初始化时才需要的操作
}
return needUpdate
}
// 重写 setData 方法
defFields.methods = defFields.methods || {
}
defFields.methods._setData = function (data, callback) {
const originalSetData = this._originalSetData // 原始 setData
originalSetData.call(this, data, callback) // 做 data 的 setData
const needUpdate = calcComputed(this) // 计算 computed
originalSetData.call(this, needUpdate) // 做 computed 的 setData
}
// 初始化 computed
calcComputed(defFields, true) // 计算 computed
}
})
在组件中使用:
const beh = require('./behavior.js')
Component({
behaviors: [beh],
data: {
a: 0,
},
computed: {
b() {
return this.data.a + 100
},
},
methods: {
onTap() {
this.setData({
a: ++this.data.a,
})
}
}
})
<view>data: {
{
a}}</view>
<view>computed: {
{
b}}</view>
<button bindtap="onTap">click</button>
此实现只是作为一个简单案例来展示,请勿直接在生产环境中使用。
开发一个开源的自定义组件包给他人使用,首先需要明确他人是要如何使用这个包的,如果只是拷贝小程序目录下直接使用的话,可以跳过此文档。此文档中后续内容是以 npm 管理自定义组件包的前提下进行说明的。
在开发之前,要求开发者具有基础的 node.js 和 npm 相关的知识,同时需要准备好支持 npm 功能的开发者工具,点此下载。
为了方便开发者能够快速搭建好一个可用于开发、调试、测试的自定义组件包项目,官方提供了一个项目模板,下载使用模板的方式有三种:
项目模板中的构建是基于 gulp + webpack 来执行的,支持开发、构建、测试等命令,详情可参阅项目模板的 README.md 文件。
npm install -g @wechat-miniprogram/miniprogram-cli
miniprogram init --type custom-component
命令行工具的更多用法可以查看 github 仓库上的 README.md 文件。
PS:第一次使用
miniprogram init
初始化项目会去 github 上拉取模板,因此需要保证网络畅通。
针对自定义组件的单元测试,可参阅文档单元测试。
自定义组件示例
以下为官方提供的自定义组件,可以参考并使用:
自定义组件扩展示例
以下为官方提供的自定义组件扩展,可以参考并使用:
在编写高质量的自定义组件过程中,单元测试是永远避不开的一个话题。完善的测试用例是提高自定义组件可用性的保证,同时测试代码覆盖率也是必不可少的一个环节。小程序从基础库版本 2.2.1 开始拥抱开源,支持使用 npm 安装自定义组件,那针对自定义组件的单元测试也是必须支持的。
以下就来介绍如何对自定义组件进行单元测试。
小程序的运行环境比较特殊,不同于常见的浏览器环境,它采用的是双线程的架构。而在进行单元测试时,我们并不需要用到这样复杂的架构带来的利好,我们进行的是功能测试而无需苛求性能、安全等因素,因此我们提供了一个测试工具集以支持自定义组件在 nodejs 单线程中也能运行起来。
我们先安装一下测试工具集——miniprogram-simulate:
npm i --save-dev miniprogram-simulate
假设我们有如下自定义组件:
<view class="index">{
{prop}}view>
// /components/index.js
Component({
properties: {
prop: {
type: String,
value: 'index.properties'
},
},
})
/* /components/index.wxss */
.index {
color: green;
}
我们想要测试渲染的结果,可以按照如下方式编写测试用例:
// /test/components/index.test.js
const simulate = require('miniprogram-simulate')
test('components/index', () => {
const id = simulate.load('/components/index') // 此处必须传入绝对路径
const comp = simulate.render(id) // 渲染成自定义组件树实例
const parent = document.createElement('parent-wrapper') // 创建父亲节点
comp.attach(parent) // attach 到父亲节点上,此时会触发自定义组件的 attached 钩子
const view = comp.querySelector('.index') // 获取子组件 view
expect(view.dom.innerHTML).toBe('index.properties') // 测试渲染结果
expect(window.getComputedStyle(view.dom).color).toBe('green') // 测试渲染结果
})
PS:测试工具集中的 wx 对象和内置组件都不会实现真正的功能,如果需要测试一些特殊场景的话,可以自行覆盖掉测试工具集中的 api 接口和内置组件。
PS:目前因为有部分自定义组件功能仍未支持(如抽象节点等),故测试工具暂无法全部覆盖自定义组件的特性,后续会继续完善。
测试工具集中提供了一些方便测试的接口,比如:
更多详细的用法可以参阅 github 仓库上的文档。
基础库 2.12.0 开始支持,低版本需做兼容处理。
如果想要知道 setData 引发界面更新的开销,可以使用更新性能统计信息接口。它将返回每次更新中主要更新步骤发生的时间戳,可以用来大体上估计自定义组件(或页面)更新性能。例如:
Component({
attached() {
// 调用时机不能早于 attached
this.setUpdatePerformanceListener({
withDataPaths: true}, (res) => {
console.log(res)
})
}
})
setUpdatePerformanceListener
方法接受一个 options
对象和回调函数 listener
作为参数。
其中, options
对象包含以下字段:
字段 | 类型 | 说明 |
---|---|---|
withDataPaths | Boolean | 是否返回变更的 data 字段信息 |
listeners
返回携带一个 res
对象,表示一次由 setData 引发的 更新过程 。根据 setData 调用时机的不同,更新过程大体可以分为三类:
updateProcessId
;updateProcessId
,但还有一个 parentUpdateProcessId
;每次成功的 setData 调用都会产生一个更新过程,使得 listener
回调一次。不过 setData 究竟触发了哪类更新过程很难判断,更新性能好坏与其具体是哪类更新也没有必然联系,只是它们的返回值参数有所不同。
res
中包含以下字段:
字段 | 类型 | 说明 |
---|---|---|
updateProcessId | Number | 此次更新过程的 ID |
parentUpdateProcessId | Number | 对于子更新,返回它所属的更新过程 ID |
isMergedUpdate | Boolean | 是否是被合并更新,如果是,则 updateProcessId 表示被合并到的更新过程 ID |
dataPaths | Array | 此次更新的 data 字段信息,只有 withDataPaths 设为 true 时才会返回 |
pendingStartTimestamp | Number | 此次更新进入等待队列时的时间戳 |
updateStartTimestamp | Number | 更新运算开始时的时间戳 |
updateEndTimestamp | Number | 更新运算结束时的时间戳 |
说明:
setUpdatePerformanceListener
只会激活当前组件或页面的统计, parentUpdateProcessId
有可能是其他组件或者页面的更新过程 ID 而未被统计回调,如果想要知道页面内所有的更新过程,需要在所有组件中都调用 setUpdatePerformanceListener
;setUpdatePerformanceListener
时传入第二个参数 listener
为 null
即可。