从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。查看自己使用的小程序基础库版本,可以通过在开发者工具右侧点击详情查看:

最基本的组件

小程序的组件,其实就是一个目录,该目录需要包含4个文件:

  1. xxx.json
  2. xxx.wxml
  3. xxx.wxss
  4. xxx.js

声明一个组件

首先需要在 json 文件中进行自定义组件声明(将 component 字段设为 true 可这一组文件设为自定义组件)


  1. {
  2.   "component": true
  3. }

其次,在要引入组件的页面的json文件内,进行引用声明


  1. {
  2.   "usingComponents": {
  3.     //自定义的组件名称     : 组件路径,注意是相对路径,不能是绝对路径  
  4.     "component-tag-name": "path/to/the/custom/component"
  5.   }
  6. }

 

这样,在主页面就可以使用了。

相比于vue的组件引入,小程序的方案更简洁。vue组件引入是需要 import 之后,同时在 components 里面注册,而小程序的组件只需要在 .json 里面注册,就可以在 wxml 里面使用。

使用slot

和vue 相同,小程序也有slot概念。

单一slot

在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。


  1. // 主页面内,<addlike>是组件
  2. <addlike item="item" my_properties="sssss">
  3.     <text>我是被slot插入的文本</text>
  4. </addlike>
  5.  
  6. // addlike 组件
  7. <view class="container">
  8.     <view>hello, 这里是组件</view>
  9.     <view>hello, {{my_properties}}</view>
  10.     <slot></slot>
  11. </view>
  12.  
  13. // 渲染后
  14. <view class="container">
  15.     <view>hello, 这里是组件</view>
  16.     <view>hello, {{my_properties}}</view>
  17.     <text>我是被slot插入的文本</text>
  18. </view>

多个slot

如果需要在组件内使用多个slot, 需要在组件js中声明启用:


  1. Component({
  2.   options: {
  3.     multipleSlots: true // 在组件定义时的选项中启用多slot支持
  4.   },
  5.   properties: { /* ... */ },
  6.   methods: { /* ... */ }
  7. })

使用:

  1. // 主页面
  2. <addlike item="item" my_properties="sssss">
  3.     // 在普通的元素上加入 slot 属性,指定slotname, 就可以变成子元素的slot了
  4.     <text slot="slot1">我是被slot1插入的文本</text>
  5.     <text slot="slot2">我是被slot2插入的文本</text>
  6. </addlike>
  7.  
  8. // 子页面
  9. <view class="container">
  10.     <view>hello, 这里是组件</view>
  11.     <view>hello, {{my_properties}}</view>
  12.     <slot name="slot1"></slot>
  13.     <slot name="slot2"></slot>
  14. </view>

Component构造器

 刚才我们说了,一个组件内应该包括js,  wxml, wxss, json 四个文件。wxml 相当于是 HTML,wxss 相当于是 css, 那么js 里面应该写什么呢?

微信官方提供的案例中:


  1. Component({
  2.  
  3.   behaviors: [],
  4.  
  5.   properties: {
  6.    
  7.   },
  8.   data: {}, // 私有数据,可用于模版渲染
  9.  
  10.   // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
  11.   attached: function(){},
  12.   moved: function(){},
  13.   detached: function(){},
  14.  
  15.   methods: {
  16.     onMyButtonTap: function(){
  17.      
  18.     },
  19.     _myPrivateMethod: function(){
  20.      
  21.     },
  22.     _propertyChange: function(newVal, oldVal) {
  23.  
  24.     }
  25.   }
  26. })

里面调用了一个Component构造器。Component构造器可用于定义组件,调用Component构造器时可以指定组件的属性、数据、方法等。具体 Component里面可以放什么东西,如下所示:

properties Object Map 相当于是vue的props,通过该属性,外界向组件内传入数据。组件的对外属性,是属性名到属性设置的映射表,属性设置中可包含三个字段, type 表示属性类型、 value 表示属性初始值、 observer 表示属性值被更改时的响应函数
data Object 组件的内部数据,和 properties 一同用于组件的模版渲染。也就是说,通过this.data 可以同时获得 data 和 properties
methods Object 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 组件事件
behaviors String Array 类似于mixins和traits的组件间代码复用机制,参见 behaviors
created Function 组件生命周期函数,在组件实例进入页面节点树时执行,注意此时不能调用 setData
attached Function 组件生命周期函数,在组件实例进入页面节点树时执行
ready Function 组件生命周期函数,在组件布局完成后执行,此时可以获取节点信息(使用 SelectorQuery )
moved Function 组件生命周期函数,在组件实例被移动到节点树另一个位置时执行
detached Function 组件生命周期函数,在组件实例被从页面节点树移除时执行
relations Object 组件间关系定义,参见 组件间关系
options Object Map 一些组件选项,请参见文档其他部分的说明

 

组件与数据通信

组件化必然要涉及到数据的通信,为了解决数据在组件间的维护问题,vue, react,angular 有不同的解决方案。而小程序的解决方案则简洁很多。

主页面传入数据到组件

properties相当于vue的props,是传入外部数据的入口。

  1. // 主页面使用组件
  2. <a add_like="{{add_like}}">
  3. </a>
  4.  
  5. // 组件a.js 内
  6. Component({
  7.     properties:{
  8.         add_like:{
  9.             type:Array,
  10.             value:[],
  11.             observer:function(){
  12.                 
  13.             }
  14.         }
  15.     }
  16. })

注意: 传入的数据,不管是简单数据类型,还是引用类型,都如同值复制一样(和红宝书里面描述js函数参数传入是值复制还不一样,红宝书里面的意思是:简单数据类型直接复制数值,引用类型复制引用,也就是说在函数内修改参数对象的属性,会影响到函数外对象的属性)。

如果是Vue的props, 则可以通过 .sync 来同步,而在小程序子组件里面,调用this.setData()修改父组件内的数据,不会影响到父组件里面的数据, 也就是说,子组件property的修改,仿佛和父组件没有任何关系。那么,如果是在子组件内修改父组件的数据,甚至是修改兄弟组件内的数据,有没有简单的方法呢?下面会有讲到

 

组件传出数据到主页面

和vue类似,组件间交互的主要形式是自定义事件。

组件通过 this.triggerEvent() 触发自定义事件,主页面在组件上 bind:component_method="main_page_mehod" 来接收自定义事件。

其中,this.triggerEvent() 方法接收自定义事件名称外,还接收两个对象,eventDetail 和 eventOptions


  1. // 子组件触发自定义事件
  2. ontap () {
  3.     // 所有要带到主页面的数据,都装在eventDetail里面
  4. var eventDetail = {
  5. name:'sssssssss',
  6. test:[1,2,3]
  7. }
  8. // 触发事件的选项 bubbles是否冒泡,composed是否可穿越组件边界,capturePhase 是否有捕获阶段
  9. var eventOption = {
  10. composed: true
  11. }
  12. this.triggerEvent('click_btn', eventDetail, eventOption)
  13. }
  14.  
  15. // 主页面里面
  16. main_page_ontap (eventDetail) {
  17.     console.log(eventDetail)
  18.     // eventDetail
  19.     // changedTouches
  20.     // currentTarget
  21.     // target
  22.     // type
  23.     // ……
  24.     // detail   哈哈,所有的子组件的数据,都通过该参数的detail属性暴露出来
  25. }

组件之间数据通信

和vue提出的vuex的解决方案不同,小程序的组件间的通讯简单小巧。你可以和主页面与组件通讯一样,使用自定义事件来进行通讯,当然更简单方便的方法,是使用小程序提供的relations.

relations 是Component 构造函数中的一个属性,只要两个组件的relations 属性产生关联,他们两个之间就可以捕获到对方,并且可以相互访问,修改对方的属性,如同修改自己的属性一样。

  1. Component({
  2.    relations:{
  3.     './path_to_b': {                 // './path_to_b'是对方组件的相对路径
  4.         type: 'child',               //  type可选择两组:parent和child、ancestor和descendant
  5.         linked:function(target){  }  // 钩子函数,在组件linked时候被调用 target是组件的实例,
  6.         linkChanged: function(target){}
  7.         unlinked: function(target){}
  8.         }
  9.     },
  10. })

比如说,有两个组件如代码所示:


  1. // 组件a slot 包含了组件b
  2. <a>
  3.     <b></b>
  4. </a>

他们之间的关系如下图所示:

 

两个组件捕获到对方组件的实例,是通过 this.getRelationNodes('./path_to_a')方法。既然获取到了对方组件的实例,那么就可以访问到对方组件上的data, 也可以设置对方组件上的data, 但是不能调用对方组件上的方法。

 


  1. // 在a 组件中
  2. Component({
  3.     relations:{
  4.         './path_to_b': {
  5.             type: 'child',
  6.             linked:function(target){  }  // target是组件b的实例,
  7.             linkChanged: function(target){}
  8.             unlinked: function(target){}
  9.         }
  10.     },
  11.     methods:{
  12.         test () {
  13.             var nodes = this.getRelationNodes('./path_to_b')
  14.             var component_b = nodes[0];
  15.             
  16.             // 获取到b组件的数据
  17.             console.log(component_b.data.name)
  18.             
  19.             // 设置父组件的数据
  20.             // 这样的设置是无效的
  21.             this.setData({
  22.                 component_b.data.name:'ss'
  23.             })
  24.             // 需要调用对方组件的setData()方法来设置
  25.             component_b.setData({
  26.                 name:'ss'
  27.             })
  28.         }
  29.     }
  30. })
  31.  
  32. // 在b 组件里面
  33. Component({
  34.     relations:{
  35.         './path_to_a': {                      //注意!必须双方组件都声明relations属性
  36.             type:'parent'
  37.         }
  38.     },
  39.     data: {
  40.         name: 'dudu'
  41.     }
  42. })

 

注意:1. 主页面使用组件的时候,不能有数字,比如说 <component_sub1> 或 <component_sub_1>,可以在主页面的json 里面设置一个新名字


  1. {
  2.     "usingComponents":{
  3.         "test_component_subb": "../../../components/test_component_sub2/test_component_sub2"
  4.     }
  5. }

2. relations 里面的路径,比如说这里:



是对方组件真实的相对路径,而不是组件间的逻辑路径。

3. 如果relations 没有关联,那么 this.getRelationNodes 是获取不到对方组件的

4. 本组件无法获取本组件的实例,使用this.getRelatonsNodes('./ path_to_self ') 会返回一个null

4. type 可以选择的 parent 、 child 、 ancestor 、 descendant 

 


现在我们已经可以做到了两个组件之间的数据传递,那么如何在多个组件间传递数据呢?

 

如上图所示,同级的组件b 和同级的组件c , b 和 c 之间不可以直接获取,b可以获取到a, c 也可以获取到a,而a可以直接获取到 b 和 c。所以,如果想获取到兄弟元素,需要先获取到祖先节点,然后再通过祖先节点获取兄弟节点

我在组件b 里面,我需要先找到祖先组件a的实例,然后用祖先组件a的实例的getRelationNodes方法获取到组件c的实例。

看见没?恐怕我们又要写一大堆重复性的代码了。

 

幸好,微信小程序还提供了behavior 属性, 这个属性相当于 mixin,很容易理解的,是提高代码复用性的一种方法。

思路:

假设目前有三个组件,组件a,  组件b, 组件c, 其中组件b和组件c是兄弟组件,组建a是b和c的兄弟组件。为了减少代码的重复性,我们把获取父组件的方法,和获取兄弟组件的方法封装一下,封装在 behavior 的 methods 中。只要是引入该behavior的组件,都可以便捷的调用方法。

实现:

新建一个behavior文件,命名无所谓,比如说relation_behavior.js


  1. // 在 get_relation.js 文件里面
  2. module.exports = Behavior({
  3. methods:{
  4.     // 获取父组件实例的快捷方法
  5. _parent () {
  6. // 如果根据该路径获取到acestor组件为null,则说明this为ancesor
  7. var parentNode =  this.getRelationNodes('../record_item/record_item')
  8. if (parentNode&&parentNode.length !== 0) {
  9. return parentNode[0]
  10. } else {
  11. return this
  12. }
  13. },
  14. // 获取兄弟组件实例的快捷方法
  15. _sibling(name) {
  16. var node = this._parent().getRelationNodes(`../${name}/${name}`)
  17. if (node &&node.length > 0) {
  18. return node[0]
  19. }
  20. }
  21. }
  22. })

然后在组件b, 和 组件c 上引入该behavior,并且调用方法,获取父组件和兄弟组件的实例


  1. // 组件b中
  2. var relation_behavior = require('./path_to_relation_behavior')
  3. Component({
  4.     behaviors:[relation_behavior],
  5.     methods:{
  6.         test () {
  7.             // 获得父组件的实例
  8.             let parent = this._parent()
  9.             
  10.             // 访问父组件的数据d
  11.             console.log(parent.data.name)
  12.             
  13.             // 修改父组件的数据
  14.             parent.setData({
  15.                 name: 'test1'
  16.             })
  17.             
  18.             // 获得兄弟组件的实例
  19.             let sibling = this._sibling('c')
  20.             
  21.             // 访问兄弟组件的数据
  22.             console.log(sibling.data.name)
  23.             
  24.             // 修改兄弟组件的数据
  25.             sibling.setData({
  26.                 name:"test"
  27.             })
  28.         }
  29.     }
  30. })
  31.  
  32. // 组件c中
  33. var relation_behavior = require('./path_to_relation_behavior')
  34. Component({
  35.     behaviors:[relation_behavior],
  36.     methods:{
  37.         test () {
  38.             // 获得父组件的实例
  39.             let parent = this._parent()
  40.             
  41.             // 访问父组件的数据d
  42.             console.log(parent.data.name)
  43.             
  44.             // 修改父组件的数据
  45.             parent.setData({
  46.                 name: 'test1'
  47.             })
  48.             
  49.             // 获得兄弟组件的实例
  50.             let sibling = this._sibling('b')
  51.             
  52.             // 访问兄弟组件的数据
  53.             console.log(sibling.data.name)
  54.             
  55.             // 修改兄弟组件的数据
  56.             sibling.setData({
  57.                 name:"test"
  58.             })
  59.         }
  60.     }
  61. })

 

 

同时需要注意,c和b两个组件,从relations属性的角度来说,是a的后代组件。

但是组件b和组件c 所处的作用域, 都是主页面的作用域,传入的property都是主页面的property,这样就保证了组件数据的灵活性。relations 像一个隐形的链子一样把一堆组件关联起来,关联起来的组件可以相互访问,修改对方的数据,但是每一个组件都可以从外界独立的获取数据。

 

看了这么多理论的东西,还是需要一个具体的场景来应用。

比如说,我们有个一个分享记录图片心情的页面,当用户点击【点赞】的按钮时候,该心情的记录 点赞按钮会变红,下面的一栏位置会多出点赞人的名字。

 

如果不通过组件化,很可能的做法是 修改一个点赞按钮,然后遍历数据更新数据,最后所有记录列表的状态都会被重新渲染一遍。

如果是通过组件化拆分:把点赞的按钮封装为 组件b, 下面点赞人的框封装为组件c, 每一个心情记录都是一个组件a

下面是代码实现


  1. // 在主页面内
  2. <view wx:for='{{feed_item}}'>
  3.     <a item='{{item}}'>
  4.         <b></b>
  5.         <c></c>
  6.     </a>
  7. <view>
  8.  
  9.  
  10. // 在组件a内
  11. var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
  12. Component({
  13.     behaviors:[behavior_relation],
  14.     relations:{
  15.         '../b/b':{
  16.             type: 'descendant'
  17.         }
  18.     }
  19. })
  20.  
  21. // 在组件b内
  22. var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
  23. Component({
  24.     behaviors:[behavior_relation]
  25.     relations:{
  26.         '../a/a':{
  27.             type: 'ancestor'
  28.         }
  29.     },
  30.     data: {
  31.      is_like: false //控制点赞图标的状态
  32.     },
  33.     methods:{
  34.      // 当用户点赞的时候
  35.      onClick () {
  36.      // 修改本组件的状态
  37.      this.setData({
  38.      is_like: true
  39.      })
  40.      // 修改 c 组件的数据
  41.      this._sibling('c').setData({
  42.      likeStr: this._sibling('c').data.likeStr + '我'
  43.      })
  44.      }
  45.     }
  46. })
  47.  
  48. // 在组件c内
  49. var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
  50. Component({
  51.     behaviors:[behavior_relation],
  52.     relations:{
  53.         '../a/a':{
  54.             type: 'ancestor'
  55.         }
  56.     },
  57.     data:{
  58.      likeStr:'晓红,小明'
  59.     }
  60. })

这样,组件b 可以修改组件c中的数据。同时,组件b 和 组件c 又可以通过 properties 和 事件系统,和主页面保持独立的数据通信。


本文转载:CSDN博客