微信小程序自带了很多基础组件,但是在使用过程中我们可能需要根据基础组件来创建自定义的组件。例如 WeUI 就是官方开发的自定义组件。 WeUI 与自己开发的组件区别在于,自开发组件需要本地引入,而 WeUI 除了 npm 方式外,还支持 useExtendedLib 扩展库引入。
WeUI
因为 useExtendedLib 扩展库引入与 npm 方式引入相较简单,而且不占用小程序的包体积,扩展库引入也相当于引入了对应扩展库相关的最新版本的 npm 包,所以 WeUI 只研究 useExtendedLib 扩展库引入。
引入 WeUI 和使用准备
在 app.json 文件中写入 "useExtendedLib": {"weui":true}
即引入了WeUI。使用前,需要在页面的 json 文件中声明需要使用的组件。例如需要使用 dialog 弹窗组件 和 Searchbar 搜索组件 :
{
"usingComponents": {
"mp-dialog": "weui-miniprogram/dialog/dialog",
"mp-searchbar": "weui-miniprogram/searchbar/searchbar"
}
}
至于各组件的样式、使用方法、参数及引用的代码,则可以到 WeUI 的官方介绍里查找:
使用WeUI
引入并声明了相应组件后,就可以和使用基础组件一样使用 WeUI 组件了。
<!-- WeUI_test.wxml -->
<mp-searchbar placeholder="搜一下"></mp-searchbar>
具体的使用方法可以查看 WeUI 官方介绍。
自定义函数的使用
自定义的组件简单理解就是带有样式、架构和一些基本函数的类。在研究自定义组件前,先研究下自定义的函数。
自定义函数分为内嵌函数和外联函数。因为微信小程序 wxml 中无法调用 .js 定义的函数,所以在页面结构上要使用函数的话需要使用 wxs 脚本。wxs 脚本最典型的使用场景就是过滤器了。下面我们分开来研究。
内嵌函数
内嵌函数是写在页面内的函数,其使用范围为当前页面。
.js 函数
.js 文件中的自定义函数较简单,写在页面的page里即可。调用时可以使用组件事件绑定函数,也可以在其他函数内部调用。
<!--pages/test/test.wxml-->
<button bindtap='add'>点我 + 1</button>
<button bindtap='tap'>点我 + 2</button>
<view style="text-align: center;margin: 30rpx; font-size: 40rpx;" >{{Num}}</view>
// pages/test/test.js
Page({
data: {
Num: 0
},
add: function (n) {
var num
if(typeof n == 'number'){
num = n
}else{
num = 1
}
this.setData({ Num: this.data.Num + num })
},
tap: function () {
this.add(2)
}
})
需注意的是,当点击触发事件绑定的函数时,传递的参数为触发的事件类型。
wxs 函数
wxs 类似于 JavaScript 但是其实是完全不同的两种语言。wxs 写函数时,需写在 < wxs > 标签内部,使用 module.exports 对象导出。例如上个例子稍改动下:
<!--pages/test/test.wxml-->
<button bindtap='add'>点我 + 1</button>
<button bindtap='tap'>点我 + 2</button>
<view style="text-align: center;margin: 30rpx; font-size: 40rpx;">{{m1.addStr(Num)}}</view>
// 将文本添加引号
<wxs module="m1">
module.exports={
addStr:function(str){
return '"' + str + '"'
}
}
</wxs>
外联函数
外联函数是写在外部文件中的函数,可以在需要使用的页面中引入再进行使用,所以使用范围可以为整个应用程序。
.js 函数
.js 的外联函数写在单独的 .js 文件,使用时引入。引入时使用 require()
函数来引用相应文件里的函数。
// utils/test1.js
module.exports = {
strTest: strTest
}
function strTest(str1, str2, str3 = 'Hello', str4 = '!') {
return str1 + str2 + str3 + str4
}
<!--pages/test1/test1.wxml-->
<button bindtap='tap'>点我显示</button>
<view style="text-align: center;margin: 30rpx; font-size: 40rpx;">{{str}}</view>
// pages/test1/test1.js
const utils = require('../../utils/test1.js')
Page({
data: {
str: ''
},
tap: function () {
this.setData({
str: utils.strTest(' Han ', ' mei ')
})
}
})
需注意的是,如果需要使用异步函数,内嵌时可以在回调函数中进行处理,而外联函数则可以使用 Promise 类来进行处理。
wxs 函数
wxs 的外联函数也是写在单独的文件里,不同的是文件扩展名为 .wxs。引入时在 < wxs > 标签内部引入,且必须有 module 。在上个例子上修改:
// utils/tools.wxs
module.exports = {
addStr: addStr
}
function addStr(str) {
if(str) return '"' + str + '"'
}
<!--pages/test1/test1.wxml-->
<button bindtap='tap'>点我显示</button>
<view style="text-align: center;margin: 30rpx; font-size: 40rpx;">{{tools.addStr(str)}}</view>
<wxs src="../../utils/tools.wxs" module="tools"></wxs>
wxs 的使用注意
使用 wxs 需要注意的有:
- wxs 有自己的数据类型
- wxs 不支持 ES6 及以上的语法形式。例如 let 、const 等都不支持
- 遵循 CommonJS 规范,例如 module 对象、 module.exports 对象等
- wxs 不能用于组件的事件绑定
- js 的 data 下的数据成员可以作为参数传给 wxs ,但是 wxs 不能调用 js 的数据对象、函数等,也不能向 js 传递数据
自定义组件
WeUI 官方组件的样式基本就是微信官方默认样式,如果不能满足需求,则可以自定义组件。
创建自定义组件
首先在微信开发工具里建立个文件夹作为自定义组件的文件夹,一般会在根目录下创建名为 components 的文件夹放置自定义组件。然后在此文件夹下创建文件夹,用以区分组件(就像不同页面在不同文件夹下,创建在同一文件夹下容易混淆)。右键后创建的文件夹,选择新建 component 即建立了4个新文件:.js / .wxml / .wxss / .json 。
这4个和页面类似的文件就是创建的自定义组件的文件了。不同文件的作用和定义可以参考页面。通过分析代码可以知道,组件和页面的不同在于 .json 文件声明了 "component": true
表示这是一个组件。且在 .js 文件中,定义的函数不是 page 而是 Component ,表示成员代码属于组件类中,且具体的定义和使用方法也不同。
使用自定义组件
组件的引用方式分为局部引用和全局引用,区别在于使用范围是页面还是整个小程序。具体的引用区别是在 app.json 中引用还是页面的 .json 文件中引用。引用代码其实和使用WeUI一样:
{
"usingComponents": {
"my-test": "/components/test/test"
}
}
在页面中使用也和 WeUI 一样,直接使用组件标签即可:
<my-test></my-test>
自定义组件的外观
自定义组件的外观和页面基本一样,分为结构和样式两方面
自定义组件的结构
自定义组件的样式
默认情况下,组件是样式隔离的,即组件的样式不会对页面及其他组件样式产生影响。这个在 WeUI 的测试中就可以提现,大部分 WeUI 组件的样式是不可更改的,小部分可更改样式的组件也需要特定的方式进行更改。但是样式隔离也只是针对 class 选择器,而 id 选择器、属性选择器、标签选择器则不受样式隔离的影响。
有时候,需要在外界能够控制组件内部的样式,则可以通过 styleIsolation 来修改隔离样式选项。用法有2中:
- 在组件的 .json 中声明
{
"styleIsolation": "isolated"
}
- 在组件的 .js 文件中新增配置信息
Component({
options: {
styleIsolation: 'isolated'
}
})
styleIsolation 的选项有三种:
- isolated :默认值。表示启用样式隔离
- apply-shared : 应用共享。页面的样式将影响到组件,但是组件中指定的样式不会影响页面
- shared : 完全共享。组件、页面和其他声明了会受到影响的组件会相互影响
自定义组件的逻辑交互
自定义组件的逻辑交互和页面一样,是由 .js 完成的。其包含了数据、方法和属性三个方面。
组件的数据
用于组件模板渲染的私有数据,和页面一样,需要定义到 data 节点中。
组件的方法
组件的方法函数和页面不同。页面写在 page 函数的参数类里,即和 data 同一级别即可。而组件的方法函数需要写在 methods 节点中。
在调用时,尽管函数写在了 methods 节点中,但是使用 this.function 就能够调用了,而不用 this.methods.function 。即使用上和页面的函数一样。
组件的属性
和页面不同,组件相较多了一个写在 properties 节点内的属性。和系统组件及 WeUI 等组件一样,属性用来接收外界传递到组件中的数据。例:
Component({
properties: {
max: {
type: Number,
value: 10
}
}
})
这是完整的属性定义方式,定义了一个名称为 max 的属性,其类型为数值,默认值为10。当不需要设置默认值时,可以使用简化的方式定义属性,例:
Component({
properties: {
max: Number
}
})
在 .js 里调用的时候和使用 data 节点里的数据类似:
this.properties.max
自定义组件的属性的使用方法和系统组件等其他组件一样:
<my-test max="5"></my-test>
data 和 properties 的区别
在小程序中,properties 属性和 data 数据的用法相同,都是可读可写的。当我们定义了 data 和 properties 的成员后,使用 console.log(this.data)
或 console.log(this.properties)
得到的输出效果是一致的,都同时包含了 data 和 properties 的所有成员。
其主要区别在于:
- data 更倾向于存储组件的私有数据
- properties 更倾向于存储外界传递到组件中的数据
因为 data 和 properties 是一样的,所以双方的数据成员均可以用于页面渲染,也可以使用 this.setData()
来进行修改。
自定义组件的数据监听器
数据监听器用于监听和响应任何属性和数据字段的变化,从而执行特定的操作。其用法如下:
Component({
observers: {
'字段A, 字段B': function(字段A的新值, 字段B的新值) {
//do something
}
}
})
例如:
<!--components/test/test.wxml-->
<text>{{n1}} + {{n2}} = {{sum}}</text>
<button bindtap="addN1">点我 n1 + 1</button>
<button bindtap="addN2">点我 n2 + 1</button>
Component({
data: { n1: 0, n2: 0, sum: 0 },
observers: {
'n1,n2': function (n1, n2) { this.setData({ sum: n1 + n2 }) }
},
methods: {
addN1: function () { this.setData({ n1: this.data.n1 + 1 }) },
addN2: function () { this.setData({ n2: this.data.n2 + 1 }) }
}
}
监听器除了监听 data 的数据成员,也可以监听其他的对象的属性。使用中只要将监听的字段名更换成对象.属性名即可。
监听器的触发是使用 this.setData()
方法时触发。即当用于对象和对象属性时:this.setData({对象: 新值})
或 this.setData({'对象.属性: 新值'})
。例:
<!--components/test1/test1.wxml-->
<view style="background-color: rgb({{fullColor}});" class="colorBox">
颜色值:{{fullColor}}
</view>
<view class="middle">
<button size="mini" bindtap="changeR">R</button>
<button size="mini" bindtap="changeG">G</button>
<button size="mini" bindtap="changeB">B</button>
</view>
<view class="middle">R:{{rgb.r}} G:{{rgb.g}} B:{{rgb.b}}</view>
.colorBox{
color:white;
text-align: center;
line-height: 200rpx;
text-shadow: 2rpx 2rpx 2rpx black;
}
.middle{
text-align: center;
}
Component({
data: {
rgb: { r: 0, g: 0, b: 0 },
fullColor: '0, 0, 0'
},
methods: {
changeR() { this.setData({ 'rgb.r': this.data.rgb.r + 5 > 255 ? 255 : this.data.rgb.r + 5 }) },
changeG() { this.setData({ 'rgb.g': this.data.rgb.g + 5 > 255 ? 255 : this.data.rgb.g + 5 }) },
changeB() { this.setData({ 'rgb.b': this.data.rgb.b + 5 > 255 ? 255 : this.data.rgb.b + 5 }) },
},
observers: {
'rgb.r, rgb.g, rgb.b': function (r,g,b){
this.setData({ fullColor: `${r}, ${g}, ${b}` })
}
}
})
需要注意的是,使用 this.setData
改变哪个就要监听哪个,如果改变的是对象而监听对象属性,和改变对象属性而监听对象是不行的。但是可以使用通配符 ** 来监听对象下所有属性的变化,所以上个例子中监听部分可以改为:
observers: {
'rgb.**': function(obj) {
this.setData({ fullColor: `${obj.r}, ${obj.g}, ${obj.b}` })
}
}
自定义组件的纯数据字段
不用于页面渲染的 data 字段,只在当前组件的内部逻辑层使用,则可以定义为纯数据字段,以提升页面更新的性能。其使用方法为:在 Component 的 options 节点中,指定 pureDataPattern 为一个正则表达式。则所有 data 的数据字段名称符合这个正则表达式的字段就被定义为纯数据字段。例如:
Component({
options: { pureDataPattern: /^_/ }, // 所有 _ 开头的字段
data: {
a: true, // 普通的数据字段
_b: true // 纯数据字段
}
})
关于生命周期
组件是包含在页面之中的,所以要研究的生命周期也包含了自身的生命周期和所在页面的声明周期
组件自身的生命周期函数
在小程序中,组件的生命周期函数有以下6种:
生命周期函数 | 参数 | 描述说明 |
---|---|---|
created | 无 | 组件实例被创建时 |
attached | 无 | 组件实例进入页面节点树时 |
ready | 无 | 组件实例在视图层布局完成后 |
moved | 无 | 组件实例被移动到节点树另一个位置时 |
detached | 无 | 组件实例被从节点树移除时 |
error | Object Error | 每当组件方法抛出错误时 |
其中 created 、 attached 、 detached 三个周期比较重要。
- created 在组件创建好时会被触发。此时还不能调用 setData ,通常在这个函数中,只应该用于给组件的 this 添加一些自定义的属性字段。
- attached 在组件初始化完成,进入页面节点树时会被触发。此时 this.data 已经被初始化完毕,绝大多数初始化的工作可以在这时进行(例如发送请求获取初始数据)。
- detached 在组件离开页面节点树后会被触发。例如退出一个页面时,会触发每个自定义组件的此函数,用于做一些清理工作。
生命周期函数定义在 Component 的 lifetimes 节点中。
组件所在页面的生命周期的研究
有时组件的行为依赖于页面状态的变化,此时就需要用到组件所在页面的生命周期。例如在页面 show 触发时,希望生成一个随机的 RGB 颜色。
在自定义组件中,组件所在页面的生命周期函数有3个:
生命周期函数 | 参数 | 描述说明 |
---|---|---|
show | 无 | 组件所在页面被展示时 |
hide | 无 | 组件所在页面被隐藏时 |
resize | Object Size | 组件所在页面尺寸发生变化时 |
监听组件所在页面的生命周期函数,定义到 Component 的 pageLifetimes 节点中。
自定义组件的插槽
在自定义组件的 wxml 结构中,可以提供一个 <slot> 节点,用于承载组件使用者提供的 wxml 结构,这就是插槽。插槽其实就是起一个占位的作用,表示这里使用组件使用者提供的内容。
如果把 <view> 视为一个自定义组件的话, <view> 与 </view> 中间的部分就是插槽了,可以由组件使用者提供具体内容。而提供的内容,则放置在组件 <slot> 所标记的位置。
组件的插槽默认只能使用1个,当需要使用多插槽时,可以在组件的 .js 文件中进行设置:
Component({
options: {
multipleSlots: true
}
}
定义多个插槽时,使用不同的 name 来进行区分:
<view>
<slot name="before"></slot>
<view>一些内容</view>
<slot name="after"></slot>
</view>
在使用的时候,需要用 slot 属性将节点插入到不同的插槽中:
<component-test>
<view slot="before">插入到节点 slot name="before" 的内容</view>
<view slot="after">插入到节点 slot name="after" 的内容</view>
</component-test>
自定义组件的父子组件之间的通信
在自定义组件中使用自定义组件,或在页面中使用自定义组件,就形成了父子关系。
父子组件之间通信方式有3种:
- 属性绑定
用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容的数据 - 事件绑定
用于子组件向父组件传递数据,可以传递任意数据 - 获取组件实例
父组件可以通过this.selectComponent()
获取子组件实例对象来直接访问子组件的任意数据和方法
下面来具体研究
属性绑定
属性绑定就是在父组件中将绑定的数据赋值给子组件的属性,例:
<!-- 父组件的wxml -->
<view>父组件的值 count = {{count}}</view>
<view> 下面是子组件</view>
<my-test2 count="{{count}}"></my-test2>
// 父组件的js
Page({
data: { count: 0 }
})
<!-- 子组件的wxml -->
<view>子组件中 count = {{count}}</view>
// 子组件的js
Component({
propreties: { count: Number }
})
属性绑定是单项的,即如果在子组件中属性数据有变化,父组件相应的数据是不会跟随变化的。
事件绑定
事件绑定用于子组件向父组件传值,使用步骤为:
- 在父组件的 js 中,定义一个函数,这个函数即将通过自定义事件的形式,传递给子组件
- 在父组件的 wxml 中,通过自定义事件的形式,将步骤1的函数引用,传递给子组件
- 在子组件的 js 中,通过调用
this.triggerEvent('自定义事件名称', { /* 参数对象 */ })
,将数据发送到父组件 - 在父组件的 js 中,通过 e.detail 获取到子组件传递的数据
<!-- 父组件的wxml -->
<view>父组件的值 count = {{count}}</view>
<view>下面是子组件</view>
<my-test2 count="{{count}}" bind:sync="syncCount" ></my-test2>
// 父组件的js
Page({
data: { count: 0 },
syncCount(e) { this.setData({ count: e.detail.value }) }
})
<!-- 子组件的wxml -->
<view>子组件中 count = {{count}}</view>
<button bindtap="addCount">+1</button>
// 子组件的js
Component({
propreties: { count: Number },
methods: {
addCount() {
this.setData({ count: this.properties.count + 1 })
this.triggerEvent('sync', { value: this.properties.count })
}
}
})
获取组件实例
可以在父组件中调用 this.selectComponent("id或class选择器")
来获取子组件的实例对象。调用时需传入一个选择器,例如 this.selectComponent(".my-component")
。需注意的是,选择器不能使用标签选择器。
<!-- 父组件的wxml -->
<view>父组件的值 count = {{count}}</view>
<button bindtap="getChild">获取子组件实例</button>
<view>下面是子组件</view>
<my-test2 count="{{count}}" bind:sync="syncCount" class="customA" id="cA"></my-test2>
// 父组件的js
Page({
data: { count: 0 },
syncCount(e) { this.setData({ count: e.detail.value }) },
getChild() {
// 不能使用标签选择器
const chlid = this.selectComponent('.customA') // 或使用 id 选择器 #cA
chlid.setData({ count: chlid.properties.count + 2 }) // 调用子组件的 setData 方法
chlid.addCount() // 调用子组件的 assCount 方法
}
})
子组件的代码同上一个例子。
组件间的代码共享
微信小程序里,可以使用 behaviors 来实现组件间的代码共享。每个 behavior 都包含一组属性、数据、生命周期函数和方法。当组件引用时,其各成员会被合并到组件中。每个组件可以引用多个 behavior , behavior 也可以引用其他的 behavior 。
创建 behavior 实例
调用 Behavior(Object object)
方法就可以创建一个 behavior 实例对象,使用 module.exports
将其共享出去。
module.exports = Behavior({
properties: { },
data: { },
methods: { }
})
使用 behavior
可以在组件中使用 require()
方法引入需要的 behavior,挂载后可以访问 behavior 中的数据或方法。
// 引入 behavior 模块
const myBehavior = require("../../behaviors/my-behivior")
Component({
// 挂载 behavior 模块
behaviors: [myBehavior],
...
})
behavior 中可使用的节点
behavior 可以使用以下节点
可用的节点 | 类型 | 描述 |
---|---|---|
properties | Object Map | 同组件的属性 |
data | Object | 同组件的数据 |
methods | Object | 同自定义组件的方法 |
behavior | String Array | 挂载引入的其他 behavior |
created | Function | 生命周期函数 |
attached | Function | 生命周期函数 |
ready | Function | 生命周期函数 |
moved | Function | 生命周期函数 |
detached | Function | 生命周期函数 |
同名字段的覆盖和组合规则
如果组件和它引用的 behavior 中包含同名的字段时,遵循以下规则:
- 如果有同名的属性 (properties) 或方法 (methods):
- 若组件本身有这个属性或方法,则组件的属性或方法会覆盖 behavior 中的同名属性或方法;
- 若组件本身无这个属性或方法,则在组件的 behaviors 字段中定义靠后的 behavior 的属性或方法会覆盖靠前的同名属性或方法;
- 在 2 的基础上,若存在嵌套引用 behavior 的情况,则规则为:引用者 behavior 覆盖被引用的 behavior 中的同名属性或方法。
- 如果有同名的数据字段 (data):
- 若同名的数据字段都是对象类型,会进行对象合并;
- 其余情况会进行数据覆盖,覆盖规则为: 引用者 behavior > 被引用的 behavior 、 靠后的 behavior > 靠前的 behavior。(优先级高的覆盖优先级低的,最大的为优先级最高)
- 生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用:
- 对于不同的生命周期函数之间,遵循组件生命周期函数的执行顺序;
- 对于同种生命周期函数,遵循如下规则:
behavior 优先于组件执行;
被引用的 behavior 优先于 引用者 behavior 执行;
靠前的 behavior 优先于 靠后的 behavior 执行; - 如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。