重新思考 React 项目架构

by Teobler on 20 / 09 / 2022

views

在开始聊之前,我想定义一下架构的概念。大部分小伙伴都会问一个问题:

前端能有啥架构,不就是文件放到哪个文件夹吗?

对,但也不对,所以我们需要对齐认知:

到底什么是架构?

在开始之前,我希望你将视野从前端这个点扩展开,站在你正在参与开发的整个系统来思考这个问题。

然后我们先来看看一个相对权威的对架构的定义是什么样的:

Architecture is the fundamental organization of a system embodied in its components, their relationships to each other, and to the environment, and the principles guiding its design and evolution.

这是 IEEE1471 对于架构的一个定义,它来自于 IEEE标准协会 下辖的 IEEE计算机协会

从这里的定义来看架构指的是“系统的基本组织”,体现为“组成这个系统的组件以及组件之间的关系”,同时还应该包含该系统“与环境的关系”,最后架构还应该“指导系统的设计和演变”。

好的,那么系统又是啥?

A system is a collection of components organized to accomplish a specific function or set of functions. The term system encompasses individual applications, systems in the traditional sense, subsystems, systems of systems, product lines, product families, whole enterprises, and other aggregations of interest. A system exists to fulfill one or more missions in its environment.

所以系统指的是“为了完成某个或者一系列功能而组合起来的组件的集合”,它可以是“单个应用、传统意义上的系统、子系统、系统的系统、产品线、产品系列”等。

这里的组件可以理解成系统中的组成部分,比如一个系统中的数据库、一个个拆分好的服务、不同的前端应用等。

组件之间的关系指的是组件之间的交互关系是怎样的,比如前端应用通过 http 请求调用后端服务,后端服务调用数据库完成数据更新。

我们好像还漏了个环境

The environment, or context, determines the setting and circumstances of developmental, operational, political, and other influences upon that system.

可以看到这里的环境并不单纯指开发人员平时所说的“测试环境、生产环境”。它还包含了运营环境、政治环境等会对系统造成影响的更为广义的环境。

总结来看,架构像是一种蓝图,它规定了系统中都有什么样的组件,组件与组件之间如何交互,当变化出现时应该如何进行扩展和改变来进行应对,从而实现对系统的演进。更重要的是它还需要关注组织结构、业务特点和部署环境等环境限制,在受限的情况下进行合理规划。

那么前端架构呢?

到这里我们就需要将目光从整个系统再放回到前端这个应用上来。

由于各个项目各个组织的环境不尽相同,没有办法找到一个途径来解决不同环境下的不同问题,所以这篇文章也不会关注系统的环境,会将重点放在架构描述的前半部分。

接下来我会以 React 为例进行展开,首先是 ng 已经做的太完善了,没必要在官方文档的基础上造轮子,照着文档来就完事了。其次是 Vue 我没咋用过,没啥发言权,最后是 React 以“自由”著称,各种写法五花八门,我想总结一个相对靠谱的样子出来。

组件

在开始聊组件前我想再次和大家对齐认知 - React 只是一个 UI 库。

A JavaScript library for building user interfaces.

你说它简陋也好,说它扯淡也行;你夸它自由也罢,扁它是撒手掌柜也可以。但这就是事实,它真的就只管用它的规则来渲染 UI。它不管你的路由,不管你的 API,甚至不太想管跨页面(全局) UI state。

那都是你开发者的事,关我 React 什么事?

于是“理所应当”,React 的生态非常“繁荣”。为了能更好地完成你的项目,你需要补齐一些组件:

Request Client

这是你与后端服务通信的关键,大多数项目在 fetch API 普及后就开始使用 fetch API 了,当然也有很多人偏向于老牌的 axios,选什么不重要,关键你得有。

更多人开始慢慢地在 Client 外面包一层,我把它叫做 Client Wrapper 它可以帮你更好地管理缓存,提升开发体验,比如 React QuerySWR

View Model

我其实没有想到更好的名字来命名这个组件,这个组件百分之九十九的情况下是一个 React Hook,它向外暴露 React 组件所需要的所有 callback 和 data,负责处理和持有 UI 所需的 data 和相关计算,同时提供用于反馈用户操作的 callback。

奇怪的点在于这个命名来自于面向对象,但是由于 React 开始全面拥抱函数式,一个函数被用 Model 来命名着实有点奇怪。

Model

这个组件主要用来处理业务逻辑和后端数据转换,相当于对业务的建模,是一个充血模型。它一般被 View Model 调用,用来转换后端数据、内聚相关业务逻辑等等。

它带来的好处是屏蔽后端业务对前端 UI 的影响,只要 Model 保持接口不变,后端数据变化或者业务发生改变,在前端UI设计不变的情况下可以将变化屏蔽在 Model 内,不用对其它地方进行修改。

它还有可能会处理一些通用的逻辑,比如处理缓存,比如错误处理等等,所以也可能会出现类似于 Error Model 这样的 Model。

与 View Model 这个东西的命名同理,把一个函数叫做 Model 确实有些奇怪。但是数据加行为这样的组合好像叫 Model 也没啥不妥?

组件关系

在拥有这些组件的情况下来看看我们这个系统的架构会长成什么样子。

architecture-of-react

图中箭头代表调用关系,连线代表可能成为这个组件的一些实现。Client Wrapper 画成了虚线是因为可能有的项目觉得它是多余的,并不需要,所以是一个可有可无的组件。

当用户操作页面上的元素时,React Component 充当的 View 组件会触发 View Model 中暴露的 callback,callback 会根据具体情况调用 Client 和(或)Model 处理响应逻辑,最后根据不同的情况更新 View Model 中的 data 实现对页面数据的更新。

需要说明的是图中省略了一些技术组件,比如全局 state 管理,比如可能涉及的事件处理组件等等。他们应该按照职责的不同被归属到不同的组件中,而如果你发现有一些组件没办法进行归属那或许就是你扩展自己架构的时候了。

目录结构

到这里我们已经聊了组件和一半组件之间的关系。为什么是一半呢?因为组件之间不仅仅有调用关系,它还应该包含在文章开头提到的:

文件该放到哪个文件夹里

组件应该如何在项目中被归类的问题我在这篇文章中已经聊过一次了,但是既然是“重新思考”架构,理所应当我想对里面的东西做一些改进。

whole-folder-of-project

目录划分的思想没有发生改变,还是分类分层,但是有了不一样的架构之后会有一些改变。

首先是一些“无关紧要”的目录,比如 assets mocks request context 它们的作用和文件夹一样已经很表意了,不做过多解释。

components

与上一篇文章不一样的是,我将 ui-components 和 components 合并到了一起。

在实践的过程中发现,一些时候 ui-components 和 components 没有明显的界限,比如里面稍微有一些逻辑但又不多的时候放在哪里就会很尴尬;再比如团队里大家对于不同组件的认知不一样,分类的时候就会不一样;

最后其实这样拆分开以后并没有多太多的好处,如果仅仅是为了类似于“拆一个公共的 UI 库”会很方便这样的理由维护两个 components 文件夹会增加许多不必要的心理负担,要考虑的问题会城北增长,属实没有必要。

其次合并后的文件夹也可以减少维护成本,降低管理负担。

model

由于 Model 这个组件在架构中的出现,项目中应该不会再出现各种各样的 utils 函数了,他们应该被以各种模型的方式内聚到一个个 Model 中。如果某个 Model 变得很复杂,说明你的模型出了些问题,可以考虑对其进行拆分下放到一个文件夹中来管理。

pages

这个文件夹没有太多改变,但是由于 Model 和 View Model 的引入会有些许不同:

pages-folder

最后

我们从架构的定义开始聊起一直到聊完前端架构中组件与组件之间的关系。有意思的是最后我们的架构变成了一个几乎等于 MVVM 的架构,于是我第一次在我脑子里体会到了架构的演进。

在我四年的一个个项目中,项目架构慢慢清晰起来:

Class 时代按部就班拆分文件夹 Functional 时代按部就班拆分文件夹 为了做 TDD 将逻辑与 UI 以 hooks 的方式分离 为了解决杂乱的 utils 文件和隔离业务逻辑加入了 Model

最后我查阅了一些关于 Android 项目 MVVM 的资料,发现里面存在大量模版代码,也回想起项目上 Android 小伙伴吐槽他们所使用的整洁架构和 MVVM 存在大量无用的中间层。

所以个人认为至少在现在我遇到的这些项目中这些模版代码属实没有必要,这些组件已经能够支撑起大多数业务项目。

另一方面我也真真切切体会到了思考和多元化的力量。这些演进出现的契机要么是为了解决难题(比如大家说的前端不好 TDD);要么是我在写了一段时间后端代码同时学习 DDD 的东西加入进来的(建模);要么是 pair 或看书看博客的时候一些间接直接的输入(和组里 TL pair 讨论,看到邱大师的博客)。

可能它现在不是最后的终点,但它是我现在能看到的前方。

References

  1. GUI Architectures
  2. How To Structure React Projects From Beginner To Advanced
  3. Tao of React - Software Design, Architecture & Best Practices
  4. 可维护的 React