渐进改造
SparkUI的产生为SPA改造提供了坚实的基础。如果按最理想的方式推进,只要业务开发团队基于SparkUI对现有的Ruby on Rails的单体应用的前端部分、基于Golang微服务方式对其后端部分进行重构改写、践行前后端整合的最佳实践,即可达成前后端分离的目标。而文章开头曾提到,现存的Rails应用体积大、复杂度高,纵使有着业务开发团队的全力支持,我们也很难在一个较短时间内彻底完成SPA改造。更何况市场千变万化,在业务部门服务老客户、获取新客户过程中,产品经理们也会不断地提出新的产品需求给我们的开发团队,技术演进和业务推进两者需要取得一个平衡。我们为达成这一平衡,所提出的方案是:渐进改造。
混合工程结构
我们的业务模块在Ruby on Rails工程中是以Module方式存在的,除了公共的MVC和资源放在统一的Module里,每个业务Module都有自己的MVC和资源(这里的资源特指JavaScript和CSS)。我们以业务Module作为改造的单元。
由于资源等限制,前后端分离改造在前端、后端的推进节奏并不一致。比较多的情况是Module前端改造先行,后端依旧沿用Rails原有的Controller(也有部分适配工作)。在这种情况下,Module经SparkUI改写的SPA前端独立于Rails工程之外进行打包部署所带来的好处并不明显,故将这部分SPA前端代码的源码依旧放在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>
至于路由,既然我们已经在SPA中实现前端路由,那在Rails端的后端(页面)路由就可以委托给前端:
scope 'spa' do get '/', :to => 'spa#index', :as => 'spa' get '*pages', :to => 'spa#index' end
经由以上方案,我们在尽量短的周期改写了更多的业务模块,对运维的影响也非常小。对于这些业务模块,我们预期在其改写后端微服务时将前端代码从Rails里彻底分离出来,完成该模块的前后端分离。
在上述Ruby on Rails项目之外,FreeWheel也启动了若干个新项目。这些项目一步到位,直接按照前后端分离架构设计开发,其前端均为完全基于SparkUI的SPA。
静态资源服务器
我们也基于Nginx开发了一套轻量的静态资源服务器,前端利用Webpack编译打包成Tar包并独立上线。
前端资源,包括JS、CSS、图片、字体等文件,在打包时会在文件名上追加一段SHA1值作为文件指纹,文件指纹相同意味着文件内容没有变化,这就保证了多版本静态资源文件可以共存,由入口页面决定使用哪个版本。基于这样的设计,前端代码在CI(Continuous Integration,持续集成)的基础上,最终进一步实现了CD(Continuous Deployment,持续部署)。
文件名带有指纹的文件,在Nginx上可以设置长效的客户端缓存,对于大文件,我们也采用了预压缩,通过Gzip格式减小文件传输的体积。更进一步,我们为静态资源配置了CDN,并将静态资源服务器指定为CDN的源。这些措施都有效提升了SPA页面在用户浏览器端的加载速度。
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可以追逐更高的代码质量,目前其源代码已超10万行,单元测试覆盖率高达99.81%;
• 业务模块代码可以更有计划地升级SparkUI版本,在此之前无须反复回归测试。
新老JS代码混用
对于Rails工程的部分功能模块,其前端实现有很大一部分是基于jQuery开发的JS。虽然这些代码并不是基于React或SparkUI开发的,但它们也可以直接在SPA中独立使用。我们在统一的粒度下,创建了一层对React友好的适配器Spark-adapter,对原有jQuery JS接口进行了封装和隔离。业务模块开发人员可以自行决定对于这一部分JS代码是基于SparkUI重写还是放在Adapter中以继续沿用。
质量保证
作为商业应用,其软件质量是绝不能妥协的。前后端分离和SPA改造不能成为降低软件质量的理由。我们保证质量的核心是测试:
• SparkUI组件库本身要具有最高标准的单元测试覆盖率;
• 业务模块改写为新前端时,也要基于SparkUI提供的基础设施编写单元测试;
• 对于Rails工程原有的自动化测试脚本,在业务模块改造为基于SparkUI的新前端时,也要同时更新;
• 将测试加入CI (Continuous Integration) Pipeline,一有Merge Request提交就执行测试,测试成功才允许Merge;
• 各组Lead在Merge Request上做代码审查时严格把关。
另外一个有效实践是为新上线新前端的模块提供回滚机制。因为在这一阶段,Rails工程里特定功能模块的新老前端代码可以同时存在,只需在功能入口处设置一个开关,就可以在线上执行SPA遇到严重问题时随时切换回老前端。