阅读 3359

复杂前端系统开发的难点和重点

从广告投放管理系统,到房屋设计审批系统,ERP 系统,税务报表管理系统,众筹信息管理平台,研发流程管理系统,做前端这些年我开发了很多这样的系统,现在大家好像喜欢叫他们中后台系统,或者 B 端系统,我觉得他们是比较典型的复杂前端系统,这篇文章希望通过我的经验总结一下开发这些系统时的难点和解决难点时需要关注的重点。

本文主要关注功能的场景和模型,而不是技术选型,技术方案对解决问题有帮助,但是不会改变问题的实质,比如用 Redux 相比用 jQuery,需要发的请求一个也不会少,只是在数据向视图的转化和事件的绑定等具体操作上有了简化。

那么什么样的系统是复杂的前端系统?开发中又有哪些难点呢?

从交互上直观地看,我们可以看到有以下典型元素的系统经常是更复杂的。表单,弹窗,标签页,分页,加载更多,表格等。那么这些元素为什么会使系统变得复杂呢?我们逐一看一下:

表单

众筹管理平台的创建筹款表单
众筹管理平台的创建筹款表单

从传统网站开始,表单就是系统的难点,它的复杂在于用户要通过它输入数据,这还涉及到我们大家都熟悉的表单验证。在用了 Redux 等工具之后,问题得到了一定程度的简化,问题的模型也变得更清晰,表单的每个项目都是一个状态值,与用户的输入绑定,而各个表单项目之间还可能有关联逻辑,甚至是关联的数据请求,比如用户选择了这个项目的值之后,需要请求相关子项目的数据。还有我们常见的 input suggestion,根据用户输入实时获取数据给出可选项。表单校验又会给每个表单项增加错误相关状态,包括是否有错误和错误信息等。在表单基础上更复杂的交互有表单配合弹窗,表单嵌套表单等,主要模型是针对某个表单项需要选择或者新增相关信息,比如表单中有地址项,而地址项可以新增,这时候就涉及到模块之间的通信,在信息新增完成后需要根据新增信息更新表单数据。

标签页

非常常见的元素,主要是监听点击事件做模块之间的切换,复杂在于每个模块都可能有自己的初始化等逻辑,而模块之间又可能有关系,或者在某些情况下需要在切换时重置模块状态。

分页,加载更多

也是比较常见的元素,做的是模块内部数据的更新和增加,接收用户的点击事件,请求相应数据。

表格

广告投放系统中的表格
广告投放系统中的表格

展示型的表格复杂度不高,一般只是有初始化的请求,数据加载后就直接渲染了。复杂的表格有两个方向:

  • 内部操作型,常见的比如针对每一条数据的删除按钮,甚至点击删除按钮后需要有确认弹窗或者确认浮层,这就需要有更多的状态来控制了。还有有时候可能会有编辑按钮,通过弹窗或者将表格行切换成输入框来进行更新,这时候就把表单揉进来了。
  • 外部操作型,比如常见的复选框 与外部操作按钮,针对选中行做操作,还有常见的针对表格的筛选。
  • 除了这两个方向外,其实还有单纯前端展示特性上的难点,比如一屏放不下的表格有时候会有滚动页面时表头跟随的需求,不过这种难点并不是本文关注的重点。

小结

通过上面对这些典型元素的分析,我将这些元素增大系统复杂度的原因概括为两个方面,一是增加了模块的数目,二是增大了模块内部的复杂度。而模块内部的复杂度又涉及到用户事件,数据请求和数据状态三个方面,这三个方面又是互相关联的,比如用户事件经常会改变系统数据状态,也经常触发数据请求,数据请求返回的数据一般也会作为数据状态的一部分。

模块

这个是一个功能相对独立的功能块。在技术上通常表现为 Redux 一个子 Store 对应的部分,它一般有自己的一些状态(state),可能需要发一些请求去获取一些初始数据。一般模块的生命周期是发请求做数据的初始化,响应用户事件改变内部状态,也可能会再发一些数据请求,有时可能需要对自身状态进行重置或者重新发出初始化时的数据请求对自身数据进行刷新,在生命周期结束时还可能需要清理自己的状态数据。

模块之间的通信

模块增加之后的系统复杂度增加不是线性的,可能由于模块之间的互动形成几何式增长,而模块之间的通信的方法也是前端关注的一个重点,Flux 模型解决的一个很重要的问题就是模块间通信方式混乱,在 Flux 数据单向流动模型下模块之间的常见通信方式精简为以下两种。

  • 通过 Action。因为 Flux 系统中系统的主要状态全部在 Redux state 中,因而只要通过 Action 改变相应 state,相应模块就会接收到改变,这些改变的影响可以直接反映到视图上,或者相应模块可以监听属性变化,在适当情况下做出 side effect 响应。
  • 父子通信。父模块可以通过传递属性值让子模块拿到最新的数据状态,通过传递属性函数让子模块可以随时通知父模块自己的状态变化。

模块之间通信的时机一般在模块初始和结束时,这样的模块关系比如兄弟步骤关系,上一步在结束时会把自己的信息存储好并开启下一步,或者父子关系比如上面我们提到过的模块中填写地址时调用新建地址模块新增地址,新增地址模块结束后把新增地址信息传递回来。

如果模块之间有频繁的通信,那么他们很可能可以归纳在同一个大的模块下,比如上面提到的表格和它外部的操作和筛选按钮。

模块之间公用逻辑的复用

模块多了就避免不了出现重复性的逻辑,比如权限校验,多个模块可能需要同样的对操作者权限进行校验并在无权限时给出提示的逻辑,还有例子比如多个模块都需要根据鼠标的移动位置让组件执行一定的逻辑。目前 HOC 和 React Hooks 是解决复用问题的主要方案,复用虽然不能让模块本身的复杂度降低,但是可以减轻我们开发多个同样功能时的工作量。

用户事件

这包括输入框,按钮等触发的我们常见的事件比如 onChange, onClick 和 onScroll 等等,他们毫无疑问是整个前端系统的核心,因为它们代表用户的操作,而用户的操作是系统运转的入口,增加了入口自然增加了复杂度。

数据请求

十年前 Gmail 开创先河向我们展示了 Ajax 能够在浏览器上做出什么样的应用,开启了前端大发展的十年。那为什么它是特别的?一直到现在我们的 WEB 应用的核心功能是什么?其实还是随时随地的数据通信能力和与之配合的局部刷新,这也是为什么 WEB 应用有一个名称叫做 RIA —— Rich Internet Application,而数据请求的层次多少和密度大小也是模块复杂度的重要因素。当然现在我们的数据请求不止 Ajax 一种,还有 WebSocket 和 SSE 等,他们会让数据请求的逻辑变得更加复杂。

有些情况我们的模块不能只通过一次请求获得全部所需数据,比如接口琐碎或者数据源复杂,就需要拼数据,比如请求了列表信息还得去请求每个 item 的详细关联信息,所以有 GraphQL 解决接口 under-fetching 和 over-fetching 的问题,服务端提供数据,要解决请求的问题,改造服务端是一个重要的途径。当然如果服务端不容易改造,很多时候前端还是得自己解决问题。

Redux Saga 和 Redux Observable 就是为了解决复杂事件和数据请求逻辑而产生的,对 Redux 而言也就是处理 Action 和副作用(Side effects)。

Redux Saga 可以用它的 Effects 简单地描述竞争性请求(Race),不阻塞请求(Fork),等待全部请求完成(All)等;可以控制包括数据请求在内的动作顺序,比如典型的 Login Logout 流程,只有登录之后才可以做登出操作;还可以在 Action 层内部通过 Put 和 Take 做订阅模式,比如在第一个 Saga 中定义发起数据请求前后一系列动作的执行流程,而在第二个 Saga 中定义数据请求本身的流程,比如到多个地方获取数据做组装,第一个 Saga 会通过 Put 发布数据请求动作,第二个 Saga 会通过 TakeEvery 订阅这个动作做出反应。

Redux Observable 和它背后的 RxJS 的事件控制能力则更强大,比如可以轻松地监听 Triple Click 事件,可以定义多个事件的组合触发关系,比如定义多个事件必须都发生过才可以触发整体事件,而后续任何一个再发生都会触发整体事件。

事件和数据请求,或者说是 Action 和 Side effects,他们的复杂度不仅取决于数量,更取决于他们之间的时序和逻辑关系。

状态

现代框架中的 state 概念,是单向数据流动的核心,React 和 Redux 等工具中都有,相信大家都能理解,系统需要的 state 越多也就代表系统更加复杂。

总结

模型

说了这么多,大家可能会想,这些东西有什么用呢?我认为上面的模型基本上抽象出了我们要解决问题的框架,我们在接到一个新系统的需求或者一个新的功能需求时,可以先构思一下有哪些模块,模块之间是否有通信,需要在什么时机进行通信,多个模块之间是否有公用代码。针对每个模块,需要想一下模块是否需要初始化数据和重置数据,模块内部的用户事件有哪些,获取数据的逻辑和时机如何,模块需要记录的状态有哪些。做了这些思考之后我们对系统的复杂度就有了一个大致认识,对于评估工作量和做排期计划都是有帮助的,更重要的是如果不提前想好这些重要的细节我们可能会在开发的后期才发现他们,他们甚至可能一直被忽略因而引起线上 Bug,模块的重置和数据请求的时序逻辑是常见的容易被忽略的问题。

除此之外,我们可以根据模型给出一些典型的功能场景,然后对比用不同技术框架来实现功能时的优缺点,这样便于我们对比具体的代码,而不是凭空地争论。

最后,希望这些思考能够抛砖引玉,欢迎大家批评指正。

文章分类
前端
文章标签