SparkUI:一个可供参考的前端开发实践

  • 曹倩芸

2017 年 8 月 28 日

话题:语言 & 开发文化 & 方法前端

SparkUI 是一套完整且灵活的前端开发解决方案。该方案基于 React,由 Modula 应用状态管理框架、一系列可重用的前端组件、以及构建 SPA 所需的各类支持库组成。该方案重视可重用性、灵活性、可测试性以及开发效率,解决了前端社区常见的一些针对商业前端应用开发的痛点,如复杂状态、Side Effect,组件拆分等,更在工程实践、文档化、本身代码质量等方面达到较高标准,为前后端分离架构下的商业前端应用开发提供了坚实的基础。目前 SparkUI 已成功应用在 FreeWheel 的前端项目中。

Spark UI 的诞生和演进

  • 技术选型

FreeWheel 产生升级原有前端框架、开始重新设计 SparkUI 的想法大约始于两年前。2015 年 7 月之前,FreeWheel 其实已有一套基于 jQuery 的前端开发框架,但由于当时 jQuery 的版本较老、技术栈陈旧、整个框架维护不佳,而且缺乏一套可供新同事学习的文档,再加之 React 的兴起,因此我们决定对原有前端框架进行升级。

另一方面,纵观整个大局势下,也有越来越多的互联网公司都在转向 React.js 去开发前端组件,除了性能因素外,很大一部分原因是用 jQuery 去写很复杂的 DOM 操作,后期代码会变得越来越难维护。

当时,JavaScript 框架中的“当红者”除了 React 外还包括 Angular、Vue 等,我们的前端团队主要对 React 和 Angular 两者进行了技术选型的评估。评估的各项指标显示,React.js 相对 Angular.js 的学习曲线更低,而且也较为符合我们当时的基本想法——使用组件化的设计思想,所以,基于 React 开发前端开发框架的行动也随之展开。

FreeWheel 新老前端框架对比

  • 迭代升级

FreeWheel 真正开始启动 SparkUI 是在一年半之前,这一年多也经历了几次比较大的迭代,从最初的 0.X 版本到 Spark 1.0 的版本,大概是以下几个逐步过渡阶段:

0.X 版本主要是基于 Flux 的应用状态管理框架。因为 React 本身只是在 MVC 架构下的视图这一层,只能用来构建视图组件(View Component),本身并没有状态管理的能力,而顶层组件的状态也愈发难以维护,因此当时的 React 生态圈中,React+Flux 这类比较经典的架构在 FreeWheel 之外的其他许多公司也得到更为频繁的应用。

但随着 FreeWheel 前端应用过程中页面交互越来越复杂,组件繁多的同时,各个组件状态繁杂以及状态间存在诸多映射关系,因此 Flux 在状态管理中的表现也越来越满足不了我们的需求。当时开发社区中出现了新的状况管理的理念和技术 Redux,所以在从 0.X 版本提升到 Spark1.0 的过程中,我们在状态管理设计的思想上也从 Flux 切换到了 Redux。

1.0 版本之后已经全面引入了 Redux 作为状态管理的框架,但这并不意味着是完全照搬 Redux 的状态管理机制。实际上,FreeWheel 仅仅是用到 Redux 基本状态的存取接口和一些基本工具,在此基础上,我们主要是运用到 SparkUI 框架的核心模块(其内部称之为 Modula)来管理整个状态,这其中也引入了诸如对象树(Model Tree)这类的概念——因为在 Redux 中的状态并没有层级,而都是平行展开,但 FreeWheel 的应用中,一个对象可能存在非常复杂的层级结构,所以需要引入 Model Tree 对应用数据进行集中管理(Modula 相关设计理念将在后文中具体阐述)。

各阶段中的应用状态管理框架对比

除了状态管理框架的调整之外,1.0 还引入了“函数式编程”的理念。所谓的“函数式编程”理念,是将在框架内部状态的流转完全用一种函数式的编程方式来实现。“函数式编程”最大的优势,就是把一个程序动态运行的过程以一种函数的方式来将其抽象。如果状态扭转过程是一个函数的话,其实能够保证传给函数的原始对象的状态不被改变,只是函数以新的状态输出。当一个应用比较复杂的时候,这样能够保证高效地跟踪和管理应用状态的改变。在 SparkUI 框架里,它也正作为一个基本的编程范式在使用。

同时,社区对“函数式编程”中 Side Effect 的作用一直有不同的声音,但在实际应用中,我们发现 Side Effect 很难避免。比如说两个组件之间在操作上存在关联性——在一个 Grid 里操作完之后势必要影响另外一个 Grid 的展示或行为,这个过程我们也是通过 Side Effect 来支持的。

因此可以说,Spark1.0 基本完成了对 0.X 版本的一次彻底的革新,但同时也导致原有产品中使用 0.X 的页面和应用需要切换到完全不同的 API,这个过程也成为我们的一段重要经验。之后框架的升级改造,我们坚持的基本原则是,所有的改变都是以向后兼容(Backward Compatible)的方式修改,给应用方提供平滑过渡的过程。到目前为止,1.0 版本的整个框架处于相对比较稳定的状态,也已经在我们的生产环境里广泛使用。

SparkUI 架构整体解析

SparkUI,可以理解为我们所谓的分层设计理念,整个 SparkUI 的架构和功能如下:

  • 最底层就是 React 提供的 API,主要提供了基本组件的创建,包括生命周期管理的 API 等等。
  • 接着在此之上封装了 Modula 模块,Modula 模块主要是做应用状态的管理,其本身还是使用了 Redux 来进行实际状态的存取和事件的分发, 并利用 Immutable.js 来保证对象树(Model Tree)的不可变性。
  • Modula 之上提供了一部分状态管理中所用到的、做 SPA(Single Page Application)所依赖的工具。
  • 这层工具之上是 SparkUI 的组件库。因为 FreeWheel 当前主要以商业应用为主,其特点在于界面的变化和演进相对而言会更慢,但却特别强调新、旧界面间的高度一致性。所以我们在应用中抽象出了前端的组件库(比如有称之为 Grid——高度定制化的一种表单的组件等),而所有的应用又会利用这些组件实现它们各自的功能。

SparkUI 框架

  • Modula 模块

SparkUI 框架的设计过程中其实吸收了很多 Redux 状态管理的思想,现在也是使用了 Redux 来进行应用状态的存取和事件的分发,但和 Redux 最大的区别在于,状态管理复杂程度以及应用状态数量不同,其管理思路也具有一定的差异性。 

上文中提到的 SparkUI 框架核心模块——Modula 实际上就是基于 Redux(但并不限于 Redux)的管理,它与部分 Redux 生态(如 Redux-devtools)兼容,且已完整封装并隐藏了底层的 Redux。下图简要介绍了 Modula 与 React、Redux 的关系:

Modula 应用状态管理框架

例如,在 Redux 里,应用状态是完全平展开的结构且不存在任何的层级关系,因为缺乏一个对象化的组织,所以要在状态众多的情况下,在 Redux 的 Store 上找到某个状态就只能依靠纯记忆。而 Modula 引入了对象树(Model Tree)后,所有的状态都可以被对象化,即通过预先定义好的结构来组织状态。尽管是比较复杂的组件,在页面上的展示可能也只是一个表单或 Table。

如果给这个 Table 设定一个较为复杂的状态——加一个搜索条,搜索条本身有简单搜索和复杂搜索的区分,上面还有复杂的工具栏、动作条,其本身或许还需要支持翻页等。如此多的状态之下,用 Redux 的方式可能会有好几百个状态在一个 Store 里,于是管理起来就会非常困难;但 Modula 就可以组织得更好,下面是 Modula 主要的设计理念:

  • Application State=Initial State+Deltas,其中 Delta 是由 Actions 触发的(借鉴 Flux, Elm);
  • Application State 可以由一棵 Model Tree 来描述,这棵树的每个节点都是一个可以描述有效业务实体的 Model(借鉴 Redux,Elm);
  • 由一个给定的 Application State 到另一个 State 的 Transition 可以由 Model Tree 提供的 Reactions 所描述,一次成功的 Action 到 Reaction 的匹配会将 Model Tree 演变为下一个状态(原创);
  • Side Effect 是上述 State Transitions 的结果,它包含了一个更新的 Model 实例,以及零至多个 Callback Functions(借鉴 Elm);

对于 Modula 中 Side Effect 问题的处理,Modula 模块中的 Receiver 可以返回 Side Effect,一个 Side Effect 可以是 Sender 或 Bubble Event 的引用,也可以是一段匿名函数(箭头函数);List-A 读取完成时会根据 List-A 中包含的 ID,自动触发读取 List-B。

所以目前在状况管理上,Modula 相对于 Redux 会是一个比较适应复杂前端状态的应用改进。

  • 前端路由框架 Spark Router:

此外,为了能够支持构建典型的 SPA,我们开发了一个叫 Spark Router 的组件。它主要也是基于 Modula,相比于 React Router(其状态并不存储在 Redux 的 Store 上),Spark Router 里的状态管理能够和应用中其他部分的状态管理采用同样的机制。

此前,应用状态都分散在 React Router 的 State 与 Modula Model(Redux state)里,两者经常遇到同步问题,我们的解决方案是将路由相关的 State 也合并进 Modula 中。因而,Spark Router 主要就是针对 Model 配置路由,Component 可根据 Model 切换相应界面,这样就不必再在 Spark Router 的状态管理和应用中其他部分的状态管理之间添加同步设计,也让程序变得更简单。

SparkUI 当前的应用

SparkUI 目前已经在 FreeWheel 的生产环境中使用了超过一年的时间。我们内部几乎所有的 UI 产品都开始在使用 SparkUI。但现阶段还仍处于从旧有实践过渡到新的基于 SparkUI 实践的过程。 

我们之所以会自己设计和搭建基于 React 的前端 MVC 解决方案,也在于 FreeWheel 的系统是广告资源管理系统,该系统的客户群体大多有非常复杂的工作流,并且是要通过 UI 来实现,因而导致了前端的应用状态多且复杂。所以,SparkUI 框架的特点,或者说其应用场景即:擅长于用来构建有比较复杂的前端状态的应用。

我们在实践过程中也借鉴了很多业界比较好的实践,包括 Redux、Mobx 等,而且能被重用的东西我们都尽量重用,比如 Elemental 这类的基础组件,这样可以在很大程度上降低使用者在 SparkUI 上需要的额外学习成本。此外,我们专门为 SparkUI 做了一个 Documentation Portal,里面有非常丰富的演示能帮助需要使用 SparkUI 的同事来学习。目前,在 FreeWheel 内部,前端团队会定期给其他团队做使用交流和分享,并及时同步最新的一些改进。

SparkUI 还只是供 FreeWheel 公司内部使用,暂时并未开源。但据了解,FreeWheel 和其母公司 Comcast 都对此非常鼓励,目前也已经开始对开源 SparkUI 走相关法务流程。FreeWheel 首席架构师张晗表示:“SparkUI 可能并不会全部开源,组件库这类属于产品特定需求的部分会被拿掉,像 Modula 这样的通用模型部分会属于开源的范畴。如果你的应用需要复杂的前端功能,特别是需要对具有相互关系的状态进行较多维护时,就可以考虑使用我们的 Modula。”

SparkUI 的未来规划 

首先,我们目前需要对框架核心的升级进行性能优化。虽然 React 框架本身的性能也在不断优化,但它其实并不是以性能见长的前端框架,影响性能很重要的一点就是状态变化的计算,根据状态变化的计算来重新渲染这个页面。尤其当状态比较多的时候,此类检查就会比较费时。对此,我们会提出并引入一些标记方式,通过在对象树(Model Tree)上标识一个范围,只要在这个范围里的子状态被更新,服务状态所对应到的视图(View)就需要重新被渲染(Render)。

其次,我们还在不断完善单页 Web 应用的资质。因为 FreeWheel 在做新的前端框架以及前端产品的升级过程中也同时在做前后端的分离。同时,我们的业务系统也做前后端的分离。所以说我们也希望把所有的展示逻辑、非数据逻辑都尽量地移到前端实现,最终整个应用可以变成 SPA。目前,我们已经有对路由、国际化的支持,现阶段还在开发权限模块,目前的权限控制是在服务器端,接下来的目标是增强对权限控制、会话(Session)的管理。

 

语言 & 开发文化 & 方法前端