阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

FreeWheel 前后端分离改造实践

  • 2017-08-18
  • 本文字数:6856 字

    阅读完需:约 22 分钟

在现代前端应用的工程实践中,前后端分离的架构会为两端带来更多的灵活性,已成为主流趋势。与之相对的,传统的单体 Web 应用(Monolithic Web Application)则将前后端代码放在一起,虽然耦合性较强,但在产品研发的特定阶段仍具有较强的优势,早期的 ASP.NET、Java Spring MVC,以及 Ruby On Rails 都是单体应用的代表性框架。

本文将以 FreeWheel 从单体应用改造为前后端分离的实践为例,着重介绍其间前端所遇到的挑战和解决方案。

相较消费者,商业用户对前端应用的需求更具复杂性,且更强调质量。FreeWheel 深耕企业级的视频广告领域 10 年,其基于 Ruby On Rails 框架为广告主打造的 Web 管理应用已经历多轮迭代和演进,目前已达到 20 多个产品模块,1200+ 页面,代码量已达到 143.5 万行代码,其中包含 39 万行基于 jQuery 的传统 JS 代码。为保证其质量,其中包含了 20.2 万行单元测试代码,除此以外,还有独立的近 2 万个自动化测试脚本。在两年前,我们感受到了单体应用的局限性,并决定将其改造成为前后端分离的架构。

技术选型

FreeWheel 前端展现的业务多种多样,但其用户体验强调高效性和高一致性。为辅助业务研发团队进一步提升前端开发的效率和效果,我们在改造前期订立了组件化的目标,力求将统一的用户体验和复杂的内部交互逻辑封装进组件,通过自动化测试保证其质量,并最终在业务模块中广泛复用。

针对以上目标,我们选择 React 作为新前端核心技术,以 ES6 作为开发语言,利用 Webpack 和 Babel 进行编译打包。以 Mocha 全家桶加 Enzyme 作为单元测试框架保证组件质量。每个业务模块均开发、打包并发布为一个独立的 SPA(Single Page Application,单页应用),多个模块 SPA 之间,除了以统一的 SSO 服务保证用户认证外,并无更多的耦合,这一点保证了多个业务模块团队的工作不会互相制约。

在单向数据流框架选择上,我们基于 Facebook 的 Flux 推进了相当长的一段时间。在上线两个业务模块后,我们认识到 FreeWheel 的业务对前端数据流需求的复杂度远高于常见的 TodoMVC 样例,Flux 实现这些需求时会遇到较多困难。我们评估了当时的社区新秀 Redux,它能一定程度缓解我们遇到的问题但仍有局限性。我们最终决定以 Redux 和 ImmutableJS 为基础,开发一套新的单向数据流框架 spark-modula。这点在下一节会有详细描述。

类似的还有前端路由。我们初期的选型是 react-router,随后根据项目需要开发了新的前端路由框架 spark-router。

更详细的前端框架选型如下:

至于后端,我们的选型是以 Golang 开发的微服务。借此契机,团队将原来内置于单体应用中的后端服务重新做了一次梳理,并逐步重构成微服务架构中的若干个微服务。前端在通过 SSO 验证后,以 JSON 格式与微服务交换数据。这些微服务除了满足前端使用,也会通过 gateway 作为 API 暴露给我们的客户,更会为公司内部的其他微服务提供基础。后端架构不是本文重点,故不赘述,有兴趣的读者请参见 FreeWheel 发表的其他文章。

新轮子 SparkUI

为了推进前后端分离改造,我们成立了一个专门的前端小团队,与业务模块开发团队紧密合作,经历数十个迭代,开发并完善了一套基于 React 的前端框架,内部名称为“SparkUI”(这一名称与 Apache Spark 或 Java Spark 无关)。下图是 SparkUI 框架的简要架构:

其中上游的 React、Redux、ImmutableJS 等框架为 SparkUI 的直接依赖,下游的 Business Components 业务组件、Business Modules 业务模块则为基于 SparkUI 框架开发业务代码的产出;衔接上下游的,则是 SparkUI 的核心组成部分。

可重用组件 Library Components

SparkUI 截止至截稿日已积累了 40 个子 package,其中很大一部分为可重用的 UI 组件,我们称之为 Library Components,例如 spark-loading、spark-calendar、spark-raw-grid 等。凡是业务模块提出的对前端组件的需求,只要与业务并不直接相关的,我们都会设计并迭代开发相应的可重用组件。

我们在设计可重用组件时,遵循的一些要点包括:

  • 无状态组件(Stateless Component)优于状态化组件(Stateful Component);
  • 组合组件(Composing Components)优于具有 DSL(Domain Specific Language)属性的单一组件;
  • 高阶组件(HOC,Higher-Order Component)优于混合属性(Mixins)。

应用状态管理框架 spark-modula

上一节提到 Flux 所提供的单向数据流不能完全满足我们的业务需求。我们在对比了 Flux 和 Redux 后,决定自主开发一只新轮子。当时面对的挑战包括但不限于:

  • 在 Ruby On Rails 应用中,开发团队曾设计并开发了大量的 Model,这不仅是因为要遵守 RoR 的 MVC 实践,更是因为业务的复杂程度客观要求有完整的建模,并基于模型推进前端的开发。我们的新轮子需要以类似的方式来消化业务的复杂性;
  • 在业务的前端需求中,常常有一个页面内包含 2 个甚至多个 Grid,这些 Grid 之间会互相影响。比如一个典型场景:“Grid A 在自动加载后,如果只包含一条记录,则自动选中这条记录,并按该记录 ID 读取 Grid B”。这样的交互在 React 社区被称为副作用即 Side Effects。我们的新轮子需要用相对简单的方式支持 Side Effects 的处理。

这只应用状态管理的新轮子我们起名为 Modula,并入 spark-modula 包。经过快速迭代,Modula 框架已正式替代早期的 Flux,应用于业务模块开发。Modula 包括 Model 模型、Constants 常量、Container 容器、Test Utility 测试工具四个组成部分,其中 Model 包含 Props/Hierarchy、Context、Sender/Receiver、Delegates、Bubble Event、Lifecycle Methods、Services、Local Props 等概念/API。以下是一个典型的 Model 例子:

可以看出上半部分相当于 Model 的 schema,Props/Hierarchy/Context 基于 Immutable 数据结构实现了数据模型;而下半部分相当于 Model 的行为,Sender/Receiver + Modula Container 实现了单向数据流。

Modula 框架基于 Redux 但并不限于 Redux,与部分 Redux 生态(如 redux-devtools)兼容,且已完整封装并隐藏了底层的 Redux。

关于 SparkUI 更完整的介绍,请参见后续更详细的文章。

前后端整合

从单体应用改造成前后端分离的架构后,理想状态下,前后端可以分别独立开发、测试、部署,然而若想实现整体业务,则需要将前后两端整合。本节将介绍我们开展改造工作以来,在前后端整合领域积累的部分最佳实践。

RESTful 接口

后端接口均按照社区 RESTful 接口标准定义:

  • 语义化 URL,活用 GET/POST/PUT/DELETE 四种 HTTP 方法;
  • 支持 JSON 与 XML 两种数据呈现格式,默认情况下,HTTP 请求和响应均使用 JSON,加入 XML 参数,请求和响应改为使用 XML;
  • 优先使用 HTTP 状态码(Status Code)表现后端成功状态或各类常见错误,如 HTTP 200(OK)、401(Unauthorized)、422(Unprocessable Entity) 等;
  • 统一业务错误码和错误消息;
  • 以 ISO 8061 标准输入输出日期时间,如:2015-09-08T01:55:28Z。

在前端我们基于浏览器 fetch 接口,封装了 spark-fetch 包,提供如下功能:

  • 浏览器 fetch 的所有功能;
  • JSON 序列化、反序列化;
  • 为 HTTP 错误统一显示对话框,其中 401 状态会跳转至登录页面;
  • 根据用户需要缓存特定资源;
  • 防止 Cross-Site Request Forgery (CSRF)。

我们为前端开发了一套简单的 Discover 服务发现,以 key-value 方式描述前端中会用到的 RESTful 服务,spark-fetch 包在发起 HTTP 请求时只要传入 key 和相关参数即可。目前主要用来防止前端代码里 hard-code 服务 URL,之后会与整个公司级别的服务发现整合起来。

除此之外,我们还在后端开发了一套 API Gateway,提供认证(authentication)、限流(throttling)、跨域等公共功能。上述 RESTful 接口本身无须处理认证等逻辑。在部署后端服务后,只有 API Gateway 开放给外网访问,其他 RESTful 接口均限于机房内网访问,经由 API Gateway 的反向代理提供给外网。即前端在调用这些接口时,必须经过 API Gateway 调用。

认证授权

文章一开始提到的单体 Web 应用其实在 FreeWheel 有多套,分别对应于多个业务线或产品线。这些单体应用开发的阶段有先有后,架构和实现的设计也存在着差别,其中很重要的一点就是认证方式的差别,为了满足多个应用联合登录的需求,尤其是向后兼容 SPA 的联合登录,我们在后端以 Golang 开发了新的 SSO 服务。SPA 在登录页面调用 SSO 接口,登录成功则获取 token 并存入 cookie,这样后续的接口请求就会将 cookie 传入 API Gateway 以获取认证信息。

至于授权(authorization),我们在现有的 Ruby On Rails 应用中大部分是基于 CanCan 框架实现的,改造为前后端分离架构后,我们将与导航、功能入口相关的授权信息从后端完整传回前端,用前端代码判断特定导航或组件是否显示、是否禁用。当然,RESTful 接口中仍有完整的授权判断逻辑。如果有恶意用户通过 hack 的方式修改了前端授权信息访问了本不能访问的界面,他依旧无法获得列表数据、也无法提交数据修改。

后端 Docker 容器化

在业务模块开发过程中,开发人员需要在开发前端代码的同时能访问到后端接口及测试数据。如果是单体应用的开发,开发人员只要配置一套开发环境即可达到这个目标,但在前后端分离后,前端开发人员除了配置前端开发环境,还要配置后端。后端代码有更新时,需要及时检出代码并顺利编译,数据库有更新时也需要执行相应的 SQL 脚本。这些日常工作成为前端开发人员的痛点。

后端 Docker 容器化有效解决了这一痛点。我们目前的 CI (Continuous Integration) Pipeline 会在后端代码检入远程 Git 后触发编译,编译成功后会创建一个包含该编译版本的 Docker image 并上传至公司内部的 Docker image 仓库,类似的还有数据库,以及其他中间件的 image。前端开发人员不再需要搭建后端开发环境,只需在开发机上安装 Docker(如 Docker for Mac),在前端工程内会维护一个 docker-compose.xml,声明了前端工程所需要的后端 Docker image,每次该文件更新后,前端开发人员只需要运行 docker-compose up -d 即可启动一系列 Docker container,在本机运行完整的后端服务,这里甚至包含了适用于开发的部分测试数据。

整合测试

前后端的分离和整合对质量保证提出了新的要求。我们在前端编写 fetch 逻辑时,会以 mock 方式编写对应的单元测试。后端每个接口也有响应的单元测试。而这两端分别的单元测试还不足以保证软件质量,理论上讲,纵使两者单元测试覆盖率均达到 100%,也不能保证覆盖所有用例。作为质量保证的关键环节,在两端的单元测试都通过后,我们的 CI 会执行端到端的自动化测试。这些自动化测试模仿了用户的使用场景,完整的覆盖了前端、后端、数据库乃至其他中间件。

渐进改造

SparkUI 的产生为前后端分离改造提供了坚实的基础。如果按最理想的方式推进,只要业务开发团队基于 SparkUI 对现有的 Ruby On Rails 的单体应用的前端部分、基于 Golang 微服务方式对其后端部分进行重构改写、践行前后端整合的最佳实践,即可达成前后端分离的目标。而文章开头曾提到,现存的 Rails 应用体积大、复杂度高,纵使有着业务开发团队的全力支持,我们也很难在一个较短时间内彻底完成前后端分离的改造。更何况市场千变万化,在业务部门服务老客户、获取新客户过程中,产品经理们也会不断地提出新的产品需求给我们的开发团队,技术演进和业务推进两者需要取得一个平衡。我们为达成这一平衡,所提出的方案是:渐进改造。

混合工程结构

我们的业务模块在 Ruby On Rails 工程中是以 Module 方式存在的,除了公共的 MVC 和资源放在统一的 Module 里,每个业务 Module 都有自己的 MVC 和资源(这里的资源特指 Javascript 和 CSS)。我们以业务 Module 作为改造的单元。

由于资源等限制,前后端分离改造在前端、后端的推进节奏并不一致。比较多的情况是 Module 前端改造先行,后端依旧沿用 Rails 原有的 Controller(也有部分适配工作)。在这种情况下,Module 经 SparkUI 改写的前端(以下统称为“新前端”)独立于 Rails 工程之外进行打包部署所带来的好处并不明显,故将这部分新前端代码的源码依旧放在 Rails 工程 Module 目录下,通过 Webpack 打包的 bundle JS/CSS 也按照 Module 对资源文件的约定(convention)放在 modules/my_module/app/assets/javascripts/my_module/compiled 目录下,并藉由 Rails Asset Pipeline 打包进 Rails 工程发布包进行统一部署。

对于上述 bundle JS/CSS,我们仍使用 Rails 页面模版作为入口,以期减少对 Rails 工程的影响:

复制代码
<%= javascript_include_tag "my_module/compiled/my_module" %>
<%- @js_module_alias = "my_module" %>
<div id="spa"></div>
<script>
(function() {
var React = require('react');
var ReactDOM = require('react-dom');
var AppContainer = require('<%= @js_module_alias %>').AppContainer;
ReactDOM.render(
React.createElement(AppContainer),
document.getElementById('spa')
);
})();
</script>

至于路由,既然我们已经在新前端中实现前端路由,那在 Rails 端的后端(页面)路由就可以委托给前端:

复制代码
scope 'spa' do
get '/', :to => 'spa#index', :as => 'spa'
get '*pages', :to => 'spa#index'
end

经由以上方案,我们在尽量短的周期改写了更多的业务模块,对运维的影响也非常小。对于这些业务模块,我们预期在其改写后端微服务时将前端代码从 Rails 里彻底分离出来,完成该模块的前后端分离。

在上述 Ruby On Rails 项目之外,FreeWheel 也启动了若干个新项目。这些项目一步到位,直接按照前后端分离架构设计开发,其前端完全基于 SparkUI。我们也基于 Nginx 开发了一套轻量的静态资源服务器,前端利用 Webpack 编译打包成 tar 包并独立上线。

SparkUI 独立工程

在小步快跑阶段,我们将 SparkUI 源码直接放在 Rails 公共 Module 中,令我们可以快速验证可重用组件的设计是否满足业务需要。然而这样的结构会带来几方面问题:

  • 版本管理。任何对 Spark 的迭代都会直接影响到业务模块;
  • 开发效率。SparkUI 是纯 JS 库,Rails 工程开发环境给 SparkUI 开发带来一定负担;
  • 源码权限。任何业务模块开发人员均可修改 SparkUI 代码,带来潜在代码冲突;
  • 跨工程复用。任何 Rails 工程之外的工程在利用 SparkUI 时都会比较繁琐。

我们在 SparkUI 推出 1.0 版本时,将其源码从 Rails 工程中摘出,移入一个新的纯前端工程。SparkUI 在这个新工程中,仍由 Babel 和 Webpack 打包,但会作为 library 发布到公司 Nexus 上私有 NPM Repository 里。Rails 工程或其他纯前端工程在其 package.json 和.npmrc 配置中声明对特定版本 SparkUI 的依赖,执行 npm install 后则可以在前端代码中使用 SparkUI。

这一改变大大解放了 SparkUI 和业务模块两方的生产力:

  • 独立的代码库可以隐藏部分 SparkUI 的内部 API 或工具代码,防止业务模块中滥用;
  • 不同的发版节奏令 SparkUI 可以追逐更高的代码质量,目前其源代码共计 9.3 万行,单元测试覆盖率高达 99.43%;
  • 业务模块代码可以更有计划地升级 SparkUI 版本,在此之前无须反复回归测试。

新老 JS 代码混用

对于 Rails 工程的部分功能模块,其前端实现有很大一部分是基于 jQuery 开发的 JS。虽然这些代码并不是基于 React 或 SparkUI 开发的,但它们也可以直接在前后端分离后的前端中独立使用。我们在统一的粒度下,创建了一层对 React 友好的适配器 spark-adapter,对原有 jQuery JS 接口进行了封装和隔离。业务模块开发人员可以自行决定对于这一部分 JS 代码是基于 SparkUI 重写还是放在 Adapter 中以继续沿用。

质量保证

作为商业应用,其软件质量是绝不能妥协的。前后端分离改造也不能成为降低软件质量的理由。我们保证质量的核心是测试:

  • SparkUI 组件库本身要具有最高标准的单元测试覆盖率;
  • 业务模块改写为新前端时,也要基于 SparkUI 提供的基础设施编写单元测试;
  • 对于 Rails 工程原有的自动化测试脚本,在业务模块改造为基于 SparkUI 的新前端时,也要同时更新;
  • 将测试加入 CI (Continuous Integration) Pipeline,一有 Merge Request 提交就执行测试,测试成功才允许 Merge;
  • 各组 lead 在 Merge Request 上做代码审查时严格把关。

另外一个有效实践是为新上线新前端的模块提供回滚机制。因为在这一阶段,Rails 工程里特定功能模块的新老前端代码可以同时存在,只需在功能入口处设置一个开关,就可以在线上执行新前端遇到严重问题时随时切换回老前端。

总结

前后端分离架构是诸多前端应用系统的必经之路,而现实情况往往需要顾及诸多历史架构。本文以单体应用为背景,设计开发可重用组件库为手段,在保证效率与质量的基础上,逐步改造为前后端分离架构。希望对同样面对这一情况的读者有所帮助。

文中提到的 SparkUI 框架,其中与 FreeWheel 业务并不直接相关的纯技术部分,比如 spark-modula、spark-router 等包,我们已计划将其逐步开源。希望届时能与更多的前端技术专家和群体深入探讨、共同进步,并最终对前端社区有所贡献。

前端之巅

「前端之巅」是 InfoQ 旗下关注前端技术的垂直社群。投稿请发邮件到 editors@cn.infoq.com,注明“前端之巅投稿”。

​​​​​​​

2017-08-18 02:479260

评论

发布
暂无评论
发现更多内容

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

Java 程序员 后端

吐血总结——90%程序员面试都用得上的索引优化手册

Java 程序员 后端

因为一次 Kafka 宕机,我明白了 Kafka 高可用原理!

Java 程序员 后端

如何避免企业在碳排放数据上造假?

石云升

学习笔记 碳中和 碳交易

可以回答一下:Redis和mysql数据是怎么保持数据一致的嘛?

Java 程序员 后端

Apache Pulsar 在 BIGO 的性能调优实战(下)

Apache Pulsar

分布式 中间件 BIGO Apache Pulsar 消息系统 Apache BookKeeper

喝了杯咖啡,我突然对MySQL锁、事务、MVCC-有了新的认识!

Java 程序员 后端

发量能决定一个程序员的水平吗

Java 程序员 后端

史上最全Java面试266题:算法+缓存+TCP+JVM

Java 程序员 后端

听我讲完GET、POST原理,面试官给我倒了杯卡布奇诺

Java 程序员 后端

数据服务基础能力之元数据管理

数据分析 数据 元数据 数据管理 业务数据

国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer

Java 程序员 后端

企业数字化转型的起手式是什么?

百度大脑

人工智能 百度

网络安全漏洞复现与分析

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞挖掘

同程内网流传的分布式凤凰缓存系统手册,竟遭GitHub强行开源下载

Java 程序员 后端

工作五年之后,对技术和业务的思考

程序员 技术 职场 互联网人 业务

【高并发】SimpleDateFormat类到底为啥不是线程安全的?(附六种解决方案,建议收藏)

冰河

Java 并发编程 多线程 高并发 异步编程

【Redis源码分析专题】(1)从本质分析你写入Redis中的数据为什么不见了?

洛神灬殇

redis Redis 核心技术与实战 11月日更 缓存驱逐

哪有什么中年危机,不过是把定目标当成了有计划

Java 程序员 后端

哭了,我居然回答不出来女同事的问题:索引为什么能提供查询性能---

Java 程序员 后端

可以回答一下:Redis和mysql数据是怎么保持数据一致的嘛?(1)

Java 程序员 后端

活动预告|ArchSummit全球架构师峰会

第四范式开发者社区

双非本科毕业的我,为何能在金九银十期间斩获京东、字节、快手的offer

Java 程序员 后端

同一个Spring-AOP的坑,我一天踩了两次,深坑啊

Java 程序员 后端

四、StringRedisTemplate 和RedisTemlate有什么不同

Java 程序员 后端

基于 ElasticSearch 实现站内全文搜索(1)

Java 程序员 后端

双非本科进不了大厂?阿里技术四面+交叉面+HR面,成功拿到offer

Java spring 程序员 mybatis

万文讲解知乎实时数仓架构演进

大数据老哥

同一份数据,Redis为什么要存两次

Java 程序员 后端

图像处理网站

Java 程序员 后端

同事问我如何Java实现,搞定分析栈和队列数据结构的实现过程不就好了

Java 程序员 后端

FreeWheel前后端分离改造实践_架构_宋一玮_InfoQ精选文章