理解前端框架的本质
前端在很大程度上能抽象成这两类功能。你的业务需求无非就是围绕着这两个点在打转。
- 展示数据
- 处理表单
而 Angular、React 和 Vue 这些视图框架的出现,则是可以让你用一种更加有效的方式来组织代码。接着, 自然而然的,。组件库可以让你更加高效地实现业务需求。理解了这些就会发现,做业务需求无非就是在做填空题。
让我们回顾历史,可以发现,前端(泛指各种客户端和浏览器端)框架的发展方向大致是这样。
- 无框架:20世纪60年代,技术员们直接使用系统提供的API绘图,并逐渐形成一套开发GUI雏型系统。
- MVC:Trygve Reenskaug 于 Xerox PARC 工作时,发现了这种模式。他把应用程序分成了三个部分:模型(Model)、视图(View)和控制器(Controller)。模型负责管理应用程序的数据和业务逻辑,视图负责展示数据给用户,控制器负责处理用户输入并更新模型和视图。主要贡献是提供了关注点分离,这极大的影响了后续软件开发的模式。MVC弊端在于,容易形成巨大且难以维护的 Controller 逻辑。
- MVP:在 MVC 的基础上,IBM 的 Mike Potel 在1996年的一篇论文中提出 MVP 概念,其核心是,把视图和模型完全分离,引入了 Presenter 层,你可以直接理解为 Presenter 对象持有了视图对象并主动更新视图,这解耦了视图层和模型层。
- MVVM:2005年,微软的 John Gossman 发布 WPF(Windows Presentation Foundation)时,提出了 MVVM 架构模式。MVVM 是对 MVP 的改进,主要区别在于引入了 ViewModel 层来处理业务逻辑和用户交互。ViewModel层负责处理视图层的事件和更新模型层的数据,把视图层和模型层完全分离的同时,也不耦合具体的视图和模型。
- Reactive MVVM:2010年,微软的 Reactive Extensions(Rx)库被引入,为 MVVM 架构引入了响应式编程和声明式视图的概念。这让 ViewModel 从框架中脱离出来,使得开发者能脱离具体的视图框架而独立开发,同时也使得 ViewModel 更加灵活和可测试,也能把这种思想引入到其他语言中,比如 RxJS, RxJava等。
- Flux MVVM :2014年,Facebook 发布了 Flux 架构模式,它是一种基于单向数据流的架构模式。Flux 架构模式强调将应用程序的状态存储在一个单一的数据源中,并且只能通过特定的方式来更新状态。
从上面的发展路径可以看到,大家会倾向于寻找一种能够分离数据和视图的架构设计模式。而大家发现,从 MVC、MVP 再到 MVVM,与其主动操纵GUI框架的接口去同步数据层,倒不如把数据层的状态变成响应式的,让视图层通过一个中间层来获取对应的状态变更。让大家从繁琐的 GUI 操作中解放出来,专注于业务逻辑的实现。
从上面的历史发展来看,现代前端框架的本质就是一种响应式的视图框架。而基于这些框架编写的代码,你需要关注以下这么几个点,来保证你的代码质量。
关注数据流
如果你有装修房子的水电的经验(或者没有经验也没问题),只要你思考过怎么装修,那么装修步骤就会是这样。
- 在蓝图上设计好管道走向。
- 然后施工时,按照蓝图上的路线来进行管道安装工作。
- 最后,通电、通水,检查管道是否畅通。
这其实跟软件开发很类似。因为在响应式编程中,也是有管道(pipeline) 这个概念的。你可以把数据看作是水,而管道就是用来控制水的流向。所以,在开发时,我们需要关心的是管道应该如何架设,这也是响应式编程的核心思路。
举个例子,假设我们需要实现一个简单的“输入搜索并显示结果”的功能。
关注“操作”的写法(命令式): 你需要在输入事件中手动调度所有相关的数据更新。
typescript
关注“数据流”的写法(声明式/响应式): 你只需要定义数据之间的依赖关系(管道),数据会自动流转。
typescript
业务逻辑应当是命令式代码
业务逻辑应该是命令式的,而在的如今的前端开发中,很多时候我们使用了声明式的代码来编写逻辑,导致产生一种难以名状的奇怪状态写法。例如,在React中,我们需要实现一个二次确认的提交表单的功能,一般来说,会写成这样。
tsx
真的,很不直观。声明式代码不应该把单一逻辑处理处理成多个状态的切换,上面的做法太丑陋了。尽管我们通过 ViewModel 把视图层的命令式代码替代掉,但并不意味着你处处都需要用命令式代码。再说一遍,业务逻辑需要是命令式的代码。
tsx
至于里面的 useDialog,我使用 Promise.withResolvers() 写了个参考写法。
tsx
可以看到,上面的代码更加直观,阅读也容易理解,出错也容易排查。
解耦模块间的依赖
在响应式编程中,数据流是单向的;而我们在开发项目过程中,模块中的引用也应该是单向的。 解耦模块间的双向依赖其实可以使用以下两种方式实现:
- 事件订阅模式
- 依赖注入模式
下面仅以事件订阅模式为例,展示如何解耦模块间的依赖。
例子
有一种很流行的设计,喜欢把网络请求模块和UI模块结合在一起。
tsx
当用户点击提交按钮时,数据被发送,并且会提示用户提交成功或者失败;当用户token失效时,会自动跳转到登录页。这种写法把网络请求和UI逻辑耦合了起来。在依赖层面看来,已经开始有点 bad-smell 了。
如何解决呢?实际上,网络请求不应该知道具体UI的逻辑。网络请求模块只需要提供一个事件系统,处理请求中不同的状态。UI模块只需要订阅这些事件,就可以知道本次请求的状态。
tsx
http请求模块引入上面的事件中心。
typescript
最后,UI模块订阅来自网络模块的事件。
typescript
这样,UI模块就只需要订阅来自网络模块的事件。而网络模块也依赖UI模块,调试起来也会方便很多。这个思路可扩展各个不同模块之间的通信,这就解耦了不同模块间的依赖。
总结
前端开发的核心在于理解框架本质与代码组织方式。现代前端框架本质上是响应式的视图框架。为了编写高质量代码,建议遵循以下原则:
- 关注数据流:利用响应式编程思想,通过定义数据间的依赖关系(管道)来自动流转数据,而非手动调度更新。这能让代码更具声明式特征,减少副作用。
- 保持业务逻辑命令式:虽然视图更新是响应式的,但对于线性的业务流程(如“点击按钮 -> 二次确认 -> 提交数据”),应保持命令式的逻辑编写方式。避免为了迎合框架而过度拆分状态(如设置多个
open状态位),导致逻辑分散和难以追踪。 - 解耦模块依赖:使用订阅发布模式(Event Center)分离业务逻辑(如网络请求)与视图层。UI 只需响应状态变化,无需关心具体请求逻辑,从而降低耦合度,提升可维护性。
通过平衡声明式的数据流与命令式的业务逻辑,可以构建出更清晰、易维护且逻辑自洽的前端应用。