微信小程序不支持自定义组件,只提供了一个非常受限制的模板功能,尤其缺乏了开发产品中最重要的几个功能:
- 模板内的数据只能由当前页面传递,无法预先设置一些初始化数据以达到复用的目的;
- 模板内的数据变化无法通知到当前页面,也就是说模板不知道谁在使用它,只能在当前页面定义方法绑定在模板内部的组件事件上来处理交互逻辑
- 模板内部无法预先设置一些内部逻辑代码。
这些局限性分分钟叫人抓狂,这个模板功能只是实现了UI层复用的功能,所有的逻辑代码都需要在页面上重新写一遍。开发和维护的效率一下子回到了解放前,再也没法愉快地思考业务需求了,先把这个坑补补。 大路貌似走不通,只能另辟弯道,曲线救国了。 解决思路:所有的页面都通过全局Page函数来定义,它提供了一个onLaunch的触发事件,看来能在这里想想办法。我们大致需要这样的自定义组件或者说是模板:
/*这是一个组件基类*/
module.exports = class BaseComp {
constructor({ key, page, data = {} }) {
this.key = key
this.page = page
this.data = data ... }
/*当组件内部数据变化后,能在这里更新页面*/
update(){}
/*当页面触发事件时,组件也应该能捕捉到*/
onLoad() {} onReady() {} onShow() {} onHide() {} onUnload() {}}
/*这是一个文本框组件*/
module.exports = class FormInput extends BaseComp {
constructor( { key, page, data }) {
/*这里可以定义一些默认数据*/
data = Object.assign({
type: 'text',
label: '',
placeholder: '',
value:'',
...
focus: false
}, data)
super( { key, page, data })
}
onfocus(e){...}
onblur(e){...}
onchange(e){...}
}
小程序的视图(wxml)和js无法像webpack那样打包在一起,所以只能利用现有的模板功能,把小程序原生组件组合成一个自定义组件。
<template name="formInput">
<view>
<label>{{label}}</label>
<input type="{{type}}" class="{{focus?' active':''}}"
placeholder-class="{{placeholderClass}}"
placeholder="{{placeholder}}"
value="{{value}}"
data-key="{{key}}"
data-index=""
bindfocus="_EventProxy"
bindblur="_EventProxy" />
<text>{{message}}</text>
</view>
</template>
1. 写个Page函数,在App初始化之前中引入,把小程序的全局Page替换掉。在这个Page函数内根据配置参数实例化自定义组件,把当前页面赋给组件的page属性,并向组件传入当前页面的'onLoad','onReady'等事件;
const Page = this.Page
this.Page = function( config ) {
if( config.components ) {
let onLoad = config.onLoad
config.onLoad = function() {
for( let key in config.components ) {
let opts = config.components[ key ]
opts.key = key
opts.page = this
config.components[ key ] = new comp[ opts.is ]( opts )
config.components[ key ].onLoad( arguments )
}
if( onLoad ) {
onLoad.call( this, arguments )
}
}
......
2. Page函数在当前页面上添加一个统一的事件代理(Proxy),自定义组件内部的小程序原生组件上的事件全部指向这个代理方法,由这个事件代理根据事件对象判断是由哪个自定义组件、自定义组件内的哪个原生组件触发的,触发的是哪个事件,然后再调用自定义组件的处理方法去执行内部逻辑。
config._EventProxy = function( e ) {
let dset = e.currentTarget.dataset
let comp = config.components[ dset.key ]
/* 组件上定义的事件名必须为 on + event.type
data-index用来区分多个同名事件 */
let event = 'on' + e.type + dset.index
if( comp && comp[ event ] ) {
comp[ event ].call( comp, e )
}
}
最后再执行小程序的Page函数
Page( config )
3. 自定义组件基类中实现了更新页面数据的公共方法
update() {
let origin = this.page.data[this.key] || {}
let data = Object.assign({}, origin, this.data)
this.page.setData({
[this.key]: data
})
}