# 第10章 如何正确地进行抽象

关于抽象,它其实是一个通用的能力。而掌握了抽象的能力后,当你用到应用、页面中,不管是组件化、配置化还是数据流等处理,都可以水到渠成。

对于写业务代码,很多前端都觉得枯燥无趣,且认为容易达到技术瓶颈。其实并不是这样的,几乎所有被我们称之为“技术需求”、“技术工具”的开发,它都来自于业务的需要,Vue 也是。而在前端领域,业务开发就真的只是调节样式、拼接模板、绑定事件、接口请求、更新页面这些内容吗?其实也不是的,在学习完本章之后,你会发现前端的世界也可以这么精彩,而 Vue 也可以这么好玩。

我们下面将按照将页面划分成模块、模块抽象成数据、对应用进行配置化,以及组件的抽象、组件配置化的顺序,来探索这样一种新玩法吧。

# 10.1 页面划分成模块

产品在设计一个页面的时候,会根据内容和功能的不同,设计出不同的模块,然后再拼凑成页面。对于前端同学来说,拿到一个设计好的交互稿或者设计图之后,需要进行逆向拆解,我们要把一个页面按照功能和内容划分出一个个的模块。而我们拆出来的模块并不一定完全跟产品设计的一致,会根据不同的粒度、视觉和易抽象程度来进行划分。

# 10.1.1 什么是模块

我们来看看常见的应用页面,这里我截取了自己的博客来进行说明:

image
图 10-1 博客页面

我们可以直观地根据视觉感受来划分下:

image
图 10-2 博客页面模块划分

大致可以分为三大块:

  • 头部:快速导航栏
  • 左侧:内容板块
  • 右侧:推广导航板块

其实论坛类、博客类的页面大多如此,我们再来看看用 Vuepress 搭建的前端游乐场(跟 Vue 官网很像):

image
图 10-3 前端游乐场页面

除此之外,还有视频类、电商类等各种角色的网站,大家有空也可以去看看,思考下里面是怎么划分的。或许你会觉得,想这些有什么用呢?这对我们平时的工作有什么帮助吗?其实观察 -> 思考 -> 总结也是有意思的事情,可以多一种角度来思考自己的工作内容,也能提高写代码的趣味性。如果你要认真地把这个过程放置到你的工作中,也可以找到很多提升工作效率的方法,也会让你的路越走越顺畅。

模块的划分,其实最终在代码中呈现出来的,常常是组件的划分。

# 10.1.2 组件与模块

《第4章 Vue 组件的使用》中,我们详细地介绍了组件。虽然组件和模块是不一样的两个概念,但是模块有些时候也可以作为一个组件来维护,而模块也可以是属于某个组件、或是包含哪些组件的关系。模块更多是在是视觉上呈现的划分,而组件则更偏向功能上的划分。一个模块是否可以成为一个组件,需要看这个模块是否拥有属于自己的状态、数据、事件等对于组件的封装也都已经在 4.4 章节中有详细的描述。

# 10.2 模块抽象成数据

想象一下,在把数据与逻辑分离到极致的时候,我们看一个应用/页面,会看到一具静态的逻辑躯壳,以及动态的数据组成。数据如灵魂般地注入到应用/页面中,可使其获得生命。关于如何进行数据的抽离,通常来说可以把变化的部分分离和抽象,然后通过注入的方式,来实现具体的功能和展示。

是否有点抽象?这样的一个分离过程,也可以理解为我们写好的一个页面,需要从后台获取到数据,然后根据数据渲染出对应的内容。在这里,页面就是静态的,而获取的数据就是动态的。从另外一个角度来说,除了后台请求的数据,我们在 Vue 中通过data绑定的数据都可以抽离。关于这些可抽离的数据,我们来简单识别和划分一下。

# 10.2.1 状态数据

在一个应用的设计里,我们可能会拥有多个组件,每个组件又各自维护着自己的某些状态,同时部分状态相互影响着,叠加起来呈现出应用最终的整体状态。这些状态,都可以通过数据的方式来表示,我们简单称之为状态数据。怎么定义状态数据?最浅显或是最直观的办法就是,这些数据可以直接影响页面的呈现,如对话框的出现、隐藏,标签的激活、失活,长流程中的进行中步骤等,都可以作为状态数据。在 Vue 里面,状态数据会经常与 v-showv-if等逻辑结合使用。

我们的应用,大多数都是呈现树状结构,一层层地往下分解,直到无法分割的某个简单功能。同时,我们的组件也会呈现出来这样树状的方式,状态是跟随着组件维护,某个功能状态属于组件自己,最外层的状态则属于整个应用,当然这个应用可以看做是一个组件。

image
图 10-4 博客模块划分

如图 10-4,图中的每个模块都可以附着着一个“是否可见”的状态。我们的应用状态整体上也是会呈现树状的方式,与我们的组件相对应,就像 DOM 节点树、CSS 规则树和渲染树的关系。

# 10.2.2 动态数据

我们还有很多的数据,如内容、个人信息等,都是需要我们从数据库拉取回来的。这种需要动态获取然后用于展示或是影响展示的一些数据,我们可以称作动态数据。动态数据不同于状态数据,并不会跟随着应用的生命周期而改变,也不会随着应用的关闭而消失。它们独立存在于外界,通过注入的方式进入应用,并影响具体的展示和功能逻辑。

和状态数据不一样,动态数据并不一定呈现为树状的形式。它可以是并行的,可以是联动关系,随着注入的地方不一样,最终在应用中形成的结构也会不一致。我们可以简单理解为每个动态数据都是平等的。

image
图 10-5 文章列表

如图 10-5,这里每篇文章内容,都是单独的一份从后台请求的数据注入。其实博客通常是静态模板,不存在从后台请求的情况,这里打个比喻,大家可以想象下社区里的文章、知乎帖子、微博等等。

# 10.2.3 将数据与应用抽离

要怎么理解将数据与应用抽离呢?形象点形容,就像是我们一个公司,所有的桌子椅子装修和电脑都是静态的,它们相当于一个个的组件,同时每个办公室也可以是一个大点的组件或是模块。那么在我们这个公司里:

  • 状态数据:椅子的位置、消耗的电量、办公室的照明和空调状态等
  • 动态数据:员工等各种人员流动

当然,公司里没有人员流动的时候,似乎就是个空壳。每天上班的时候,一个个的程序员来到公司里,给公司注入灵魂,公司得以运作。要说将数据和应用抽离,作用到这个例子中大概是这个样子的:

# 将公司和人分开(下班后)
--------------------------------------------------------
                         公司
---------------------------  ---------------------------
|                                                      |  人           人
|                                                      |      人          人
|                         办公楼                        |           人
|                                                      |   人     人     人  人
|                                                      |   人      人   人
---------------------------  ---------------------------


# 在公司正常运作的时候
--------------------------------------------------------
                         公司
--------------------------------------------------------
|   人     人             人   人       人     人 人    |
|           人            人   人     人          人    |
|        人    人    办公楼   人   人          人       |
|     人    人                人  人     人     人  人  |
|     人     人         人      人     人      人   人  |
--------------------------------------------------------

当然,人不只是站在办公楼里面这么简单,更多的,人会与各种物件进行交互和反馈,人与人之间也会相互交流和影响。但是这样简单的管理,很容易造成公司的混乱,所以我们会把人员有规律有组织地分别隔离到每个办公室、隔间里面:

# 按照组织进行分隔
--------------------------------------------------------
                         公司
--------------------------------------------------------
|   人   |  人   人  |         | 人    人    |  人 人    |
|   人   |     人    |         |    人  人   |   人  人  |
|--------    人  人  |  办公楼  |  人    人    --------- |
|     人 |  人       |         |  人     人  |   人  人  |
|     人 |   人   人 |         | 人     人   |  人   人  |
--------------------------------------------------------

这就是我们要做的,不只是如何划分数据、将数据与应用抽离,我们还需要将其有规律地管理。所以,这大概是接下来的要讲的内容。我们知道哪些数据需要抽离、如何将数据抽离出来,同时,我们还需要知道,这些数据在抽离出来之后,该怎么去进行管理。

# 10.2.4 适度的管理

与组件的封装不适宜过度一样,数据的抽象、隔离、管理,也是需要适度的。当我们的应用很小,只有简单的功能的时候,我们甚至不需要对这些状态、数据什么的进行特殊的管理,甚至几个简单的变量就可以搞定了。随着应用组件数量变多,我们开始有了组件的作用域,当组件需要通信,我们可以通过简单的事件机制、或是共享对象的方式来进行交互。

当我们的项目越做越大,要在上百的状态、上万的数据里要按照想要的方式去展示我们的应用,这时候一个状态管理工具则可以轻松解决乱糟糟的数据流问题。关于在 Vue 中怎样进行数据和状态管理的更多内容,会在 11 章讲述。

# 10.3 深入理解配置化

配置化的思想,如今也不仅仅存在于前端或者是某个领域。所有的系统和架构设计,都可以用领域抽象、数据抽离、配置化等方式,搭建灵活配置、模块解耦的系统,前端也不例外。

# 10.3.1 可配置的数据

数据的配置,或许大家会比较熟悉,我们很多的管理端都是用来进行数据配置的。而数据配置的最终效果,则包括影响展示端的页面内容、应用的状态控制等。例如文案、活动、功能展示,都可以通过数据配置进行控制。

# 应用中的可配置数据

最常见的数据配置,大概是前面说过的一些内容配置,文案、说明等,为此还产生了运营这样的职位。常见的运作方式,是搭起一整套的运营管理平台,除了一些简单的文字或是数据以外,广告内容、推荐位等,都可以通过平台进行配置。

# 代码中的可配置数据

有些时候,我们也会在代码里面抽象出一些可配置的数据。例如,这个需求产品要求查询一周的数据,我们在开发的时候并不会将 7 天写死在涉及计算的每行代码中,而是将天数配置为 7 天,设置成全局变量:

const QUERY_DAY_NUM = 7;

这样,当需要在紧急情况支持其他天数(五一、国庆、过年等假期)的时候,我们就可以只需要改动这里就可以了。更方便的情况是,这个数据的配置可以放在管理端,通过管理端下发到后台,前端展示的时候只需要从后台获取具体的天数就可以了。

# 文件里的可配置数据

虽然我们可配置的数据单独抽出来维护,但常常是将这个配置也直接写到代码里。那么如果我们需要调整这些配置,调整后还需要重新打包部署,这种情况开销大、效率低。所以在一些时候,我们会把这样的可配置数据,单独写到某个文件里维护,这个文件不合我们的代码打包到一起。当需要调整的时候,只需要单独下发一个配置文件就好了。

# 10.3.2 可配置的接口

关于接口的配置化,目前来说见过的不是特别多。毕竟现实场景中,我们的很多数据和接口并不是简单的增删查改这样的功能,很多时候还需要在接口返回前后,做一系列的逻辑处理。简单地说,很多的业务接口场景复用性不高,前后端除去协议、基础规范的定义之后,很少再能进行更深层次的抽象,导致接口配置化的改造成本较大。

配置化的实现有两点很重要的东西:规范解决方案。如果说目前较好的从前端到后台的规范,可能是 GraphQL 和 Restful 了,大家不熟悉的也可以去看看。当然,或许有些团队已经实现了,也希望能看到一些相关的解决方案。

# 10.3.3 可配置的页面

页面的配置化,可能也已经不少见了。像我刚出道的时候,也写过一个拖拽的 Demo(如图 10-6),当时自己实现完,信心倍增。大概每个前端的成长过程中,都会伴随着一个管理端配置化的需求吧。

image
图 10-6 拖拽生成 H5 的 Demo

有些时候,一些页面比较简单,里面的板块、功能比较相似,可能文案不一致、模块位置调整了、颜色改变等等。虽然说复制粘贴再改一改,很多时候也能满足要求,但是我们通过抽象和配置化,就可以把重复性的工作交给机器,省下来的精力可以做更多富有创造性的工作。这种页面的配置,基本上有两种实现方式:
(1) 配置后生成静态页面的代码,直接加载生成的页面代码。
(2) 写通用的配置化逻辑,在加载页面的时候拉取配置数据,动态生成页面。

基于 SEO 和实现复杂度各种情况,第一种方式大概是目前比较常用的,第二种的实现难度会稍微大一些。第一种方式,很多适用于一些移动端的模版页面开发,例如简单的活动页面、商城页面等等。第二种的话,更多的是一些管理平台的实现,毕竟大多数都是增删查改,形式无非列表、表单和菜单等。配置化的核心大概是场景分析和功能拆解,所以抛开使用场景来做一个所谓“通用”的配置化是不现实的。但是如果把问题范围局限在解决特定的场景,就可以做出合适的配置化功能。

# 10.4 组件配置化

这里我们来讲一下简单的配置化组件的实现,关于组件的封装前面我们也讲过了。下面的组件配置化实现说明,我们拿这样一个卡片组件来作为例子:

image
图 10-7 卡片组件样式

# 10.4.1 可配置的数据

首先是数据的配置,这大概是最基础的。当我们在封装组件的时候,很多数据都是通过作用域内的变量来动态绑定的,例如 Vue 里面则是通过datapropscomputed等实例属性来维护 scope 内的数据绑定。作为一个卡片,内容是从外面注入的,所以我们这里使用props来获取:

<template>
  <div>
    <h2>{{cardInfo.question}}</h2>
    <div>
      <div v-if="cardInfo.withImage"><img :url="cardInfo.imageUrl" /></div>
      <div>{{cardInfo.content}}</div>
    </div>
    <div>
      <span @click="likeIt()">点赞</span>
      <span @click="keepIt()">收藏</span>
    </div>
    <div>
      <p v-for="comment in cardInfo.comments">{{comment}}</p>
    </div>
  </div>
</template>
<script>
  export default {
    name: "my-card",
    props: {
      // 传入数据
      cardInfo: {
        type: Object,
        default: () => {}
      }
    },
    data() {
      return {
        isContextShown: false
      };
    },
    methods: {
      likeIt() {},
      keepIt() {}
    },
    mounted() {}
  };
</script>

上面只简单地实现部分的卡片内容,我们在使用的时候,只需要将数据传入到这个组件中就可以了:

<my-card :cardInfo="cardInfo"></my-card>

在这里,cardInfo就是我们用来配置卡片内容的数据,我们可以从后台拉取了所有卡片的列表信息,然后配合v-for来绑定和生成每一个卡片内容。

# 10.4.2 可配置的样式

样式的配置,通常是通过class来实现的。其实这更多地是对 CSS 进行配置化设计,与我们的 HTML 和 Javascript 关系则比较少。样式的配置,需要我们考虑 CSS 的设计,通常来说我们有两种方式:
(1) 根据子元素匹配,来描述 CSS。
(2) 根据子 class 匹配,来描述 CSS。

# 根据子元素配置 CSS

这是以前比较常用的一种方式,简单地说,就是通过 CSS 匹配规则中的父子元素匹配,来完成我们的样式设计。例如,我们有个模块:

<div class="my-dialog">
  <header>I am header.</header>
  <section>
    blablablabla...
  </section>
  <footer>
    <button>Submit</button>
  </footer>
</div>

样式则会这样设计:

.my-dialog {
  background: white;
}
.my-dialog > header {}
.my-dialog > section {}
.my-dialog > footer {}

或者说用 LESS 或是 SASS:

.my-dialog {
  background: white;
  > header {}
  > section {}
  > footer {}
}

通过这种方式设计,或许我们在写代码的时候会稍微方便些,但是在维护上面很容易踩坑。只需要调整一次页面的 DOM 结构,就可以让你改 CSS 改到崩溃。

# 根据子 class 配置 CSS

其实相对于匹配简单的父子和后代元素关系,使用 class 来辅助匹配,可以解决 DOM 调整的时候带来的问题。这里我们使用 BEM 作为例子来解释下大概的想法吧。BEM 的意思就是块(block)、元素(element)、修饰符(modifier),是一种前端命名方法论。大家感兴趣可以去搜一下。简单说,我们写 CSS 的时候就是这样的:

.block{}
.block__element{}
.block--modifier{}

表 10-1 BEM 命名规范

命名 说明 举例
B-block
可以与组件和模块对应的命名
如 card、dialog 等
E-element 元素 如 header、footer 等
M-modifier 修饰符
可视作状态等描述
如 actived、closed 等

这样的话,我们上述的代码则会变成:

<div class="my-dialog">
  <header class="my-dialog__header">I am header.</header>
  <section class="my-dialog__section">
    blablablabla...
  </section>
  <footer class="my-dialog__footer">
    <button class="my-dialog__btn--inactived">Submit</button>
  </footer>
</div>

搭配 LESS 的话,其实样式还是挺容易写的:

.my-dialog {
  background: white;
  &__header {}
  &__section {}
  &__footer {}
  &__btn {
    &--inactived
  }
}

其实大家看了下,就发现这样的弊端了。我们在写 HTML 的时候,需要耗费很多的时间来写这些 class 名字。更麻烦的的是,当我们需要切换某个元素状态的时候,判断条件会变得很长,像:

<button :class="isActived ? 'my-dialog__btn--actived' : 'my-dialog__btn--inactived'">Submit</button>

这样写太长了,维护性上、可读性上都不大友好。当然我们还可以这样使用:

<!-- 自己拼 -->
<button :class="'my-dialog__btn--' + (isActived ? 'actived' : 'inactived')">Submit</button>
<!-- 也可以把修饰符部分脱离 -->
<button class="my-dialog__btn" :class="isActived ? 'actived' : 'inactived'">Submit</button>

这样会稍微好一些。BEM 的优势和弊端也都是很明显的,大家也可以根据具体的团队规模、项目规模、使用场景等,来决定要怎么设计。当然,如今很多框架都支持样式的作用域,通常是通过在 class 里添加随机 MD5 等,来保持局部作用域的 class 样式,或者也可以使用 Shawdow DOM 来进行隔离。

# 10.4.3 可配置的状态和展示

可配置的状态和展示,更多时候是指某些模块的状态、展示的效果又是如何等。例如,我们需要一个对话框,其头部、正文文字、底部按钮等功能都可支持配置:

<div class="my-dialog" :class="{'show': isShown}">
  <header v-if="cardInfo.title">{{cardInfo.title}}</header>
  <section v-if="cardInfo.content">{{cardInfo.content}}</section>
  <footer>
    <button v-for="button in cardInfo.buttons">{{button.text}}</button>
  </footer>
</div>

我们可以通过cardInfo.title来控制是否展示头部,可以通过cardInfo.buttons来控制底部按钮的数量和文字。这只是最简单的实例,我们可以通过配置,来控制出完全不一样的展示效果。搭配样式的配置,更是能让组件出神入化。当然,很多时候我们组件的封装是需要与业务设计相关,这样维护性能也会稍微好一些,这些前面也都有说到过。

# 10.4.4 可配置的功能

功能的配置,其实很多也与状态和展示的配置相关。但是我们有些与业务相关的功能,则可以结合展示、功能来定义这样的配置。

举个例子,我们的这个卡片可以是视频、图片、文字三者其中之一的卡片:

  • 视频:点击播放
  • 图片:点击新窗口查看
  • 文字:点击无效果

这种时候,我们可以两种方式:
(1) 每个功能模块自己控制,同时通过配置控制哪个功能模块的展示。
(2) 模块展示会有些耦合,但在点击事件里,根据配置来进行不同的事件处理,获取不同的效果。

对应维护性和可读性来说,第一种方式会获得更好的效果。如果问什么情况下会用到第二种,大概是同样的呈现效果,在不同场景下的逻辑功能不一样时,使用会比较方便。

功能配置化这块就不过多描述了,毕竟这块需要与业务场景密切结合,后面《第13章 实战:表单配置化实现》也有介绍。大家更多地可以思考下,自己的项目中,是否可以有调整的空间,来使得整体的项目更好维护呢?