Fork me on GitHub

设计模式(6): 数据抽象与业务封装

概述

最近最近做项目的时候总会思考一些大的应用设计模式相关的问题,我把自己的思考记录下来,供以后开发时参考,相信对其他人也有用。

情景描述

我们在做项目的时候,经常会碰到各种各样的业务情景,然后为了实现这些需求,就不断地在 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 封装了如下业务:

  1. 通过 initState 加入了各种自定义的状态,这个状态有了业务:选中状态,半选中状态和未选中状态
  2. 通过 setCheckState 实现了 点击 的业务。
  3. 通过 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);
  }
},
posted @ 2019-10-29 22:57  馒头加梨子  阅读(435)  评论(0编辑  收藏  举报