概述
最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。
情景描述
我们在做项目的时候,经常会碰到各种各样的业务情景,然后为了实现这些需求,就不断地在 vue 单文件组件里面加代码来实现,最终业务越来越多,单文件组件越来越大,非常难以维护。
解决方案
我们都知道,vue 是通过数据来处理视图的,所以很多业务可以抽象成只处理数据,然后这些业务可以再抽象成 class 来进行业务封装。
event-bus
举个例子来说,vuex 或者 redux 这些状态管理的库,就是用的这个思想,把数据层脱离出去,带来的好处是简化了组件之间的数据流动。它们的源码有些复杂,我们以 event-bus 来举例说明。
首先,我们可以自己实现一个 bus 类,这个类能够储存数据,还能够进行事件的分发与监听。
import Vue from 'vue';
import Bus from 'xxxx';
Vue.prototype.$bus = new Bus();
然后,分别在组件 A 和 B 里面,我们可以监听事件和分发事件。
// 组件A -- 监听事件
created() {
this.$bus.on('xxxx', this.xxx);
},
beforeDestroy() {
this.$bus.off('xxxx', this.xxx);
},
// 组件B -- 分发事件
methods: {
xxxx() {
this.$bus.emit('xxxx', this.xxx);
}
}
这样,即使处于不同层级,组件 A 和 B 也能流畅的进行数据交互。
抽象方法
我们抽象一下实现方法,我们先把业务抽象为数据和对数据的操作,然后在组件之外实现一个 class,最后用这个 class 进行保存数据和业务处理。
上面这个例子把这个 class 放在了 Vue 实例上面,可能没有那么明显,下面举一个把它放在单文件组件里面的例子。
cascader
这一段参考了 element-cascader 的实现。
比如说,我们要自己实现一个 cascader,要怎么做?
我们上面提到过,我们对 cascader 的操作其实就是对数据的操作,所以我们可以把整个数据抽象出来,然后给它加上选中的业务功能:
import { capitalize } from '@/utils/util';
export default class Node {
constructor(data, parentNode) {
this.parent = parentNode || null;
this.initState(data);
this.initChildren(data);
}
initState(data) {
// 加上本身的属性
for (let key in data) {
if (key !== 'children') {
this[key] = data[key];
}
}
// 自定义属性
this.isChecked = false;
this.indeterminate = false;
// 用于自动取消
this.isCheckedCached = false;
this.indeterminateCached = false;
}
initChildren(data) {
this.children = (data.children || []).map(child => new Node(child, this));
}
setCheckState(isChecked) {
const totalNum = this.children.length;
const checkedNum = this.children.reduce((c, p) => {
const num = p.isChecked ? 1 : (p.indeterminate ? 0.5 : 0);
return c + num;
}, 0);
this.isChecked = isChecked;
this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
}
doCheck(isChecked) {
this.broadcast('check', isChecked);
this.setCheckState(isChecked);
this.emit('check', isChecked);
}
broadcast(event, ...args) {
const handlerName = `onParent${capitalize(event)}`;
this.children.forEach(child => {
if (child) {
child.broadcast(event, ...args);
child[handlerName] && child[handlerName](...args);
}
});
}
emit(event, ...args) {
const { parent } = this;
const handlerName = `onChild${capitalize(event)}`;
if (parent) {
parent[handlerName] && parent[handlerName](...args);
parent.emit(event, ...args);
}
}
onParentCheck(isChecked) {
if (!this.disabled) {
this.setCheckState(isChecked);
}
}
onChildCheck() {
const validChildren = this.children.filter(child => !child.disabled);
const isChecked = validChildren.length
? validChildren.every(child => child.isChecked)
: false;
this.setCheckState(isChecked);
}
}
上面实现的 class 封装了如下业务:
- 通过 initState 加入了各种自定义的状态,这个状态有了业务:选中状态,半选中状态和未选中状态。
- 通过 setCheckState 实现了 点击 的业务。
- 通过 broadcast 和 emit 实现了 父子组件联动 的业务。
当然,实际情形可能比这个更加复杂,我们只需要在上面的代码中加入各种状态和处理方法即可。
更进一步
上面封装的底层的业务,再高一层,我们可能有 搜索、自动选中 等业务,这个时候要怎么办呢?
方法是在 Node 类和单文件组件之间再封装一层,来实现这些业务,示例代码如下:
export default class Store {
constructor(data) {
this.nodes = data.map(nodeData => new Node(nodeData));
}
// 自动选中
autoSelect(query, label) {
}
// 搜索
search(searchString) {
}
}
然后我们可以在单文件组件里面直接使用它:
data() {
return {
store: null;
};
},
watch: {
data(newVal) {
this.store = new Store(newVal);
}
},