评论

组件封装的思考

封装 对话框 Modal 引发的思考

本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分!

前言

在小程序开发的早期,是没有 自定义组件(component),仅有 自定义模板(template) 的。最早接触到组件开发还是在使用 ReactVue 框架的时候,熟悉以上两个框架的读者,对小程序的组件应该会有熟悉的感觉,机制和写法差不多

为什么要有组件?

对于这个问题,很多人的第一反应也许是:代码复用

的确,代码复用是组件的核心职责,但它还有更大的使命:性能

因为通过组件封装,可以将页面拆分成多个组件,因此较大粒度的页面就被拆分成粒度较小的组件。当一些数据发生变更导致页面变化时,就只需要重新渲染包含该数据的组件即可,而不用渲染整个页面,从而达到了提高渲染性能的效果

生命周期

Vue 中,每个页面是一个 Vue 实例,而组件又是可复用的 Vue 实例,因此可以理解成,页面和组件是相同的生命周期

而小程序就将页面和组件拆分成两个类:PageComponent,因此接收的生命周期函数也是不一样的。比如,Page 接收的是:onLoadonShowonReady等函数,而 Component 则接收 createdattachedready 等函数

命名风格都不一致,真是让人头大

数据传递

Vue

Vue 的组件间数据传递的机制是这样的:父组件通过property传递数据给子组件,而子组件通过事件通知的形式传递数据给父组件

在页面包含的组件结构还比较简单的时候,这样的机制还是比较好用的。但是,随着业务的复杂度逐渐上升,组件嵌套的层数递增,会出现数据层层传递的困境

为了解决这个问题,Vue 推出了 Vuex 这样的状态管理工具,集中式存储、管理应用的所有组件的状态。并提出了“单向数据流”的理念:

小程序

小程序同样有类似的机制,property和事件。此外还提供了获取 子组件实例 的方法:selectComponent() 和 定义组件间关系的字段 relations

其中常用的就是获取子组件实例,比如:

<parent-component>
    <child-component id="child"></child-component>
</parent-component>

此时,在parent-component组件中可以直接获取child-component的实例:

Component({
    attached() {
        let $child = this.selectComponent('#child')

        // $child.doSomeThing()
    }
})

实战

背景

制作一个 对话框(modal) 组件

也许有的读者会感到困惑,官方不是有提供 wx.showModal 可以直接用吗,为什么要重复造轮子

其实,当你的产品想要结合 ModalButtonopen-type 能力时,你就会明白重复造轮子的必要性以及wx.showModal的局限性

属性定义

对话框的常见属性可以参考wx.showModal

除此以外,其中关键的一个属性就是 表示对话框当前的显示状态:visible

此时,有两种选择,第一种是将这个变量存在页面上,通过property传递给Modal组件;另外一种,就是作为Modal组件data中的一员

property传递

通过property传递的话,就相当于将 Modal 的控制权交到对应的页面,举例:

<!-- home.wxml -->

<modal visible="{{visible}}" />
// home.js

Page({
    data: {
        visible: false
    },
    toggleModal() {
        this.setData({ visible: !this.data.visible })
    }
})

此时对应的 Modal

// modal.js

Component({
    properties: {
        visible: {
            type: Boolean,
            value: false,
            observer(newVal, oldVal) {
                this.setData({ visible: newVal })
            }
        }
    }
})

这里和Vue框架有个差异,Vue对于传进来的property会自动赋值,而小程序则需要自己手动赋值

问题与办法

visible 这个变量被 ModalPage 同时使用时,会出现不显示的问题。

为了便于描述,我通过描述真实场景来讲解:

  1. 当页面需要显示对话框时,Page 传递 visible=trueModal
  2. 经过一段时间之后,用户关闭了对话框,此时 Modal 将自身的 visible 设置为 false
  3. 当页面需要再次出现对话框时,Page 继续传递visible=trueModal此时发现对话框不会显示

通过分析可以发现,由于 Page 两次传递相同的 visible=trueModal ,因此第二次传递的时候,被 Modal 直接忽略掉了。

这个问题也很好解决,大致思路就是保证每次传递的值不同即可:

  • 传递的值前面加上时间戳,组件再将时间戳移除(比较直观,但是不方便)
  • 利用对象不相等的机制,数据传递只传对象,不传基础数据类型(比如{ visible: true } !== { visible: true })

组件自身属性

这种是我推荐的方案。将 visible 属性交由组件 Modal 自行管理:

// modal.js

Component({
    data: {
        visible: false
    },
    methods: {
        show() {
            this.setData({ visible: true })
        }
    }
})

由于父组件或者当前页面可以直接获取组件的实例,因此可以直接调用组件的setData,如:

let $modal = this.selectComponent('#modal')

$modal.setData({ visible: true })

但是不建议这样使用,而是组件暴露方法让外部调用:

let $modal = this.selectComponent('#modal')

$modal.show()

组件的事件

通常,对话框都会有按钮,一个或两个。

因此 Modal 需要与父组件通过 事件(event) 的方式传递信息:当前点击了取消还是确定按钮:

<!-- home.wxml -->

<modal id="modal" bind:btntap="handleModalTap" />
// home.js

Page({
    showModal() {
        let $modal = this.selectComponent('#modal')

        $modal.show()
    },

    // 其他方法

    handleModalTap(e) {
        let { type } = e.detail

        // type = cancel or confirm
    }
})

Modal 的构造函数则是这样的:

// modal.js

Component({
    data: {
        visible: false
    }
    methods: {
        handleBtnTap(e) {
            let { type } = e.target.dataset

            this.triggerEvent('btntap', { type })
        }
    }
})
<!-- modal.wxml -->

<view class="wrapper">
    <!-- 省略其他结构 -->
    <view class="foot" bindtap="handleBtnTap">
        <button data-type="cancel">取消</button>
        <button data-type="confirm">确定</button>
    </view>
</view>

这样设计 Modal 组件,的确可以满足使用,但是不够好用

因为展示对话框时使用的是 showModal 而用户操作之后又是通过另外一个方法 handleModalTap 反馈的。当一段时间之后回看这样的代码,会发现这种写法存在思维的中断,不利于代码维护

所以,我建议结合 Promise 来封装 Modal

省略事件

由于展示对话框之后,用户必然要操作,因此可以在 showModal 的时候,通过 Promise 返回对应的操作信息即可

另外,需要引入发布订阅机制(以下使用 Node.jsEvents 举例):

// modal.js

const EventEmitter = require('events');
const ee = new EventEmitter();

Component({
    data: {
        visible: false
    },

    methods: {
        show() {
            this.setData({ visible: true })

            return new Promise((resolve, reject) => {
                ee.on('cancel', () => {
                    reject()
                })
                ee.on('confirm', () => {
                    resolve()
                })
            })
        },

        handleBtnTap(e) {
            let { type } = e.target.dataset

            ee.emit(type)
            this.triggerEvent('btntap', { type })
        }
    }
})

此时,在 Page 即可这样展示对话框:

// home.js

Page({
    onLoad() {
        let $modal = this.selectComponent('#modal')

        $moda.show().then(() => {
            // 当点击确认时
        }).catch(() => {
            // 当点击取消时
        })
    }
})

总结

组件是很好用的机制,也是最常用到的能力

因此日常开发中,应该会遇到各种各样组件封装的问题,平时遇到应该多思考总结一下,对团队和自己都很有帮助!

点赞 4
收藏
评论

3 个评论

  • 李才哥
    李才哥
    2020-07-30

    通俗易懂

    2020-07-30
    赞同 1
    回复 1
    • LeeJim🌀
      LeeJim🌀
      2020-07-30
      感谢支持
      2020-07-30
      回复
  • 小肥羊
    小肥羊
    2020-04-09
    好文
    2020-04-09
    赞同 1
    回复 1
    • LeeJim🌀
      LeeJim🌀
      2020-04-09
      2020-04-09
      回复
  • Soba
    Soba
    2020-04-09

    为什么不用wx:if

    2020-04-09
    赞同
    回复
登录 后发表内容