上一讲介绍了如何对前端应用进行模块化和组件化设计,是一个对系统设计进行抽象的过程。实际上,我们在业务功能开发过程中,也会在很多地方使用到抽象的能力,比如将页面内容抽象为数据展示,将页面的呈现方式抽象为状态。
通过抽象的方式,我们可以将整个前端应用使用数据的方式来进行表达和描述,还可以配合配置化来减少硬编码、提升功能的灵活性。
今天我就主要介绍如何将应用抽象成数据,以及怎么配合配置化来提升代码的可维护性。
如何将应用抽象成数据
要将应用抽象成数据,需要进行两个步骤:
- 将应用进行模块化和组件化划分;
- 将这些模块和组件用数据的方式进行表达。
上一讲我介绍了对前端应用进行模块化和组件化划分的一些原则,下面我会直接介绍将页面划分成模块和组件的例子。
将页面划分成模块和组件
在进行系统设计和开发代码之前,产品经理首先会进行功能设计。产品经理在设计一个页面的时候,会根据内容和功能的不同,设计出不同的模块,将它们呈现在页面中。
对于前端开发来说,我们会拿到功能需求描述以及 UI 设计图。此时我们需要结合功能说明以及交互稿或者设计图,来进行逆向拆解,即把一个页面按照功能和内容划分出一个个的模块。
以我自己的博客为例:
我们直观地根据视觉来划分模块,可以分为三大块。
- 头部:快速导航栏
- 左侧:内容板块
- 右侧:推广导航板块
上一讲我们说过,组件可理解为带有界面渲染的特殊模块,因此上述这些模块也可以视作为组件。论坛类、博客类的页面结构大多如此,除此之外还有视频类、电商类等各种角色的网站,大都可以划分为内容模块(详情页 / 列表页)、导航模块(TAB / 导航栏 / 工具栏)、功能组件(回到顶部 / 翻页组件)等。
对应用进行模块划分之后,我们可以将划分后的模块抽象成数据的方式来表达。
将模块 / 组件抽象成数据
通常来说,我们可以抽离出应用中存在变化的内容和动态获取的数据,再通过将这些数据与页面内容绑定的方式(比如使用前端框架进行绑定),来控制具体功能的展示。
我们可以通过数据的来源,将其分成内部数据(状态数据)和外部数据(动态数据)两种。
1. 内部数据(状态数据)
在一个应用的设计里,我们可能会有多个组件,每个组件又各自维护着自己的某些状态。其中,部分组件的状态会相互影响,所有状态的结合体便是应用最终的整体状态。这些状态维护在应用内部,可以通过数据的方式来表示,我们简单称之为内部数据(状态数据)。
怎么定义内部数据呢?最浅显、最直观的办法就是,这些数据来自应用本身,同时影响应用的呈现状态。如对话框的出现和隐藏、标签的激活和失效、进度条的状态等,都可以作为状态数据。
比如,用户在网页中的一些操作,点击某个按钮之后,会出现弹窗,这个弹窗的展示内容以及是否展示的状态可以用对象来描述。
const dialog = {
isShow: true,
title: "弹窗标题",
content: "弹窗内容"
};
2. 外部数据(动态数据)
除了应用本身的状态数据,还有很多不属于应用状态的数据,比如文章内容、个人信息等,都是需要从其他地方(服务端、缓存、文件等)获取的。这些需要从外部获取,用于页面展示或是影响展示的一些数据,我们将它称作外部数据(动态数据)。
外部数据不同于内部数据,它并不会跟随着应用的生命周期而改变,也不会随着应用的关闭而消失。这些数据独立存在于外部,通过动态获取和注入的方式进入应用,从而影响应用的展示内容和功能逻辑。
举个例子,上面博客中的内容板块为列表页,这样的列表页可以用一个数组来表示:
const articleList = [
{ id: 333, title: "文章标题", brief: "文章简介", date: "日期" }
];
这些数据需要从本地数据库中读取,或者通过 HTTP 请求从服务端获取,再根据获取结果渲染出每个列表的内容。
我们在设计模块和组件的时候,需要将这些内部数据和外部数据抽离出来,其中内部数据在组件和模块内部进行维护。当应用启动的时候,再通过注入外部数据的方式,使其可以正常运行。
将数据与应用抽离
要怎么理解将数据与应用抽离呢?如果将应用比作是一个公司,公司里所有的桌椅、电脑等设备都是静态的,每个工位可理解为一个组件,同时办公室也可以认为是大一点的组件或是模块。
那么在我们这个公司里:
- 内部数据是椅子的位置、消耗的电量、办公室的照明和空调状态等;
- 外部数据是行政人员、技术人员、设计人员等各种人员流动。
每天上班的时候,一个个的工作人员来到公司里,开始干活,此时公司也开始运作。
# 将公司和人分开(下班后)
--------------------------------------------------------
公司
--------------------------- ---------------------------
| | 人 人
| | 人 人
| 办公楼 | 人
| | 人 人 人 人
| | 人 人 人
--------------------------- ---------------------------
# 在公司正常运作的时候
--------------------------------------------------------
公司
--------------------------------------------------------
| 人 人 人 人 人 人 人 |
| 人 人 人 人 人 |
| 人 人 办公楼 人 人 人 |
| 人 人 人 人 人 人 人 |
| 人 人 人 人 人 人 人 |
--------------------------------------------------------
当然,大家不只是站在工位里这么简单,我们会与各种物件进行交互和反馈(挪动桌椅、开灯开空调等),人与人之间也会相互交流和影响(对需求、方案评审等)。
如果我们每个人的工位都是随机的,在工作过程中会很不方便,所以大家会被有规律有组织地分别隔离到每个办公室、隔间里面。
# 按照组织进行分隔
--------------------------------------------------------
公司
--------------------------------------------------------
| 人 | 人 人 | | 人 人 | 人 人 |
| 人 | 人 | | 人 人 | 人 人 |
|-------- 人 人 | 办公楼 | 人 人 --------- |
| 人 | 人 | | 人 人 | 人 人 |
| 人 | 人 人 | | 人 人 | 人 人 |
--------------------------------------------------------
同样的,我们在设计应用的时候,除了需要考虑如何划分模块、对应用数据进行抽象,还需要将其有规律地管理。当我们将模块和组件的状态和内容抽象成数据之后,就可以将这些数据独立出来进行管理,也可以更好地解决模块 / 组件间耦合、依赖、通信等问题。
我们还能观察到,桌椅和其他办公设备其实也可以通过外部获取,也就是说,内部数据同样可以转换为外部数据,通过动态获取的方式来恢复应用之前的状态。比如某个表单在二次编辑的时候,需要恢复为提交时的状态,包括勾选框是否选中、输入框填写的内容等。
那么,我们将这些数据和应用抽离之后,可以用来做些什么呢?我们可以通过变更数据的方式来更新应用的状态,在第 9 讲中我们也有介绍数据驱动的编程方式。
我们还可以通过设计合适的状态机,来解决像 Web 页面这种天然异步系统中事件被多次触发问题,比如用户连续点击某个按钮,通过状态判断是否需要进行相应的逻辑处理等。
除此之外,当我们对数据进行抽象和分离之后,还可以很方便地实现应用的配置化。
实现应用的配置化
配置化的思想不仅仅存在于前端或者是某个领域,大多数的系统和功能设计,都可以用领域抽象、数据抽离、配置化等方式,减少需求开发和维护的成本。
将应用中的数据抽离之后,我们可以对这些数据进行配置。
可配置的数据
前面我们介绍了影响应用状态的数据,其中包括了与界面有关和与界面无关的数据。
当我们希望对某个产品进行配置化调整的时候,一般会使用运营管理平台来进行运营和管理。通过搭起一整套的运营管理平台,我们可以通过平台进行配置,管理应用的状态和数据,包括:
- 界面展示内容,如活动文案、广告内容、推荐位等;
- 功能逻辑,比如有效时间的计算、活动上线和下线、是否需要用户输入信息等。
如果应用功能逻辑通用性较好、复杂程度较低,我们甚至可以通过配置的方式快速搭建出前端页面,比如活动类配置系统、表单类配置系统等。
对于一些功能简单的页面,页面结构、功能比较相似,区别在于文案不一致、模块位置的调整、颜色的改变等。相比于通过复制粘贴然后调整逻辑,通过抽象和配置化的方式,我们节省重复性的工作,同时还避免了项目过多、重复代码难以维护等问题。
以上这种简单页面的配置,基本上有两种实现方式:
- 根据配置生成静态页面的代码,直接加载生成的页面代码;
- 实现通用的功能逻辑,在加载页面的时候动态加载配置数据,从而生成展示的页面。
配置化的核心大概是场景分析和功能拆解,所以抛开使用场景来做一个所谓 “通用” 的配置化是不现实的。为了做出合适的配置化功能,我们需要把问题范围局限在解决特定的场景,比如用于电商活动的页面、表单提交类的页面等。
以一个组件开发为例,我们来看看怎么实现组件的配置化。
组件配置化
以下图中的卡片组件来作为例子:
为了方便地表达数据绑定,后面的代码会基于 Vue 框架实现。
我们可以实现以下的配置化能力:状态和展示内容可配置、样式可配置、功能逻辑可配置。
1. 状态和展示内容可配置
例如,我们需要一个对话框,其头部、正文文字、底部按钮等功能都可支持配置,我们可以用代码这样表示。
<div class="my-dialog" :class="{'show': isShown}">
<header v-if="cardInfo.hasTitle">{{cardInfo.title}}</header>
<section v-if="cardInfo.hasContent">{{cardInfo.content}}</section>
<footer>
<button v-for="button in cardInfo.buttons">{{button.text}}</button>
</footer>
</div>
通过这样的方式,我们可以:
- 通过
cardInfo.hasTitle
来控制是否展示头部; - 通过
cardInfo.buttons
来控制底部按钮的数量和文字。
通过这种配置方式,我们可以控制组件中具体某些功能的状态和展示内容。
2. 样式可配置
样式的配置,通常是通过class
来实现的,比如可以使用条件语句进行判断,并绑定不同的class
:
<!-- 自己拼接完整 class -->
<button :class="'my-dialog__btn--' + (isA ctived ? 'actived' : 'inactived')">Submit</button>
<!-- 也可以将 class 拆分 -->
<button class="my-dialog__btn" :class="isActived ? 'actived' : 'inactived'">Submit</button>
通过这样的方式,我们可以控制界面的展示样式。如今很多框架会通过在class
里添加随机 MD5 这样的方式,来保持局部作用域的class
样式不受其他地方影响。
除了class
之外,我们当然也可以直接将条件语句和style
进行绑定,来控制具体的某个样式。
3. 功能逻辑可配置
举个例子,我们的这个卡片可以是视频、图片、文字三者其中之一的卡片。
- 视频:点击播放。
- 图片:点击新窗口查看。
- 文字:点击无效果。
这种时候,我们可以使用这两种方式来实现功能逻辑的配置化。
- 通过控制 UI 展示来控制点击事件的处理:每个模块(视频、图片、文字)绑定自己的点击事件,同时通过配置控制哪个模块的展示,从而控制事件的处理逻辑。
- 通过逻辑判断来控制点击事件的处理:绑定的点击事件里,根据配置来进行不同的事件处理,展示不同的效果。
可以看到,一个组件中可以通过配置的方式来控制它的状态、展示内容、样式,甚至是功能逻辑。而我们的应用常常是通过不同的组件和模块组成的,同样可以通过配置的方式来控制应用的各个状态、内容以及功能。
小结
今天我主要介绍了应用中常见的数据抽象,通过对应用进行合适的数据抽象,并将这些数据从应用中抽离从而实现应用的配置化,我们可以减少需要开发的代码量,提升系统的可维护性。
配置化的思想可以用在各个地方,我们也常常将其称为 “一切皆可配置化”。
实际上,除了页面内容、应用状态的配置化,我们甚至可以实现接口的配置化,比如 GraphQL 可用于自定义的接口 API 查询。
除了这些,你觉得还有哪些问题可以通过配置化来解决的呢?欢迎在留言区进行讨论。