关于抽象,它其实是一个通用的能力。而掌握了抽象的能力后,当你用到应用、页面中,不管是组件化、配置化还是数据流等处理,都可以水到渠成。
对于写业务代码,很多前端都觉得枯燥无趣,且认为容易达到技术瓶颈。其实并不是这样的,几乎所有被我们称之为“技术需求”、“技术工具”的开发,它都来自于业务的需要,Vue 也是。而在前端领域,业务开发就真的只是调节样式、拼接模板、绑定事件、接口请求、更新页面这些内容吗?其实也不是的,在学习完本章之后,你会发现前端的世界也可以这么精彩,而 Vue 也可以这么好玩。
我们下面将按照将页面划分成模块、模块抽象成数据、对应用进行配置化,以及组件的抽象、组件配置化的顺序,来探索这样一种新玩法吧。
1. 页面划分成模块
产品在设计一个页面的时候,会根据内容和功能的不同,设计出不同的模块,然后再拼凑成页面。对于前端同学来说,拿到一个设计好的交互稿或者设计图之后,需要进行逆向拆解,我们要把一个页面按照功能和内容划分出一个个的模块。而我们拆出来的模块并不一定完全跟产品设计的一致,会根据不同的粒度、视觉和易抽象程度来进行划分。
1.1 什么是模块
我们来看看常见的应用页面,这里我截取了自己的博客来进行说明:
图 10-1 博客页面
我们可以直观地根据视觉感受来划分下:
图 10-2 博客页面模块划分
大致可以分为三大块:
- 头部:快速导航栏
- 左侧:内容板块
- 右侧:推广导航板块
其实论坛类、博客类的页面大多如此,我们再来看看用 Vuepress 搭建的前端游乐场(跟 Vue 官网很像):
图 10-3 前端游乐场页面
除此之外,还有视频类、电商类等各种角色的网站,大家有空也可以去看看,思考下里面是怎么划分的。或许你会觉得,想这些有什么用呢?这对我们平时的工作有什么帮助吗?其实观察 -> 思考 -> 总结
也是有意思的事情,可以多一种角度来思考自己的工作内容,也能提高写代码的趣味性。如果你要认真地把这个过程放置到你的工作中,也可以找到很多提升工作效率的方法,也会让你的路越走越顺畅。
模块的划分,其实最终在代码中呈现出来的,常常是组件的划分。
1.2 组件与模块
第4章 Vue 组件的使用中,我们详细地介绍了组件。虽然组件和模块是不一样的两个概念,但是模块有些时候也可以作为一个组件来维护,而模块也可以是属于某个组件、或是包含哪些组件的关系。模块更多是在是视觉上呈现的划分,而组件则更偏向功能上的划分。一个模块是否可以成为一个组件,需要看这个模块是否拥有属于自己的状态、数据、事件等对于组件的封装也都已经在 4.4 章节中有详细的描述。
2. 模块抽象成数据
想象一下,在把数据与逻辑分离到极致的时候,我们看一个应用/页面,会看到一具静态的逻辑躯壳,以及动态的数据组成。数据如灵魂般地注入到应用/页面中,可使其获得生命。关于如何进行数据的抽离,通常来说可以把变化的部分分离和抽象,然后通过注入的方式,来实现具体的功能和展示。
是否有点抽象?这样的一个分离过程,也可以理解为我们写好的一个页面,需要从后台获取到数据,然后根据数据渲染出对应的内容。在这里,页面就是静态的,而获取的数据就是动态的。从另外一个角度来说,除了后台请求的数据,我们在 Vue 中通过data
绑定的数据都可以抽离。关于这些可抽离的数据,我们来简单识别和划分一下。
2.1 状态数据
在一个应用的设计里,我们可能会拥有多个组件,每个组件又各自维护着自己的某些状态,同时部分状态相互影响着,叠加起来呈现出应用最终的整体状态。这些状态,都可以通过数据的方式来表示,我们简单称之为状态数据。怎么定义状态数据?最浅显或是最直观的办法就是,这些数据可以直接影响页面的呈现,如对话框的出现、隐藏,标签的激活、失活,长流程中的进行中步骤等,都可以作为状态数据。在 Vue 里面,状态数据会经常与 v-show
、v-if
等逻辑结合使用。
我们的应用,大多数都是呈现树状结构,一层层地往下分解,直到无法分割的某个简单功能。同时,我们的组件也会呈现出来这样树状的方式,状态是跟随着组件维护,某个功能状态属于组件自己,最外层的状态则属于整个应用,当然这个应用可以看做是一个组件。
图 10-4 博客模块划分
如图 10-4,图中的每个模块都可以附着着一个“是否可见”的状态。我们的应用状态整体上也是会呈现树状的方式,与我们的组件相对应,就像 DOM 节点树、CSS 规则树和渲染树的关系。
2.2 动态数据
我们还有很多的数据,如内容、个人信息等,都是需要我们从数据库拉取回来的。这种需要动态获取然后用于展示或是影响展示的一些数据,我们可以称作动态数据。动态数据不同于状态数据,并不会跟随着应用的生命周期而改变,也不会随着应用的关闭而消失。它们独立存在于外界,通过注入的方式进入应用,并影响具体的展示和功能逻辑。
和状态数据不一样,动态数据并不一定呈现为树状的形式。它可以是并行的,可以是联动关系,随着注入的地方不一样,最终在应用中形成的结构也会不一致。我们可以简单理解为每个动态数据都是平等的。
图 10-5 文章列表
如图 10-5,这里每篇文章内容,都是单独的一份从后台请求的数据注入。其实博客通常是静态模板,不存在从后台请求的情况,这里打个比喻,大家可以想象下社区里的文章、知乎帖子、微博等等。
2.3 将数据与应用抽离
要怎么理解将数据与应用抽离呢?形象点形容,就像是我们一个公司,所有的桌子椅子装修和电脑都是静态的,它们相当于一个个的组件,同时每个办公室也可以是一个大点的组件或是模块。那么在我们这个公司里:
- 状态数据:椅子的位置、消耗的电量、办公室的照明和空调状态等
- 动态数据:员工等各种人员流动
当然,公司里没有人员流动的时候,似乎就是个空壳。每天上班的时候,一个个的程序员来到公司里,给公司注入灵魂,公司得以运作。要说将数据和应用抽离,作用到这个例子中大概是这个样子的:
# 将公司和人分开(下班后) -------------------------------------------------------- 公司 --------------------------- --------------------------- | | 人 人 | | 人 人 | 办公楼 | 人 | | 人 人 人 人 | | 人 人 人 --------------------------- --------------------------- # 在公司正常运作的时候 -------------------------------------------------------- 公司 -------------------------------------------------------- | 人 人 人 人 人 人 人 | | 人 人 人 人 人 | | 人 人 办公楼 人 人 人 | | 人 人 人 人 人 人 人 | | 人 人 人 人 人 人 人 | --------------------------------------------------------
当然,人不只是站在办公楼里面这么简单,更多的,人会与各种物件进行交互和反馈,人与人之间也会相互交流和影响。但是这样简单的管理,很容易造成公司的混乱,所以我们会把人员有规律有组织地分别隔离到每个办公室、隔间里面:
# 按照组织进行分隔 -------------------------------------------------------- 公司 -------------------------------------------------------- | 人 | 人 人 | | 人 人 | 人 人 | | 人 | 人 | | 人 人 | 人 人 | |-------- 人 人 | 办公楼 | 人 人 --------- | | 人 | 人 | | 人 人 | 人 人 | | 人 | 人 人 | | 人 人 | 人 人 | --------------------------------------------------------
这就是我们要做的,不只是如何划分数据、将数据与应用抽离,我们还需要将其有规律地管理。所以,这大概是接下来的要讲的内容。我们知道哪些数据需要抽离、如何将数据抽离出来,同时,我们还需要知道,这些数据在抽离出来之后,该怎么去进行管理。
2.4 适度的管理
与组件的封装不适宜过度一样,数据的抽象、隔离、管理,也是需要适度的。当我们的应用很小,只有简单的功能的时候,我们甚至不需要对这些状态、数据什么的进行特殊的管理,甚至几个简单的变量就可以搞定了。随着应用组件数量变多,我们开始有了组件的作用域,当组件需要通信,我们可以通过简单的事件机制、或是共享对象的方式来进行交互。
当我们的项目越做越大,要在上百的状态、上万的数据里要按照想要的方式去展示我们的应用,这时候一个状态管理工具则可以轻松解决乱糟糟的数据流问题。关于在 Vue 中怎样进行数据和状态管理的更多内容,会在 11 章讲述。
3. 深入理解配置化
配置化的思想,如今也不仅仅存在于前端或者是某个领域。所有的系统和架构设计,都可以用领域抽象、数据抽离、配置化等方式,搭建灵活配置、模块解耦的系统,前端也不例外。
3.1 可配置的数据
数据的配置,或许大家会比较熟悉,我们很多的管理端都是用来进行数据配置的。而数据配置的最终效果,则包括影响展示端的页面内容、应用的状态控制等。例如文案、活动、功能展示,都可以通过数据配置进行控制。
应用中的可配置数据
最常见的数据配置,大概是前面说过的一些内容配置,文案、说明等,为此还产生了运营这样的职位。常见的运作方式,是搭起一整套的运营管理平台,除了一些简单的文字或是数据以外,广告内容、推荐位等,都可以通过平台进行配置。
代码中的可配置数据
有些时候,我们也会在代码里面抽象出一些可配置的数据。例如,这个需求产品要求查询一周的数据,我们在开发的时候并不会将 7 天写死在涉及计算的每行代码中,而是将天数配置为 7 天,设置成全局变量:
const QUERY_DAY_NUM = 7;
这样,当需要在紧急情况支持其他天数(五一、国庆、过年等假期)的时候,我们就可以只需要改动这里就可以了。更方便的情况是,这个数据的配置可以放在管理端,通过管理端下发到后台,前端展示的时候只需要从后台获取具体的天数就可以了。
文件里的可配置数据
虽然我们可配置的数据单独抽出来维护,但常常是将这个配置也直接写到代码里。那么如果我们需要调整这些配置,调整后还需要重新打包部署,这种情况开销大、效率低。所以在一些时候,我们会把这样的可配置数据,单独写到某个文件里维护,这个文件不合我们的代码打包到一起。当需要调整的时候,只需要单独下发一个配置文件就好了。
3.2 可配置的接口
关于接口的配置化,目前来说见过的不是特别多。毕竟现实场景中,我们的很多数据和接口并不是简单的增删查改这样的功能,很多时候还需要在接口返回前后,做一系列的逻辑处理。简单地说,很多的业务接口场景复用性不高,前后端除去协议、基础规范的定义之后,很少再能进行更深层次的抽象,导致接口配置化的改造成本较大。
配置化的实现有两点很重要的东西:规范和解决方案。如果说目前较好的从前端到后台的规范,可能是 GraphQL 和 Restful 了,大家不熟悉的也可以去看看。当然,或许有些团队已经实现了,也希望能看到一些相关的解决方案。
3.3 可配置的页面
页面的配置化,可能也已经不少见了。像我刚出道的时候,也写过一个拖拽的 Demo(如图 10-6),当时自己实现完,信心倍增。大概每个前端的成长过程中,都会伴随着一个管理端配置化的需求吧。
图 10-6 拖拽生成 H5 的 Demo
有些时候,一些页面比较简单,里面的板块、功能比较相似,可能文案不一致、模块位置调整了、颜色改变等等。虽然说复制粘贴再改一改,很多时候也能满足要求,但是我们通过抽象和配置化,就可以把重复性的工作交给机器,省下来的精力可以做更多富有创造性的工作。这种页面的配置,基本上有两种实现方式:
(1) 配置后生成静态页面的代码,直接加载生成的页面代码。
(2) 写通用的配置化逻辑,在加载页面的时候拉取配置数据,动态生成页面。
基于 SEO 和实现复杂度各种情况,第一种方式大概是目前比较常用的,第二种的实现难度会稍微大一些。第一种方式,很多适用于一些移动端的模版页面开发,例如简单的活动页面、商城页面等等。第二种的话,更多的是一些管理平台的实现,毕竟大多数都是增删查改,形式无非列表、表单和菜单等。配置化的核心大概是场景分析和功能拆解,所以抛开使用场景来做一个所谓“通用”的配置化是不现实的。但是如果把问题范围局限在解决特定的场景,就可以做出合适的配置化功能。
4. 组件配置化
这里我们来讲一下简单的配置化组件的实现,关于组件的封装前面我们也讲过了。下面的组件配置化实现说明,我们拿这样一个卡片组件来作为例子:
图 10-7 卡片组件样式
4.1 可配置的数据
首先是数据的配置,这大概是最基础的。当我们在封装组件的时候,很多数据都是通过作用域内的变量来动态绑定的,例如 Vue 里面则是通过data
、props
、computed
等实例属性来维护 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
来绑定和生成每一个卡片内容。
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 来进行隔离。
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
来控制底部按钮的数量和文字。这只是最简单的实例,我们可以通过配置,来控制出完全不一样的展示效果。搭配样式的配置,更是能让组件出神入化。当然,很多时候我们组件的封装是需要与业务设计相关,这样维护性能也会稍微好一些,这些前面也都有说到过。
4.4 可配置的功能
功能的配置,其实很多也与状态和展示的配置相关。但是我们有些与业务相关的功能,则可以结合展示、功能来定义这样的配置。
举个例子,我们的这个卡片可以是视频、图片、文字三者其中之一的卡片:
- 视频:点击播放
- 图片:点击新窗口查看
- 文字:点击无效果
这种时候,我们可以两种方式:
(1) 每个功能模块自己控制,同时通过配置控制哪个功能模块的展示。
(2) 模块展示会有些耦合,但在点击事件里,根据配置来进行不同的事件处理,获取不同的效果。
对应维护性和可读性来说,第一种方式会获得更好的效果。如果问什么情况下会用到第二种,大概是同样的呈现效果,在不同场景下的逻辑功能不一样时,使用会比较方便。
功能配置化这块就不过多描述了,毕竟这块需要与业务场景密切结合,第13章表单配置化实现也有介绍。大家更多地可以思考下,自己的项目中,是否可以有调整的空间,来使得整体的项目更好维护呢?