巧用设计模式构建可配置Vue前端应用-活动页生成系统实践

3,252

背景

活动页,是各个互联网公司一个头疼的问题。为了跟上对手的脚步,需要时不时就要搞点花样。频繁重复的作业对于前端团队来讲是一件非常头疼的事情。活动发布系统是迫切需要的,让运营人员自己通过这个配置化活动页发布系统完成活动的发布。

但是配置化活动发布系统对灵活性,扩展性,维护性都具有很大的挑战。像阿里,腾讯,京东都有各自的活动发布配置系统。但是对于我们这种小团队人手短缺,质量又无法与巨头相提并论,实在是不小的挑战。更何况我们还要同时兼容3个活动方的平台(展现相同,业务逻辑和交互形式不同),对于我们来说更加是一个不小的挑战。

技术难点

我们的技术难点并不是如何制作出精美的活动页,而是如何去抽象各个组件的模型,以便于让运营可以通过各种基础UI组件来实现千变万化活动页满足各种脑洞大开的活动场景。

活动页上的一个图片,同样是图片,他可以是一个平铺的广告入口,可以是分会场入口,可以是一个调查问卷的入口,也可以是一个报名入口,同时也可能是某个参与活动的报名入口。

用户点击后会有多种不同的交互形式,跳转新页面,弹出弹层,锚点到指定位置,甚至是提交表单。

展现形式也会多种多样,轮播,滑动,平铺,堆叠。

针对不同的用途,难以预测的交互行为,和不同的展现形式,这将会是一个非常复杂而庞大的组件,如果我们开发成一个组件,以后运营说不定有会想出什么倒霉点子,开发多个组件,会有很多重复,因此,我们设计的系统如何支撑他们的业务?如何尽可能减少二次开发的工作量,降低维护成本,将是这套系统需要考虑的重中之重。

设计是抽象的过程,抽象的目的是解耦和隔离

如何去构建一个易于维护,易于扩展的系统,其实是一件很难的事情,虽然我现在说我的设计是可以做到易于维护,易于扩展的,但是保不齐以后会被运营和产品的脑洞打脸。但是架构是渐进的。尽可能的抽象,解耦各个模型,而设计模式就是这方面最好的指导。

解耦

每一个组件将会有自己的行为,UI,以及自己的交互逻辑,我们可以将其分为展现形式与交互行为。展现形式是组件在客户那里的样子,交互逻辑就是用户操作的时候进行的一系列业务逻辑。这两个逻辑单元组成一个基本的组件:

我们将一个基本组件单元分解成3个组件 -- UI组件(展现形式),交互组件(交互逻辑),组件单元(基本单位)。组件单元包含UI组件和交互组件。因此我们就可以通过使用不同的UI组件和交互组件组合的方式来组装出来具有各种不同展现形式,不同交互逻辑的前端组件了。这一方法叫做 -- 桥接模式(实现代码)。即:将抽象部分与它的实现部分分离,使它们都可以独立地变化。同时还使用了组合模式(实现代码)

因此我们对于每一个基本组件单元就可以设计一个下面的数据结构:

{
    name: '组件名称',
    id: '组件ID',
    type: '组件类型',
    uiComp: {
        name: 'UI组件名称',
        style: ''
    },
    logicComp: '交互组件名称'
}

使用Vue根据这个结构构建出页面就是如下代码:

<template>
    <Element>
        <template :is="ui组件名称" :style="组件样式"></template>
        <template :is="交互组件名称"></template>
    </Element>
</template>
<script>
export default {
    ...
}
</script>

这里遵循了单一职责原则,UI组件仅负责展现,交互组件负责交互反馈。实现了UI与逻辑的隔离。如果将来有新的交互逻辑,我们就增加一个逻辑组件,如果增加了UI展现,就加一个UI组件,任何UI组件都可以和任意同一个单元组件内的交互组件相互组合。也就满足了里氏替换原则。

抽象

通过Vue的template组件,我们很好的实现了组件的动态组合,我们可以认为template就是一个抽象工厂(代码实现)。根据我们的需要为我们提供不同的组件。而在单元组件中根本不需要关心它具体是什么。所有的基本组件单元均被抽象成一个高阶组件,它不需要关心内部的UI组件和交互组件是什么的。而对于整个页面,他只关心这些基本的单元组件的构成。并不需要不关心这些单元组件是干什么的。

但是因为UI展现和逻辑是单独的两个不同组件,那么我们如何将他们打通呢?因为UI组件才能接受到用户的动作,而动作的反馈都在交互逻辑组件中。中介模式(代码实现),即:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。这个中介对象就是我们的负责组合UI组件和交互组件的组件单元组件,它通过Vue的props来向不同的组件分发元数据与结果数据:

<template>
    <Element>
        <template :is="ui组件名称" :style="组件样式" :props="用于显示的结果数据" @action="用户操作回调函数"></template>
        <template :is="交互组件名称" :props="元数据" :payload="数据载体" @init="初始化完成回调函数" @finish="用户操作响应回调"></template>
    </Element>
</template>
<script>
export default {
    data() {
        return {
            '元数据',
            '结果数据',
            '数据载体'
        }
    },
    methods: {
        '初始化完成回调函数' (payload) {
            if (payload.type === 'ok') {
                this.'结果数据' = payload.data;
            }
        },
        '用户操作回调函数' (payload) {
            if (payload.type === '干了什么') {
                this.'数据载体' = payload.data;
            }
        },
        '用户操作响应回调' (payload) {
            if (payload.type === '结果状态') {
                this.'结果数据' = payload.data;
            }
        }
    }
}
</script>

业务逻辑组件:

<template>
    <Element>
        <Toast />
    </Element>
</template>
<script>
export default {
    props: {
        '元数据': Object
    },
    watch: {
        payload: {
            handle() {
                this.$emit('finish', action)
            },
            deep: true
        }  
    },
    mounted() {
        this.$emit('init', action)
    }
}
</script>

这里借助了Vue的props实现了观察者模式(代码实现),子组件通过观察props的变化来通过中介(单元组件)向各个子组件(ui组件,逻辑组件)进行通信。同时,每一个子组件又是一个访问者模式(代码实现)的访问者实现,通过payload类似redux中action的使用,我们就起到了统一接口的目的,从而也就实现了多态。

结束

目前,这个系统还不是很强大,甚至过于简单,有可能有些人会提出质疑,我还是那句话,架构是渐进的。随着运营人员的脑洞越来越大,产品的胆子越来越大,这个系统也会不断成长,里面的东西也会变得越来越复杂。

这个设计并没有像其他大厂那样提供功能强大而复杂功能全面的定制化组件,而是需要通过增加新组件(水平扩展)的方式来对系统进行扩展。这主要是本着【开放封闭原则】,对修改说【不】,改展现形式,就增加新的UI组件,改业务逻辑,就增加新的逻辑组件,这样才可以保持系统不至于过于臃肿和复杂以及难以维护和扩展。保持着每个组件都是很简单,并且逻辑和显示分离,遵循接口隔离原则。这样任何人加入这个项目,都可以很容易的接手系统进行扩展。而且即很大程度上降低了活动页的开发成本,也避免了绝大多数的重复劳动,同时延长了系统的使用周期,最大化压榨系统的价值。