# 小程序实现自定义组件以及自定义组件间的通信

# 快速导航

# 前言

对于组件的封装,在小程序当中对于多个页面的复用有着重要的作用,小程序中注册的每个页面都是独立的

页面的显示view层与逻辑层是通过data进行绑定关联,若需要更改页面中的数据,则通过setData的方式进行修改

那么在小程序中如何自定义组件,以及自定义组件之间是如何进行通信呢

# 实例效果

父组件count:1

通过上面一个简单的数字加减输入框组件,阅读完本文后,您将收获到

⒈ 在小程序中如何自定义组件

⒉ 在小程序页面中如何使用自定义组件

⒊ 父(外部)组件如何向子组件传值

⒋ 子组件如何接受父组件传递过来的值,同时渲染组件

⒌ 子组件内如何进行事件交互,如何向父组件传递数据,影响父组件定义的数据

⒍ 另一种方法父组件获取子组件的数据(非triggerEvent方式,即selectComponent)

⒎ 达到某些条件时,如何禁止viewbindtap事件

⒏ 数字加减输入框代码的优化

# 为什么要自定义组件?

每个小程序页面都可以看成一个自定义组件,当多个页面出现重复的结构时,可以把相同的部分给抽取出来封装成一个公共的组件,不同的部分,在页面中通过传参的方式传入组件,渲染出来即可,达到复用的目的

下面以一个简单的数字加减输入框组件为例,麻雀虽小,但五脏俱全。

# 怎么使用自定义组件?

miniprogram下的目录下创建一个components文件夹,与pages目录同级,这个文件夹专门是用来放自定义组件的

例如:在components目录下创建了一个count文件夹,然后在新建Component,组件名称命名为count,微信开发者工具会自动的创建count组件

自定义组件如下代码所示:

自定义组件定义好了,那么如何使用呢

pages目录下,这里我创建了一个customComponents页面

在要使用页面对应的customComponents.json中的usingComponents自定义组件的名称,同时引入组件的路径

{
  "usingComponents": {
    "count":"/components/count/count"
  }
}
1
2
3
4
5

注意

引入组件:使用相对路径地止也是可以的,如上面引入根路径/也可以,自定义组件名称区分大小写,为了代码的可读性,建议统一小写,多个字母之间用-连字符,例如:count-number

前面是自定义组件的名称,后面是声明创建该组件的路径

  "usingComponents": {
    "count":"../../components/count/count"
  }
1
2
3

那么在对应页面(这里是customComponents),的父组件(外部)wxml中直接调用组件,以标签形式插入就可以了的

你可以将自定义组件看作为自定义的标签,对原生wxml中的view的一种拓展,在自定义组件上可以添加自定义属性,绑定自定义事件.

父组件customComponents-页面的示例代码如下所示

在微信小程序中,使用组件就是这么简单,想要在哪个页面使用,就在哪个页面的xxx.json中声明组件,就可以了的

上面的代码也许看得有点懵逼,下面将逐步拆解的.

# 小程序中组件的通信与事件

在小程序中,组件间的基本通信方式有以下几种

  • wxml数据绑定:用于父组件向子组件指定属性设置数据(以后会单独做一小节的,本篇不涉及)

  • 事件: 用于子组件向父组件传递数据,可以传递任意数据(监听事件是组件通信的主要方式之一,自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件,监听自定义组件事件的方法与监听基础组件事件的方法完全一致)

  • this.selectComponent:如果上面两种方式都无法满足,在父组件中还可以通过this.selectComponent("类名或ID")方法获取子组件的实例对象,这样在父组件中不必通过event.detail的方式获取,可以直接访问子组件任意的数据和方法(后面也会提到)

# 如何向自定义组件内传递数据?

在页面customComponentswxml中,以标签的方式,引用count组件

这个页面,可以视作为父组件,父组件中可以定义当前组件的数据,方法等,如下所示

<count count="{{countNum}}" bind:changeCount="handleCount"></count>
1

定义在父组件中的数据,也可以视作为外部数据,例如:上面的countNum就是挂载在customComponents中的data下的,初始值countNum等于 1

父(外部)组件向子(内)组件传递数据是通过在子组件上自定义属性的方式实现的,自定义属性可以是基本数据类型(数字Number,字符串String,布尔(Boolean)等)与复杂数据类型(对象Object,数组Array)

如本示例中的,count组件上定义了count属性,这个名字并不是固定的,和自定义了changeCount方法

也就是,将countNum变量对象赋值给count属性,给count组件自定义了changeCount方法

注意

handleCount方法是定义在父组件当中的

// 父组件中自定义绑定的事件
  handleCount(event){
    this.setData({
      countNum: event.detail  // 通过event.detail可以拿到子组件传过来的值,如果不重新设置countNum,父组件的countNum是不会更新同步的
    })
  }
1
2
3
4
5
6

# 子组件内如何接收父组件传递过来的值?

在子组件内,Component构造器可以用于定义组件,调用Component构造器时,可以指定组件的属性,数据,方法等

其中properties对象接收外部(父)组件传过来的自定义属性数据,可以是对象,数组,基本数据类型等

data是定义当前组件内的私有变量数据,可用于组件模板的渲染

温馨提示

至于变量数据对象是定义在 properties 下还是挂载在 data 下,具体要看组件的使用

凡是外部传递过来的数据,那么就放置在properties中,而若是当前(内部)的组件模板渲染,那么就挂载在data

而这个data下面挂载的数据,又分为普通数据字段,和纯数据字段,其中后者纯数据字段变量用_开头

这些指定的纯数据字段需要在Component构造器的options对象中指定pureDataPattern的一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段

在小程序组件中,某些情况下,一些data中的字段,也包括setData中设置的字段,有些只参与业务逻辑,不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用

这样的数据字段被称为纯数据字段,它可以定义在全局作用域中,也可以定义在data下,若定义在data下,它会被记录在this.data中,而不会参与任何界面的渲染过程

如下所示

Component({
  options: {
    pureDataPattern: /^_/, // 指定所有 _ 开头的数据字段为纯数据字段
  },
  data: {
    a: true, // 普通数据字段
    _b: true, // 纯数据字段
  },
  methods: {
    myMethod() {
      this.data._b; // 纯数据字段可以在 this.data 中获取
      this.setData({
        c: true, // 普通数据字段
        _d: true, // 纯数据字段
      });
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面的组件中的纯数据字段不会被应用到wxml

<view wx:if="{{a}}"> 这行会被展示 </view>
<view wx:if="{{_b}}"> 这行不会被展示 </view>
1
2

properties对象中接收外部组件传递过来的数据

// components/count/count.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    count: Number, // 在这里接收外部组件传递过来的属性,同时确定传递过来数据的类型,类型有String,Boolean,Object,Array等
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

那么在内部组件中如何渲染呢,直接将properties下的变量对象与wxml中通过{{}}插值表达式进行绑定关联就可以了的 如下所示input中的count

<view>
  <view class="count-container">
    <view>-</view>
    <input type="number" value="{{count}}" />
    <view>+</view>
  </view>
</view>
1
2
3
4
5
6
7

以上就完成了子组件接收父组件外部传过来的值,然后在组件中渲染的过程

那么想要操作当前组件的数据,对加减输入框进行动态操作,在组件元素上绑定相应的事件操作就可以了的

<view>
  <view class="count-container">
    <view bindtap="reduce" class="{{count == 1? 'btn-disabled': ''}}}">-</view>
    <input bindinput="handleInput" type="number" value="{{count}}" />
    <view bindtap="add">+</view>
  </view>
</view>
1
2
3
4
5
6
7

+,-上添加了bindtap方法,进行业务逻辑的处理,如下所示

点击即可查看加减操作逻辑
// components/count/count.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    count: Number,
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {
    // 减操作
    reduce() {
      console.log('减');
      var count = this.data.count - 1;
      if (count < 1) {
        count = 1;
      }
      this.setData({
        count,
      });
    },
    // 加操作
    add() {
      console.log('加');
      var count = this.data.count + 1;
      this.setData({
        count,
      });
    },

    // 监听表单输入
    handleInput(event) {
      console.log(event);
      this.setData({
        count: event.detail.value,
      });
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 子组件如何向父组件传递数据,影响父组件定义的数据

小程序,组件与组件之间是相互隔离,独立的,通过上面的一顿操作,数字框架的加减确实已经实现了的,但是若在外部组件中,想要获取拿到子组件中的数据,如果不通过某些手段,子组件中的数据是影响不到父组件的

因为小程序当中数据的传递是单向的,也就是父组件传递数据给子组件,是通过在组件上添加自定义属性实现的,而在子组件内部的properties中接收自定义组件的属性

如果你接触过vue,与react等框架,你会发现有惊人的相似之处,vue中是props接收,而reactthis.props接收

小程序正是借鉴了它们的思想.

那父组件想要拿到子组件中的数据,换而言之,子组件又如何向父组件传递数据呢?影响到父组件中定义的初始化数据呢,该怎么办呢

父组件想要拿到子组件的数据,通过在组件上绑定自定义监听事件

监听事件

  • 事件是视图层到逻辑层的通讯方式
  • 可以将用户的行为反馈到逻辑层进行处理
  • 可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数
  • 事件对象可以携带额外信息,如 id, dataset, touches

事件系统是组件间通信的主要方式之一。自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件,监听自定义组件事件的方法与监听基础组件事件的方法完全一致

如下所示

<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- 或者可以写成 -->
<component-tag-name bind:myevent="onMyEvent" />
1
2
3
4

在本文示例中如下所示,bind:changeCount="handleCount",就是绑定了自定义changeCount事件,这句话的含义,相当于是 在count组件上监听绑定了一个changeCount事件,当触发changeCount事件时,就会调用后面父组件中定义的handleCount方法

<count
  class="count"
  count="{{countNum}}"
  bind:changeCount="handleCount"
></count>
1
2
3
4
5

而在父组件中,声明handleCount方法,可以通过event事件对象拿到子组件中的数据

Page({
  handleCount: function(event) {
    event.detail; // 自定义组件触发事件时提供的detail对象
  },
});
1
2
3
4
5

既然在父组件中通过监听自定义事件,那么在子组件内部如何触发该事件呢

触发事件

自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名detail对象事件选项

如下所示

Component({
  properties: {},
  methods: {
    onTap: function() {
      var myEventDetail = {}; // detail对象,提供给事件监听函数
      var myEventOption = {}; // 触发事件的选项
      this.triggerEvent('自定义事件名称myEvent', myEventDetail, myEventOption);
    },
  },
});
1
2
3
4
5
6
7
8
9
10

在本示例中:

点击即可查看 triggerEvent 事件方法
// components/count/count.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    count: Number,
  },

  /**
   * 组件的初始数据
   */
  data: {},

  /**
   * 组件的方法列表
   */
  methods: {
    // 减
    reduce() {
      console.log('减');
      var count = this.data.count - 1;
      if (count < 1) {
        count = 1;
      }
      this.setData({
        count,
      });
      this.triggerEvent('changeCount', count);
    },
    // 加
    add() {
      console.log('加');
      var count = this.data.count + 1;
      this.setData({
        count,
      });
      this.triggerEvent('changeCount', count);
    },
    // 监听输入框
    handleInput(event) {
      console.log(event);
      this.setData({
        count: event.detail.value,
      });
      this.triggerEvent('changeCount', event.detail.value);
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

至于为什么有三次triggerEvent,每次加,减都是子组件内部的操作,外部组件想要实时获取到,那么就需要触发父组件监听的自定义方法的,同时triggerEvent方法的第二个参数代表的就是当前子组件的内部所要传递给父组件的数据

当子组件触发了changeCount方法,会调用父组件的handleCount方法,在父组件中进行重新setData父组件中的初始化数据,就可以更新同步到页面上了的

这个过程虽然有些绕,曲折,对于初学者,需要自行感悟,理一下的

这个triggerEvent,就相当于vue中的this.$emit('绑定在父组件自定义事件名称',携带的数据)方法的,而在React中是通过this.props.方法接收,调用父组件的方法

注意

在父组件中监听的自定义方法(如上示例的changeCount),是通过triggerEvent进行触发的,是放置在子组件内部要监听的方法内的,而不是定义在methods方法中

changeCount() { // 这是错误的写法,有些小伙伴误以为自定义方法,就必须要写成方法这种形式的,它只是一个名称而已

}
1
2
3

通过以上的代码示例,文字介绍,就知道子组件如何向父组件传递数据,影响父组件定义的数据

子组件想要传递数据给父组件,影响父组件初始化定义的数据

  • 首先需要在父组件上的自定义组件上设置监听自定义方法
  • 在子组件内部的事件方法中,通过triggerEvent触发父组件中的自定义事件名称,同时,triggerEvent第二个参数为携带所需的数据
  • 在父组件中定义的方法,即可通过事件对象event.detail的方式获取到子组件中传递过来的值
  • 在父组件中,重新setData数据即可更新父组件中初始化的数据,从而渲染到页面上

以上是通过triggerEvent的方式,并携带参数传递给自定义事件,从而在父组件中可以通过event.detail的方式拿到子组件中的数据

其实,还有另外一种简便的方法,同样可以拿到

# 父组件通过this.selectComponent拿到子组件中的数据

# 前提条件

需要在父组件的引用自定义组件上,添加classid

例如:在count组件上添加了一个classcount

<count
  class="count"
  count="{{countNum}}"
  bind:changeCount="handleCount"
></count>
1
2
3
4
5

那么,在父组件中的handleCount中里调用 this.selectComponent,获取子组件的实例数据

调用时需要传入一个匹配选择器 selectorclassId都可以,如,this.selectComponent('类或ID')

本示例中是this.selectComponent('.count'),如下示例代码所示

 handleCount(){
    console.log(this.selectComponent('.count'));
    var count = this.selectComponent('.count');
    this.setData({
      countNum: count.data.count  // 重新赋值setData countNum数据
    })

  }
1
2
3
4
5
6
7
8

这种方法也是可以的,在小程序当中也很常用

# 如何禁止掉viewbindtap事件?

在做数字加减输入框时,对于减到某个数值时,想要禁用状态,遇到类似的情况时,要么把view换成button

然后当达到某个条件时,将button的状态设置为disabled属性也是可以的

但是若不用button呢,该怎么实现呢

如果用view代替button,虽然在某个条件下,可以达到样式上是禁用状态,但是如果你在测试时,这个操作仍然是在不断触发的

这样显然有些鸡肋

解决这个问题: 借助了 css3 中的一个非常好用的特性 在指定的类上添加一个pointer-events: none就可以了的

.btn-disabled {
  pointer-events: none; /*微信小程序view禁掉bindtap事件,阻止点击,它是css3的一个属性,指定在什么情况下元素可以成为鼠标事件的target(包括鼠标的样式)*/
}
1
2
3

这个属性,作用在view上,可以组织bindtap的点击

# 数字加减输入框代码的优化

在上面实现数字加减框组件,分别给,绑定了两个方法,多次出现了triggerEvent

<view>
  <view class="count-container">
    <view
      bindtap="handleCount"
      data-count="-1"
      class="{{count == 1? 'btn-disabled': ''}}}"
      >-</view
    >
    <input bindinput="handleInput" type="number" value="{{count}}" />
    <view bindtap="handleCount" data-count="1">+</view>
  </view>
</view>
1
2
3
4
5
6
7
8
9
10
11
12

在上面的加减中绑定一个相同的事件方法handleCount,而通过设置data-xx属性,判断是加还是减 那么在逻辑代码中

methods: {
  handleCount(event){
      var count = event.currentTarget.dataset.count;
      count  = this.data.count+Number(count);  // 这里之所以要把count,转换为Number,因为自定义属性的count是字符串,+加号字符串拼接,会变成一个字符串
      if(count < 1) {
        count = 1;
      }
      this.setData({
        count: count
      })
      this.triggerEvent('changeCount', count);
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

上面的代码相比于前面写的代码,就要简便得不少,看着舒服得多

在做这种类似的业务逻辑时,不妨可以通过这种方式对代码进行优化的

# 结语

本文主要是讲到了在小程序中父子组件之间如何进行通信,父组件向子组件传递数据是通过在引用组件上绑定自定义属性实现的

而子组件是通过在properities对象中进行接收的,子组件如何向父组件传递数据,它是通过在引用组件上绑定监听自定义事件,然后在子组件的事件方法内,通过this.triggerEvent方法进行触发自定义事件名,并可以携带子组件内的数据,在父组件中的函数中

可以通过event.detail可以拿到子组件中传递给父组件的值,从而重新在setData数据,就可以更新父组件中的初始化数据

这个关系虽然有点绕,至于重要性不言而喻.如果您有任何疑问,关注微信itlanCoder公众号,在文章下方留言,一起学习谈论,欢迎添加微信suibichuanji进行咨询.

# 相关链接

# 视频学习

关注公众号

一个走心,有温度的号,同千万同行一起交流学习

加作者微信

扫二维码 备注 【加群】

扫码易购

福利推荐