从小程序基础库版本 1.6.3 开始, 小程序支持简洁的组件化编程. 查看自己使用的小程序基础库版本, 可以通过在开发者工具右侧点击详情查看
最基本的组件
小程序的组件, 其实就是一个目录, 该目录需要包含 4 个文件:
xxx.JSON
xxx.wxml
xxx.wxss
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 的组件间代码复用机制,参见 |
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() 方法接收自定义事件名称外, 还接收两个对象, eventDetail 和 eventOptions.
- // 子组件触发自定义事件
- 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"
- }
- }
relations 里面的路径, 比如说这里:
是对方组件真实的相对路径, 而不是组件间的逻辑路径.
如果 relations 没有关联, 那么 this.getRelationNodes 是获取不到对方组件的
本组件无法获取本组件的实例, 使用 this.getRelatonsNodes('./ path_to_self') 会返回一个 null
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
- // 在 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 像一个隐形的链子一样把一堆组件关联起来, 关联起来的组件可以相互访问, 修改对方的数据, 但是每一个组件都可以从外界独立的获取数据.
看了这么多理论的东西, 还是需要一个具体的场景来应用.
比如说, 我们有个一个分享记录图片心情的页面, 当用户点击 [点赞] 的按钮时候, 该心情的记录 点赞按钮会变红, 下面的一栏位置会多出点赞人的名字.
如果不通过组件化, 很可能的做法是 修改一个点赞按钮, 然后遍历数据更新数据, 最后所有记录列表的状态都会被重新渲染一遍.
如果是通过组件化拆分: 把点赞的按钮封装为 组件 b, 下面点赞人的框封装为组件 c, 每一个心情记录都是一个组件 a
下面是代码实现
- // 在主页面内
- <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 和 事件系统, 和主页面保持独立的数据通信.
来源: https://juejin.im/post/5c3f2294e51d45522b4f3a40