- 组件封装的思考
本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分! 前言 在小程序开发的早期,是没有 自定义组件(component),仅有 自定义模板(template) 的。最早接触到组件开发还是在使用 [代码]React[代码]、[代码]Vue[代码] 框架的时候,熟悉以上两个框架的读者,对小程序的组件应该会有熟悉的感觉,机制和写法差不多 为什么要有组件? 对于这个问题,很多人的第一反应也许是:代码复用 的确,代码复用是组件的核心职责,但它还有更大的使命:性能 因为通过组件封装,可以将页面拆分成多个组件,因此较大粒度的页面就被拆分成粒度较小的组件。当一些数据发生变更导致页面变化时,就只需要重新渲染包含该数据的组件即可,而不用渲染整个页面,从而达到了提高渲染性能的效果 [图片] 生命周期 在 [代码]Vue[代码] 中,每个页面是一个 [代码]Vue[代码] 实例,而组件又是可复用的 [代码]Vue[代码] 实例,因此可以理解成,页面和组件是相同的生命周期 而小程序就将页面和组件拆分成两个类:[代码]Page[代码] 和 [代码]Component[代码],因此接收的生命周期函数也是不一样的。比如,[代码]Page[代码] 接收的是:[代码]onLoad[代码]、[代码]onShow[代码]、[代码]onReady[代码]等函数,而 [代码]Component[代码] 则接收 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 等函数 命名风格都不一致,真是让人头大 [图片] 数据传递 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[代码] 可以直接用吗,为什么要重复造轮子 其实,当你的产品想要结合 [代码]Modal[代码] 和 [代码]Button[代码] 的 [代码]open-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[代码] 这个变量被 [代码]Modal[代码] 和 [代码]Page[代码] 同时使用时,会出现不显示的问题。 为了便于描述,我通过描述真实场景来讲解: 当页面需要显示对话框时,[代码]Page[代码] 传递 [代码]visible=true[代码] 给 [代码]Modal[代码] 经过一段时间之后,用户关闭了对话框,此时 [代码]Modal[代码] 将自身的 [代码]visible[代码] 设置为 [代码]false[代码] 当页面需要再次出现对话框时,[代码]Page[代码] 继续传递[代码]visible=true[代码] 给 [代码]Modal[代码],此时发现对话框不会显示 通过分析可以发现,由于 [代码]Page[代码] 两次传递相同的 [代码]visible=true[代码] 给 [代码]Modal[代码] ,因此第二次传递的时候,被 [代码]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.js[代码] 的 [代码]Events[代码] 举例): [代码]// 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(() => { // 当点击取消时 }) } }) [代码] 总结 组件是很好用的机制,也是最常用到的能力 因此日常开发中,应该会遇到各种各样组件封装的问题,平时遇到应该多思考总结一下,对团队和自己都很有帮助!
2020-04-08 - 小程序内用户帐号登录规范调整和优化建议
为更好地保护用户隐私信息,优化用户体验,平台将会对小程序内的帐号登录功能进行规范。本公告所称“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。具体规范要求如下: 1.服务范围开放的小程序 对于用户注册流程是对外开放、无需验证特定范围用户,且注册后即可提供线上服务的小程序,不得在用户清楚知悉、了解小程序的功能之前,要求用户进行帐号登录。 包括但不限于打开小程序后立即跳转提示登录或打开小程序后立即强制弹窗要求登录,都属于违反上述要求的情况; 以下反面示例,在用户打开小程序后立刻弹出授权登录页; [图片] 建议修改为如下正面示例形式:在体验小程序功能后,用户主动点击登录按钮后触发登录流程,且为用户提供暂不登录选项。 [图片] 2.服务范围特定的小程序 对于客观上服务范围特定、未完全开放用户注册,需通过更多方式完成身份验证后才能提供服务的小程序,可以直接引导用户进行帐号登录。例如为学校系统、员工系统、社保卡信息系统等提供服务的小程序; 下图案例为正面示例:校友管理系统,符合规范要求。 [图片] 3.仅提供注册功能小程序 对于线上仅提供注册功能,其他服务均需以其他方式提供的小程序,可在说明要求使用帐号登录功能的原因后,引导用户进行帐号注册或帐号登录。如ETC注册申请、信用卡申请; 如下反面示例,用户在进入时未获取任何信息,首页直接强制弹框要求登录注册ETC,这是不符合规范的。 [图片] 建议修改为如下正面示例所示形式:允许在首页说明注册功能后,提供登录或注册按钮供用户主动选择点击登录。 [图片] 4.提供可取消或拒绝登录选项 任何小程序调用帐号登录功能,应当为用户清晰提供可取消或拒绝的选项按钮,不得以任何方式强制用户进行帐号登录。 如下图所示反面示例,到需要登录环节直接跳转登录页面,用户只能选择点击登录或退出小程序,这不符合登录规范要求。 [图片] 建议修改为下图正面示例形式,在需帐号登录的环节,为用户主动点击登录,并提供可取消按钮,不强制登录。 [图片] 针对以上登录规范要求,平台希望开发者们能相应地调整小程序的帐号登录功能。如未满足登录规范要求,从2019年9月1日开始,平台将会在后续的代码审核环节进行规则提示和修改要求反馈。
2019-07-20