前言
在对英才网企业线前端不断的完善过程中,我们尝试进行了前后端分离,引入Node环境、以及在使用React的过程中,自行开发DOM渲染框架,解决React兼容低版本IE的问题,在这个过程中,我们有了一些经验和体会,希望本文对您有所帮助。
为什么前后端分离
原有架构下,后端系统基于Java语言实现,通过Velocity模板实现服务器端渲染,前端同学维护静态资源,在维护页面状态时,还需要模板和脚本互传参数,模板中还会有很多UI逻辑,等等,前后端耦合很深,这种模式下开发效率非常低,联调成本非常高,同时Velocity的模板和前端代码如果是分开管理和维护,针对前端静态资源的的构建流程不方便对Velocity模板进行静态资源的引用替换,影响了自动化发布流程,也限制了前端的发展空间。
如何前后端分离
前后端分离包括前后端开发分离,以及前后端物理分离,前后端的开发分离还是基于原有的架构,通过提供相关的平台工具,比如Jello,Mock等,弥合前后端耦合的点,使前后端开发工作可以独立开展,后期通过平台无缝集成。
前后端的物理分离是完全的系统级别的分离,比如Web服务器采用PHP或者Node,或者前端同学去学Java语言,完全接管了Web服务器这一层的开发,不过对前端同学的要求太高了,不是每个人都是全栈工程师,相比PHP和Java,Node对前端同学来说,学习成本低了一些,不过仅仅是语言层面,从浏览器运行时环境切换到服务器运行环境还是需要更多的实践经验,比如我遇到很多前端开发同学想学习Node,但是无从下手,如果不切换到后端思维,还是没法深入Node的。
开发分离相比较物理分离更难一些,因为对平台工具要求比较高,引入了新的工具,也引入了新的维护工作、流程和规范,如果前端同学没有按规范编写模板,虽然在开发环境下完美运行,但是后面发布到运行环境,还是有一定的风险的,毕竟在开发模式下,模板的运行环境是不一致的,对前端新手来说,学习规范也需要不少成本。
物理分离相对更容易一些,前端接管Web服务器,开发和运行环境是一致的,不过对前端同学的要求更高了,还需要了解Web服务器相关的知识,Node对前端来说,入门成本还是比较低的,引入Node以后,针对Node的监控也是一个问题,Node是单进程单线程,如果不能很好的处理异常,进程会挂掉,虽然可以用pm2等进程管理器可以保证进程稳定性,但是异常就是不可预测的错误。
还有关于纯浏览器端渲染,也就是SPA,也是一种分离,纯浏览器端渲染的情况下,前后端耦合进一步降低,只是数据接口的耦合,通过Mock服务约定一直的API,前后端并行开发。
关于前后端分离的更多内容可以看看玉伯的这篇文章[:Web 研发模式演变](https://github.com/lifesinger/lifesinger.github.com/issues/184)
我们前后端分离计划
原有的项目目前还是基于后端的MVC架构,后续随着改版逐步迁移到浏览器端渲染,Node环境准备好以后,提供基于Node的服务器端渲染能力。
上文提到,企业线是不需要做SEO的,可以做纯浏览器端的渲染,不过为了更好的优化加载体验,以及未来在其他业务线的改版做技术储备,可以在企业线尝试去做。
要实现的目标就是,一套组件代码,同时支持前端渲染和后端渲染,这样就不需要为SEO去做些折中的方案了。
整个迭代的过程大致分三个阶段:
- 基于React做前端渲染
- 引入Node环境
- 支持服务器端渲染
基于React重构前端
企业线的前端框架选型经历了一段时间的考虑,让人纠结的点在于,很多新兴的框架对低版本IE支持不好,比如AngularJs、VueJs、ReactJs,但是我们又不能放弃低版本IE,我个人比较倾向React,下面我列出几点:
- 更好的生态支持
- 更好的组件化开发方案
- 有虚拟DOM,更好的性能
- Redux状态管理
- 多端支持,Learn Once Write Anywhere
- 服务器端渲染支持
如果我们选择兼容IE的框架,目前只有Avalon,Backbone等,这样的框架要么过时,要么生态欠佳,未来UI的扩展会受限制。
最终,我们还是选择了React生态,而且,自行实现了针对低版本IE的DOM渲染,达到了兼容低版本IE的目的。
为了实现兼容低版本IE,我们针对React进一步深入了解,比如关于Jsx的编译,虚拟DOM的渲染,diff算法,flux架构等
关于Jsx转换和DOM渲染
React引入了Jsx语法,实现在JavaScript中内联的方式书写HTML,相比之前的字符串模板引擎,开发体验上了一个台阶,我们非常希望能使用Jsx开发我们的组件,Jsx最终是如何被渲染成真是的DOM的呢,在github上,有不少针对jsx的转换器,比如:
l nativejsx:JSX to native DOM API transpilation.
l jsx-transform :This module aims to be a standard and configurable implementation of JSX decoupled from React for use with Mercury or other modules.
l jsx-runtime:Runtime for rendering JSX-IR. Runtime does not renders JSX-IR, otherwise it provides common interfaces for Renderer to be implemented.
上面三个项目都实现了对Jsx的解析,比如nativejsx,将Jsx编译成真实的DOM创建代码,jsx-transform将Jsx独立出来,可以使用在 其他的vdom技术,jsx-runtime提供了更灵活基础库,借助这个库,可以方便的将jsx转换其他的想要的格式,具体如何转换,可以自己控制,比如Jsx转换成DOM创建代码,转换成React需要的的格式,或者转换成字符串等。
具体到React中,转换过程是这样的:
为了兼容低版本IE,我们尝试自己做了DOM的渲染,和React比较,去除了虚拟DOM,过程如下:
渲染过程兼容了React的API,比如:React.createClass,React.createElement,这样以后可以平滑切换到React,并且可以和React进行性能的对比。
关于去除虚拟DOM,有利也有弊。
有利的是首次渲染的性能得到提升,React原有的方式是先构建虚拟DOM树,然后用虚拟DOM树去构建真实的DOM树,这个过程肯定是有性能损耗。
弊端是增量更新无法做diff,React的高性能体现在通过diff算法实现对DOM的增量更新,如果系统增量更新不多,虚拟DOM就不必要了。
其实最佳的方式是首次渲染就是服务器端渲染,增量更新采用React的模式,如果可以控制页面那些组件服务器端渲染,哪些做前端渲染,就更好了。
这里做个数据对比,
库文件大小:
渲染时间对比:
去除虚拟DOM后,虽然带来性能的提升,但是没有虚拟DOM的React,不能称之为React,React更多给我们带来的是对UI架构的重新思考,加入一个抽象中间层,多端能够共用一套状态数据,和状态变迁逻辑,而UI的渲染可以针对多端去实现,这样为未来带来了很好的扩展能力。
Flux框架的轻量级实现
在状态管理方面,我们目前没有引入流行Redux或者alt等Flux框架,官网提供的Flux的实现又很复杂,Redux框架又不是很了解,所以暂时先实现了一个轻量级的Flux框架,不过,实现非常简单,每个组件一个Store,UI组件监听Store的change事件,Action负责Store数据的维护。
后端API的调用放在Action中,Store存在的作用在于方便多个View共享数据,比如一个View可以监听多个Store的change事件。
这个Flux实现和官方的比较,差别很大,官方实现如下:
主要的差别如下:
- 官方的Action仅是一个指令,针对指令的处理是在Store中
- 官方有Dispatchor模块,其中注册了所有的Store,每个Action都会发送到每个Store,Store决定是否处理对应的Action,这样的设计隔离了Action和Store,不需要关心Action和Store对应关系,也就是对Action和Store做了解耦。
- 官方的业务逻辑在Store中,在Store中可以获取当前的state,处理完业务以后,更新state,触发change事件。
相比之下,我们的的设计只能是刚刚够用,缺点是Action和View的关系,Action和Store的关系,都是强耦合的,相比较之后才了解了官方设计的精髓。
还有另一个问题,页面是用多个Store,还是使用使用一个Store,这个经典的Flux实现和Redux实现的差别,当我们想实现服务器端的渲染时发现,单一的Store是最好的,而多个Store,数据准备非常不方便。所以未来,我们会引入Redux。
引入Node环境
Node环境最近才准备好,Node的优点是很简单,缺点就是太简单了,以至于在复杂的业务线上,不敢投入使用,比如遇到异常容易崩溃的单线程,比如多层的回调代码,过于自由JavaScript语言带来的不可预测性,类似Java、.Net这样的语言,有些错误可以在编译期暴露,而JavaScript执行期才知道是否有Bug,过于自由,代码混乱,可读性差,以至于有时候改别人的代码不如自己再实现一套。
为了解决JavaScript本身的问题,出现一些及技术,增强JavaScript语言的健壮性,比如Facebook的Flow,微软的TypeScript,还有CoffeeScript,都可以弥补JavaScript的类型过弱的问题。
随着对ES6的支持越来越好,对异步和面向对象方面方面JavaScript的支持也越来越好,使JavaScript具备构建大型应用的能力。
关于Node的进程监控,初期在运营线搭建了套监控系统,用到如下的系统:
l StatsD:基于Node,用于向graphite的收集器发送数据
l Carbon : 后台服务,监听端口数据(TCP/UDP)
l Whisper :数据库,存储时间序列的数据
l Graphite:基于时间序列数据库的图形展示系统
l Grafana:强大的图形自定义功能,从Graphite抽取数据
基于这个系统的监控截图:
不过系统按最低的标准配置,没有集群,只是在单一业务里尝试用,大面积用恐怕支撑不了,最近,我们将Node的监控接入集团运营部的open-falcon,架构部的WMonitor,Node的监控问题也终于解决。
引入Node对前后端分离非常有利,前端静态资源的构建、发布单独走流程,不需要对其他系统的依赖,还有Node对服务器端渲染的支持,越来越多的公司在做同构直出,借助Node解决SEO的问题。
支持服务器渲染
在支持服务器端渲染方面,React有很好的支持,可以将React组件渲染成HTML,或者借助前文提到的Jsx转换工具,直接可以将Jsx转换成HTML,方法还是很多的,前端渲染和后端渲染的过程大致如下:
需要注意的两个地方:
1.Store数据准备:
前端渲染和服务器端渲染不同的地方是,前端将渲染分阶段进行,比如,一般会分三步:
1) UI框架展现,不加载数据
2) 发起加载数据的异步请求,UI处于Loading状态
3) 数据加载结束,重新渲染UI
后端渲染需要前后端配合:
1) 后端数据准备,渲染出HTML
2) 前端做事件绑定,完成后续操作
2.导航
前端导航通过URL的hash实现导航,页面不刷新,后端导航,需要请求回发到服务器端,重新实现一次渲染。导航组件包括:站点导航、分页导航,Tab导航等。
前面提到Redux的状态管理,整个页面组件用一个Store,非常利于页面的服务器端渲染,只需要将Store数据准备好,整个组件就可以实现渲染,如何渲染,完全取决于Store的数据,比如,对需要服务器端渲染的组件,提供数据,不需要渲染的,不提供,后续在浏览器端判断,做异步的加载,如果能实现这样一套的组件架构,会提升开发效率,较少工作量,会有更好的加载体验。
结束语
本文讨论了前后端分离的一些想法,特别是针对Node实现大前端,自定义Jsx的渲染,基于React+Redux支持前后端渲染,有些想法还没有落地,后续会逐步探索,希望本文对你有所帮助。