Vue SSR在好大夫的落地

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"​介绍","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于使用过vue的前端开发者来说,对vue-ssr应该有着或多或少的了解。那让我们简单了解一下什么是vue-ssr?以及它的优劣势?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"什么是vue-ssr?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ssr是Server-Side Rendering的缩写,即由服务端负责页面的渲染和直接输出。在服务端将vue组件解析渲染成html字符串并发送到浏览器端,在浏览器端进行渲染激活并进行相关dom操作。由于应用程序的大部分代码都可以在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"服务器","attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"客户端","attrs":{}},{"type":"text","text":"上运行,故也可以被认为是\"同构\"或\"通用\"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"优势","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 更好的SEO,服务端返回的页面是带有页面信息的html文档,这对于搜索引擎的爬虫是非常友好的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 更快的内容到达时间,特别是对于缓慢的网络情况或运行缓慢的设备,无需等待所有的 JavaScript 都完成下载并执行后才能看到页面完整的内容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 跨端同构,对于我们编写的Vuejs的应用程序大部分代码都是可以在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"服务器","attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"客户端","attrs":{}},{"type":"text","text":"上运行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"劣势","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 开发条件所限,在开发ssr项目时,你需要知道你的代码哪些是在服务端运行,哪些是在客户端运行,因为window、document等这些就不能出现在初始化代码和服务的一些钩子函数中,global、process等就不能出现客户端的钩子函数中。这就需要我们写出更加通用的代码来兼容两端;并且一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 更多的服务器端负载,在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对于上面vue ssr的介绍,那么它主要解决了我们什么样的问题呢?好大夫在线为什么需要落地Vue-SSR这样一种技术?它给我们带来了什么样的好处呢?以及它又会带来什么问题和挑战呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"带着这些问题,我们来看一下目前好大夫前端技术栈的一个现状。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"现状","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一、express+handlebars","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从好大夫引入node的时候,采用的就是express+handlebars,主要是获取service端数据,处理业务逻辑,解析视图模板,将解析后的html字符串发送到浏览器端。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么有人会问,这不是典型的服务端渲染吗(SSR)?它不是已经很好地解决了SEO和更快的内容到达吗?没错,现有的模式的确是最典型的服务端渲染,并且在SEO和更快的内容到达。那么我们是不是就没有必要使用Vue SSR了呢?其实不然。目前的开发模式主要有以下几个问题:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 频繁的dom操作、复杂业务状态管理等,都会使得开发变得困难许多,dom操作仿佛回到了刀耕火种的时代。那有的人就说了,我在页面引入vue来解决这类问题不是好了,但是对于首次需要加载的数据又如何去获取填充?那又有人说了,我们首次需要的数据我们服务端渲染,对于页面交互和异步的数据,我们通过vue去绑定和获取。那接下来的问题来了,对于首次获取的数据,如何进行响应式的绑定呢?那还有人说我们可以通过vue  $mount来强制使用激活。这种是一种方式,但是这会导致vue丢弃现有的 DOM 并从头开始渲染,造成重复渲染的问题。并且此种方式存在维护多套模板的情况(vue template & handlebars template),也就是无法达到代码同构从而提升开发成本。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"app.$mount('#app', true)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.对有SEO和更快内容到达需求的情况下很难融合现有的前端技术栈(vue技术栈、react技术栈等)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 对于业务模块、抽象组件客户端和服务端无法同构。  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 相对于前端vue或是react,开发效率低。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二、express+vue spa","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于那些没有SEO需求,以及对于内容到达时间要求不高,但是有着复杂的交互和状态的流程性、功能性等页面,我们通常采用的是vue spa的模式进行开发。express相当只是提供了静态页面服务,对于页面的数据通过异步接口获取填充后再渲染。其弊端我们也很清楚,对于SEO和更快内容到达的页面是不友好的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对于目前使用的技术现状,我们想解决在使用vue情况下,也能满足SEO需求和更快的内容到达,并且实现前后端视图层代码的通用性。于是我们选择了Vue SSR。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"vue ssr落地","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一项技术的落地的过程中,肯定少不了技术选型,前期vue ssr方案调研中我们有对比过nuxt.js,发现nuxt在我们现有的node项目中没办法很好的嵌入以及自定义扩展和维护的成本较高,于是我们选择了vue官方的方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现有的node架构流程如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4d/4db218b3dcdabc594fa4f134b1356b14.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于页面的请求,到达node层,经过各种中间件处理后,会传递到路由层,路由层通过匹配的路由规则传递到controller层,controller对于业务数据的获取和业务逻辑的处理,最后拿到获取的数据渲染指定页面的模板发送给客户端。对于现有的node架构,我们如何把vue ssr应用到我们现有的node工程中去呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们提出了两种解决方案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 借鉴nuxt,我们可以单独的把vue server renderer当做一个node服务,对于页面的数据我们可以通过axios进行跨平台调用。此node服务主要服务解析路由规则,调用asyncData,通过axios获取服务端数据,渲染页面数据,响应页面请求。此方案职责清晰,功能单一,容易理解。但是介于好大夫技术场景的原因,此方案并不是我们实际使用的。主要原因如下:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于service数据的获取问题。我们node是通过调用Java、PHP获取业务数据,但是Java和PHP的接口并不是对外暴露的,导致我们如果是在客户端渲染就会报错。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"增加node中间层node api层,首先对于一个业务维护两个node工程,维护成本增加,并且目前好大夫node层不对内提供服务,还有就是涉及请求链路变长,请求验证信息透传等问题。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于现有的node开发方式也不兼容","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于方案一存在的一些问题,我们提出了第二种能很好地结合我们现有的node架构的方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 在不脱离我们现有node架构的前提下,我们做了如下设计:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f3/f387ea572cdfb164eb123d7b4a251a3e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在不影响现有的node架构模式的前提下,我们对于新增的使用vue ssr做服务端页面,添加了统一的处理。在服务端渲染时我们通过@hnpm/h-ssr-action match router规则,调用asyncData方法,获取页面数据,进行服务端数据渲染,响应页面请求。对于降级的情况下我们@hnpm/h-ssr-action不会获取页面数据,而是直接输出无数据的模板页,在客户端去异步获取service数据。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const ssrAction = require('@hnpm/h-ssr-action');\n...\nconst ssrPageRouterArr = ['/index/search',/\\/*.php/,'/nsearch/*',...];\nrouter.get(ssrPageRouterArr, [...middleware], ssrAction(app));\n...","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@hnpm/h-ssr-action  在渲染之前解析cdn资源、插入页面监控代码、统一处理error handle和404 页面、添加自定义请求头标识等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这就很好的解决了我们上面面临的问题,我们node即提供了renderer能力也提供api接口的能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么有人会问了,你是如何实现node自身调用的问题的?对于node在调用asyncData的问题上,我们是进行端的标识的,对于service端和client端来的请求,我们设置了自定义的请求头,如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// server\nreq.headers['x-hdf-ssr'] = 'server';\n\n// client\naxios.create({ ... headers: { 'x-hdf-ssr': 'client' }});","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次我们封装了统一的请求类库@hnpm/h-ssr-fetch。@hnpm/h-ssr-fetch  主要进行端的区分,我们对于服务端渲染的请求直接require action由action直接返回数据(通过match当前访问路由规则找到对应action文件,避免node自己调用自己api和cookie透传等问题),而对于客户端来的请求我们会通过axios发起请求走正常的http请求(并且对该请求做了统一的处理)。这样的话我的baseController层添加统一处理不同端的响应方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"与此同时我们还封装了@hnpm/h-ssr-server 实现客户端和服务器端路由逻辑 ,以及处理一些加载指示器、服务端降级处理、路由守卫全局配置等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此后,我们还将vue ssr集成到内部的开发工具skit中(vue ssr cli (skit ssr)  ),实现了:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"skit ssr -i 初始化vue skit工程","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"skit ssr -b 构建vue ssr工程","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@hnpm/setup-dev-server  开发环境实现热更新","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自动化构建方面我们也将打包skit ssr工程作为构建的一个环节,实现开发测试上线的一个闭环流程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"降级处理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于vue ssr很大的一个缺点是更多的服务器端负载。所以在上线前我们做了一次压力测试,在8核8进程下渲染1000条静态数据的qps量大概是2200-2300左右。如下图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7b/7b1b128fac95b54afaed9fd35eb1c3d2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"承载现有业务的qps量,我们node服务集群完全没有问题的。但是万一出现服务器压力飙升怎么办?(因为服务端渲染是CPU密集型操作,很耗CPU资源)为了保证系统的可用和稳定性,我们设计了必要的降级方案。如下:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Apollo配置渲染方式,在可预见的大流量或是系统大流量告警的情况下,及时通过平台配置使得整个node集群降级到客户端渲染。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cpu阀值降级,当单台node机器中cpu资源占用触及阀值时主动降级。避免单台机器流量过高而导致负载过高。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,对於单个流量也存在降级的情况,也就是对於单个请求出现服务端渲染失败,也会降级到客户端渲染,保证页面的可用性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"遗留的问题","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 目前在页面模板渲染数据时我们都是通过vuex获取填充,这就需要我们每次都会去从vuex mapstate数据,对于数据状态比较单一并且数据只是用来展示的情况下,通过从vuex获取数据显得有点繁琐。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 在qps压测时,我们发现相对于handlebars的vue ssr渲染的效率较低,在单一的静态数据渲染能力上handlebars的渲染能力要比vue ssr效果高出20-30%。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"未来规划","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 参考nuxt把asyncData获取的数据的数据同步到页面组件的data中,方便开发时获取页面的渲染数据。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 添加有效的缓存策略保证node服务响应请求的能力,深入对比handlebars和vue ssr页面渲染机制,提高vue ssr渲染的效率。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"------- END -------","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【作者简介】","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"梁伟:好大夫在线前端架构师,专注于前端系统设计、流程优化等前端基础建设。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a2/a2dd3cc4988032c5982b86cc5ec8355b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好大夫在线创立于2006年,是中国领先的互联网医疗平台之一。已收录国内10496家正规医院的69.2万名医生信息。其中,23万名医生在平台上实名注册,直接向患者提供线上医疗服务。“让行医简单,看病不难” ,始终追求“成为值得信赖的医疗平台”。","attrs":{}}]}],"attrs":{}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章