做者:李强,腾讯web开发工程师
商业转载请联系腾讯WeTest得到受权,非商业转载请注明出处。
原文连接:http://wetest.qq.com/lab/view/348.htmljavascript
直出这个名词是在node出现后才有的,在node出现前叫作服务端渲染。css
因此能够把直出定义为:“以node做为后端语言实现的服务端渲染并输出HTML字符串到客户端的一项技术”。这样浏览器渲染首屏的过程就由非直出下的先请求HTML,再请求js、css,最后再请求后台数据。改成直出下的直接向node服务器发起请求,而后经过内网获取到首屏数据后,组装成HTML直接返回给浏览器。这里说明下:直出并不必定就比非直出快,可是它能保证用户在不一样机型、不一样网络条件下都有一个比较好的体验。html
那什么是同构呢?前端
同构就是解决直出的一种思想,node出现后使得javascript脚本也能够在服务器端执行,经过维护一套项目代码,实如今先后端均可以执行的目的。java
QQ兴趣部落拥有页面80多个,开发人员14个,参与改造直出人力2个,使用同构的作法无疑能够最大程度上下降改造和维护成本。node
亿万级用户意味着什么呢?目前部落用户注册和使用量达亿万级, 这样大量的用户意味着存在高并发,服务随时都有可能挂掉的风险。前端页面做为整个web服务中最直接面向用户的,一旦服务不可用就将是件让全部人都很崩溃的事情了。react
本文的目的在于解决两个问题:webpack
一、 部落是怎样从一个纯前端项目改形成同构直出项目的web
二、在访问量这么大的状况下,如何保证直出服务的可用性的问题。ajax
首先明确同构直出要作好哪些工做,总结下来有三点,可称之为同构直出三要素。
一、保证DOM的一致性,若是说原本浏览器经过纯客户端代码渲染出来的页面结构是下图这样,服务端渲染出来却少了一个dom节点,那确定会致使页面显示有问题。
二、保证先后端数据的一致性,服务端不能执行dom操做,因此像绑定事件这样的工做,就须要浏览器拉取到js脚本后才能进行,若是使用服务端获取到的数据渲染出来的HTML结构与前端绑定事件时用到的数据不一致,就会致使问题。
三、保证路由的一致性,不能让用户访问a页面的时候,返回b页面给用户。
这样就能够明确作同构直出的方向,对于部落来讲,原来的项目中就使用了react和redux,因此接下来会使用这两个框架进行讲解。
同构直出是一种优化的思想,不受任何框架限制,理解其中的原理才是最重要的。那么问题就来了,如何使用react来保证dom一致性,又如何使用redux保证数据一致性?先来看一下dom一致性的实现。
在使用react作同构直出时,很关键的一个因素就是它提供了虚拟DOM的支持,是一种在内存中的对象数,使其能够支持在浏览器和node环境下执行,这也是代码能够同构的关键所在。在浏览器端经过render方法生成虚拟dom并挂载到真实DOM上。在服务端经过renderToString方法将虚拟dom拼装成HTML字符串。使用这两个方法就能够解决dom一致性的问题了,来看一下具体的实现。
首先服务端经过调用rendertostring方法将react组件渲染为html字符串,可是经过react组件渲染出来的并非标准的html格式,须要将其嵌入HTML模板中才可以被浏览器解析。当浏览器向直出服务器发起请求后,服务端将渲染好的html字符串返回,浏览器收到响应后进行渲染。浏览器经过解析html拉取到js脚本后,会执行render方法,在render方法处理过程当中会校验节点中的checksum属性,该属性是在服务端调用rendertostring方法时追加的,用于前端校验dom一致性,当校验一致时,直接执行脚本中后续的绑定事件等行为,若是不一致,将会进行虚拟DOM的diff操做,而后再进行增量更新DOM、绑定事件。在红框处,能够看到同构代码的部分。
可是,Node环境和浏览器环境毕竟仍是不同的,有这么多前端代码是不能直接在node端执行的,应该怎样在同构代码上作好平台区分呢?
解答这个问题以前,再来看一下数据一致性是如何保证的。
Redux使用单一的Store对象保存、管理页面中的全部状态,和虚拟dom同样,是一种驻在内存中的对象,代码彻底能够同构。
保证数据一致性的原理其实很简单。只要在最后组装HTML字符串时,将服务端的状态经过script标签一块儿输出给前端,而后在前端初始化 Store 时使用该数据,便可完成了数据的传递和共享,达到保证数据一致性的目的。
这里其实也存在一点问题,页面的状态大都来自于后台数据,而发送异步请求的方法在前端是ajax方法,在node端是使用http模块的request方法,这样,咱们又该怎样保证代码的同构呢?
接下来能够了解下怎样解决上面遇到的一些问题,以及部落同构直出的改造方案。
整个解决问题和改造的过程我把它比做是一次装修房子的过程,在装修房子过程当中有这样一些关键的角色,户型结构图、设计师、经过设计师设计出来的效果图、还有房子,若是此时又买了一套户型结构彻底同样的房子须要装修,那就和先后端须要渲染出来的HTML结构同样是相似的场景了。因此能够就户型结构图看作是源码,设计师看作构建工具,效果图看作构建打包后的bundle,已经装修好的房子看作浏览器,等待装修的房子看作node服务器。你们还记得咱们前面提到的第一个问题吗?前端代码中有些代码是不能在node端执行的,该怎么解决呢?
先来看一下若是在设计过程当中,想去掉一些东西该怎么作?
是否是只须要在户型结构图上作些标识,而后告诉设计师红圈中的内容表示想去掉这部分的内容就能够了?
就是按照这种思路,咱们在源码中作了些标记,而后告诉构建工具被这个标记包裹的代码是打包node端代码时须要删掉的,让构建工具识别这个标签的方法可使用自定义webpack loader或者babel插件。
而后回想下第二个问题,发送异步请求前端使用的是ajax方法,node端使用的是http模块的request方法,这个问题怎么解决?一样的,在设计过程当中,若是想改个门,是否是直接告诉设计师就能够了? 都不必在原始图上进行任何修改了。
借助这种思考方式,经过构建工具处理,就不须要对源码进行任何更改。源码中使用的是ajax方法,同时在node服务器上在全局变量下实现了一个window.ajax的方法,这样经过自定义babel插件,在对源码打包时,将ajax方法名替换成为window.ajax方法名,问题就获得了解决。
到了这一阶段——结束了设计工做,有了效果图,也就是已经打包出了一份能够在node端执行的bundle,就下来就是须要到房子里面去还原设计稿的时候了。
施工的话,单凭咱们本身确定不行,因此须要一个施工队。
施工队里面有包工头,负责承接项目,分发任务给各个工种按照设计稿进行施工。
一样的原理,咱们在node服务器上引入了直出框架机的概念,帮咱们统一管理直出服务。框架机的第一层就是玄武和TSW(不理解玄武的同窗,这里能够把它当作是起了一个koa的server,负责监听端口,接受请求并转发到业务逻辑层按照打包好的bundle去处理。)为了让业务逻辑层没必要针对每一个页面作兼容,因此须要打包出来的server bundle具备固定的结构,那咱们就来看一下bundle是怎样的一个结构。
源码的结构大体是这样子的,你们能够看到这里面有一个前端程序的打包入口,实现上是这样的,里面有对store和main组件入口的引用。由于源码中没有对服务端程序的打包入口,因此须要对store和main进行单独打包。
最终构建出来的目录大体是这样的,以a页面为例,有HTML模板、组件入口脚本、建立store对象的脚本,最后还有一个首屏action的脚本。
这个脚本是作什么的呢?
在action的脚本中封装了全部异步请求的方法,对于页面来讲,由不少组件构成,每一个组件调用各自的action方法更新自身状态,可是,首屏并不必定须要渲染全部组件,可能只须要展现组件1和组件2,因此这时就须要提取出首屏所需的action creator方法了,咱们把它封装在了名为firstAction脚本中以便构建工具打包后在服务端进行调用。这样打包后的bundle中每一个页面就都有了相同的结构。
这时就能够在框架机中的业务逻辑层统一对直出页面作处理了。当浏览器发起对页面A的请求时,经过玄武将请求转发到业务逻辑层,首先进行路由解析,确保路由一致性,这里使用正则匹配获取url中的模块名,经过模块名获取页面A的存放路径。
而后为请求建立沙箱环境,让每一个请求都能在独立的上下文环境中执行,实现上使用的是node的vm模块,若是以前没有接触过的话能够把框架机想象成是浏览器,每当有一个请求过来就会新开一个tab页,请求处理完后关闭tab页。
接着就是初始化一些全局对象,好比前面提到的window.ajax方法。而后将页面A的脚本引入,经过store脚本建立store对象,经过firstAction脚本获取首屏所需数据,执行rendertostring方法渲染组件,最后读取A页面的HTML模板,组装成HTML字符串输出给浏览器。这就是框架机基本的一个工做流程了。
最后对直出改造方案进行一下总结。首先是在node服务器上部署了一个直出框架机的服务,使用单独的代码仓库进行维护和发布。
而后经过打包构建工具构建出客户端的bundle和服务端的bundle。因为客户端和服务端的一些差别,须要在源码中使用特定的标签将node端不能执行的代码作个标记,同时还要新增一个供服务端使用的封装了首屏action的脚本,在构建工具中新增server端的打包配置,并加入一些自定义的loader和babel插件帮助咱们构建出server端的bundle。
而后将server bundle发送到node服务器上,当浏览器发起请求后,框架机帮咱们组装首屏html字符串并输出给浏览器。浏览器进行渲染后,引入前端的js脚本,进行后续的dom更新和绑定事件等工做。
以上就是改造直出的整套方案。
首先要讲的是本地开发调试在保证服务可用性方面的问题。
前面提到了框架机,那就先来讲一下框架机的开发调试模式。本地开发是以tnpm命令行工具包的形式。对于本地开发调试模式也是和命令行工具包同样,使用 tnpm link命令,创建命令的全局连接。Tnpm其实就是npm,只不过是企业内部私有npm仓库,外部访问不到。
有人说,平时开发时我连这一步也不想要怎么办?因而咱们增长了自动化测试。
能够利用Mocha + Chai 帮助咱们实现一些代码逻辑上的测试。
接下来就是容灾。在代码报错、服务器崩溃的时候,须要一套容灾方案来让业务尽可能正常运做。
兴趣部落设计了一套柔性可用的容灾方案。当直出报错的时候,会让请求自动转发到静态资源,让相对稳定的静态资源接受用户的请求,以保证业务不受干扰。
具体的原理是怎么样的呢?首先由一群Nginx服务器集群去调度用户的请求,这些请求包括了直出服务器、CDN、后台等等。一旦直出服务器挂掉了,它会自动将请求转发到CDN服务器。
上面这里是Nginx接入集群的示例代码。
业务上线前,须要先预估请求的量级,才能预先准备足够的服务器,以抗住大量用户的请求。所以须要作好压力测试。
兴趣部落在作同构直出的过程当中,使用了腾讯 WeTest 压测大师,实现更智能和自动化地压力测试。上图是压测大师的入口界面,能分别从系统角度、用户角度、业务角度,多角度帮助开发人员发析直出业务的“接客”能力。
瞬时TPS图表,分析了服务最优的承载能力。
经过服务器性能趋势图获得CPU、内存的性能瓶颈。
还支持报告的一个对比,帮助比对分析每次业务更新后的压测状况。
直出顺利完成,服务器也准备稳当了,此时就已具有了产品发布的基本条件。但为了让产品对业务成效更有把握,这里须要先作一个用户灰度。
兴趣部落这里主要是详情页作了同构直出。所以针对业务场景,咱们经过在列表页作一个区分,经过前端来控制灰度。直出的用户走带v2的连接,而非直出用户则不带。
产品发布上线时,还须要对它进行全方位监控,以防出乱子。
以上的这些数据指标,都是须要时刻关注的。
兴趣部落同构直出顺利落地,成果也是至关不错的。页面能达到秒出,慢用户占比也从6.8%,降低到1.25%。
为了帮助开发者发现服务器端的性能瓶颈,腾讯WeTest开放了上文提到的压力测试功能,经过基于真实业务场景和用户行为进行压力测试,实现针对性的性能调优,下降服务器采购和维护成本。
除了兴趣部落之外,压测大师还服务了包括王者荣耀、龙之谷手游、轩辕传奇手游、火影忍者等多款高星级手游,也包括QQ、NOW直播等明星产品。
为了让外部更多产品可以享受到简单易用的压测产品,腾讯WeTest决定将这份服务器测试能力产品化,以产品”压测大师“的形式,正式对外开放,点击连接:http://wetest.qq.com/gaps/ 便可使用。
若是对使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531