微信小程序组件化的解决方案

4,165 阅读12分钟

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

最基本的组件

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

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

声明一个组件

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

{ "component": true}

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

{  
    "usingComponents": {   
      "component-tag-name": "path/to/the/custom/component"  
    }
}

component-tag-name 字段是自定义的组件名称

后面的是组件路径,注意是相对路径,不能是绝对路径

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

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

使用slot

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

单一slot

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

// 主页面内,<addlike>是组件
<addlike item="item" my_properties="sssss">   
	<text>我是被slot插入的文本</text>
</addlike> 

// addlike 组件
<view class="container">    
	<view>hello, 这里是组件</view>    
	<view>hello, {{my_properties}}</view>    
	<slot></slot>
</view> 

// 渲染后
<view class="container">    
	<view>hello, 这里是组件</view>    
	<view>hello, {{my_properties}}</view>    
	<text>我是被slot插入的文本</text>
</view>

多个slot

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

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

使用:

// 主页面
<addlike item="item" my_properties="sssss">
    // 在普通的元素上加入 slot 属性,指定slotname, 就可以变成子元素的slot了
    <text slot="slot1">我是被slot1插入的文本</text>
    <text slot="slot2">我是被slot2插入的文本</text>
</addlike>
 
// 子页面
<view class="container">
    <view>hello, 这里是组件</view>
    <view>hello, {{my_properties}}</view>
    <slot name="slot1"></slot>
    <slot name="slot2"></slot>
</view>

Component构造器

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

微信官方提供的案例中:

Component({
 
  behaviors: [],
 
  properties: {
   
  },
  data: {}, // 私有数据,可用于模版渲染
 
  // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
  attached: function(){},
  moved: function(){},
  detached: function(){},
 
  methods: {
    onMyButtonTap: function(){
     
    },
    _myPrivateMethod: function(){
     
    },
    _propertyChange: function(newVal, oldVal) {
 
    }
  }
})

里面调用了一个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,是传入外部数据的入口。

// 主页面使用组件
<a add_like="{{add_like}}">
</a>
 
// 组件a.js 内
Component({
    properties:{
        add_like:{
            type:Array,
            value:[],
            observer:function(){
                
            }
        }
    }
})

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

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

组件传出数据到主页面

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

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

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

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

组件之间数据通信

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

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

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

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

// 组件a slot 包含了组件b
<a>    
  <b></b>
</a>

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

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

// 在a 组件中
Component({
    relations:{
        './path_to_b': {
            type: 'child',
            linked:function(target){  }  // target是组件b的实例,
            linkChanged: function(target){}
            unlinked: function(target){}
        }
    },
    methods:{
        test () {
            var nodes = this.getRelationNodes('./path_to_b')
            var component_b = nodes[0];
            
            // 获取到b组件的数据
            console.log(component_b.data.name)
            
            // 设置父组件的数据
            // 这样的设置是无效的
            this.setData({
                component_b.data.name:'ss'
            })
            // 需要调用对方组件的setData()方法来设置
            component_b.setData({
                name:'ss'
            })
        }
    }
})
 
// 在b 组件里面
Component({
    relations:{
        './path_to_a': {                      //注意!必须双方组件都声明relations属性
            type:'parent'
        }
    },
    data: {
        name: 'dudu'
    }
})

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

{
    "usingComponents":{
        "test_component_subb": "../../../components/test_component_sub2/test_component_sub2"
    }
}
  1. relations 里面的路径,比如说这里:

img

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

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

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

  3. type 可以选择的 parentchildancestordescendant

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

img

如上图所示,同级的组件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

// 在 get_relation.js 文件里面
module.exports = Behavior({
	methods:{
	    // 获取父组件实例的快捷方法
		_parent () {
			// 如果根据该路径获取到acestor组件为null,则说明this为ancesor
			var parentNode =  this.getRelationNodes('../record_item/record_item')
			if (parentNode&&parentNode.length !== 0) {
				return parentNode[0]
			} else {
				return this
			}
		},
		// 获取兄弟组件实例的快捷方法
		_sibling(name) {
			var node = this._parent().getRelationNodes(`../${name}/${name}`)
			if (node &&node.length > 0) {
				return node[0]
			}
		}
	}
})

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

// 组件b中
var relation_behavior = require('./path_to_relation_behavior')
Component({
    behaviors:[relation_behavior],
    methods:{
        test () {
            // 获得父组件的实例
            let parent = this._parent()
            
            // 访问父组件的数据d
            console.log(parent.data.name)
            
            // 修改父组件的数据
            parent.setData({
                name: 'test1'
            })
            
            // 获得兄弟组件的实例
            let sibling = this._sibling('c')
            
            // 访问兄弟组件的数据
            console.log(sibling.data.name)
            
            // 修改兄弟组件的数据
            sibling.setData({
                name:"test"
            })
        }
    }
})
 
// 组件c中
var relation_behavior = require('./path_to_relation_behavior')
Component({
    behaviors:[relation_behavior],
    methods:{
        test () {
            // 获得父组件的实例
            let parent = this._parent()
            
            // 访问父组件的数据d
            console.log(parent.data.name)
            
            // 修改父组件的数据
            parent.setData({
                name: 'test1'
            })
            
            // 获得兄弟组件的实例
            let sibling = this._sibling('b')
            
            // 访问兄弟组件的数据
            console.log(sibling.data.name)
            
            // 修改兄弟组件的数据
            sibling.setData({
                name:"test"
            })
        }
    }
})

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

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

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

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

img

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

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

img

下面是代码实现

// 在主页面内
<view wx:for='{{feed_item}}'>
    <a item='{{item}}'>
        <b></b>
        <c></c>
    </a>
<view>
 
 
// 在组件a内
var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
Component({
    behaviors:[behavior_relation],
    relations:{
        '../b/b':{
            type: 'descendant'
        }
    }
})
 
// 在组件b内
var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
Component({
    behaviors:[behavior_relation]
    relations:{
        '../a/a':{
            type: 'ancestor'
        }
    },
    data: {
        is_like: false  //控制点赞图标的状态
    },
    methods:{
        // 当用户点赞的时候
        onClick () {
            //  修改本组件的状态
            this.setData({
                is_like: true
            })
            // 修改 c 组件的数据
            this._sibling('c').setData({
                likeStr: this._sibling('c').data.likeStr + '我' 
            })
        }
    }
})
 
// 在组件c内
var behavior_relation = require('../../relation_behavior.js)  //这里引入上文说的Behavior
Component({
    behaviors:[behavior_relation],
    relations:{
        '../a/a':{
            type: 'ancestor'
        }
    },
    data:{
        likeStr:'晓红,小明'
    }
})

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

字节跳动大大大大量量量量招人了

字节跳动(杭州|北京|上海)大量招人,福利超级棒,薪资水平秒杀 BAT,上班不打卡、每天下午茶、免费零食无限供应、免费三餐(我念下菜单,大闸蟹鲍鱼扇贝海鲜烤鱼片黑椒牛柳咖喱牛肉麻辣小龙虾)、免费健身房、入职配touch bar15寸顶配全新mbp、每月还有租房房补。 这次真的机会多多,年后研发人数要扩招n倍,技术氛围好,大牛多,加班少,还犹豫什么?快发简历到下方邮箱,就现在!

仅仅是一小部分的jd链接如下, 更多的欢迎加微信~

前端jd: job.toutiao.com/s/bJM4Anjob…

后端jd: job.toutiao.com/s/bJjjTsjob…

测试jd: job.toutiao.com/s/bJFv9bjob…

产品jd: job.toutiao.com/s/bJBgV8job…

前端实习生: job.toutiao.com/s/bJ6NjAjob…

后端实习生: job.toutiao.com/s/bJrjrkjob…

持续招聘大量前端、服务端、客户端、测试、产品,实习社招都阔以

简历发 dujuncheng@bytedance.com,建议加微信 dujuncheng1,可以聊天聊地聊人生,请注明来自掘金以及要投递哪里的岗位