vue服务端渲染 同构渲染

Posted shengxiaguangnian

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue服务端渲染 同构渲染相关的知识,希望对你有一定的参考价值。

引言

javascript诞生以来,前端技术发展非常迅速。移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR 、CSR、预渲染等技术。

十年前,几乎所有网站都使用 ASP、Java、php 这类做后端渲染,但后来随着 jQuery、Angular、React、Vue 等 JS 框架的崛起,开始转向了前端渲染。2014年起又兴起了同构渲染,号称是未来,集成了前后端渲染的优点,当真如此?

我们先明确三个概念:

后端渲染:后端渲染指传统的 ASP、Java 或 PHP 的渲染机制;

CSR:前端渲染 指使用 JS 来渲染页面大部分内容,代表是现在流行的 SPA 单页面应用;

同构渲染:指前后端共用 JS,首次渲染时使用 Node.js 来直出 html。一般来说同构渲染是介于前后端中的共有部分

以及常用性能测试时间点

  • 首次内容绘制FCP(frist conntentful paint) 浏览器将第一个dom渲染到屏幕的时间,也就是通常所说的白屏时间
  • 文档加载时间DCL(domContentLoaded)
  • 首次有意义的绘制FMP(frist meaningful paint) ,就是说主要内容出现在页面上的时间,用户希望看到的主要内容出现在屏幕上的时间

在前端渲染领域,主要有以下几种方式可供选择:

   CSR  预渲染    SSR同构
优点
  • 不依赖数据
  • 局部刷新,无需每次都进行完整页面请求
  • FP 时间最快
  • 客户端用户体验好
  • 内存数据共享
  • 懒加载
  • 富交互,可使用JS实现各种炫酷效果
  • 节约服务器成本
  • 不依赖数据
  • FCP 时间比 CSR 快
  • 客户端用户体验好
  • 内存数据共享
  • SEO 友好
  • 首屏性能高,FMP 比 CSR 和预渲染快
  • 兼容性问题较优秀
  • SEO 友好
  • 首屏性能高,FMP 比 CSR 和预渲染快
  • 客户端用户体验好
  • 内存数据共享
  • 客户端与服务端代码公用,开发效率高
缺点
  • SEO 不友好
  • FCP 、FMP 慢
  • 首屏白屏问题
  • SEO 不友好
  • FMP 慢
  • 开发成本高(构建)
  • 客户端数据共享成本高
  • 模板维护成本高
  • Node 容易形成性能瓶颈
  • 实施难度大

 由上表格可得知,各种渲染模式优缺点各异,看起来并没有那么完美的解决方案,我们只能依据自身产品需求,选择最适合我们的渲染方案。

应用场景 

1、一个优秀的产品 ,离不开SEO,它包含了很多方面的知识,一般公司都会配置专门的SEO职位, 毕竟有太多事情要做了。而我们作为一个有追(破 )求(不)完(得)美(已)的前端,在开发的时候,需要做好页面结构科学、标签语义化、站点路由精简、提供必要的爬虫信息(如 Robot.txt,Sitemap,标签 role,ref 等等);然而在现实中CSR大行其道,SPA站点更是数不胜数,用户体验得到了质的提升,然而,然而,为了兼顾SEO,开发者不得不为爬虫提供专门的shadowsite,或者自己顶起下载自己的页面提供给爬虫,或者使用SSR,刀耕火种的年代。。。

2、首屏渲染速度

目前前后端的分离的前端项目需要先加载静态资源,再异步获取数据,最后渲染页面,在这个过程中的前两部页面都是没有数据的,影响了首屏的渲染速度,也就影响了用户的体验。 目前对于首屏渲染速度的提升有许多方案,在ssr之外还有龙骨,墓碑,数据直出。相比于这些方案ssr方案实现是最复杂的,但效果也是最好的。

3、方案选择

vue 中做同构,有两种,一种是基于官方Vue SSR指南推荐的SSR,一种是vueJS通用应用框架 NUXT.

官网方案,可以更直接的控制应用程序,更深入底层,也比较灵活。NUXT,你懂得,就是那种你拿来即用的效果,提供了部分额外功能,e.g. 静态站点,可用于快速实现Vue SSR

SSR和预渲染的使用场景还是有比较明显的区别的,预渲染的使用场景更多的是我们所说的静态页面的形式。而SSR适用于大型的。页面数据处理较多且较为复杂、与服务端有数据交互的功能性网站,一个明显的使用场景就是电商网站

今天为大家介绍的这个,是部分同构方案,开始。。。

使用简介

   技术图片

从官网借来的图可知,SSR有两个入口文件,entry-client和entry-server两个文件,都包含了应用代码,webpack会根据这两个入口文件分别打包成给服务端用的,Server Bundle及客户端使用的Client Bundle ,当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration (判断这个 DOM 和自己即将生成的 DOM 是否相同,如果相同就将客户端的Vue实例挂载到这个 DOM 上, 否则会提示警告)。

   而在vue服务器端渲染时,就不能只是使用web-dev-server和web-hot-middle了,因为我们需要添加服务器渲染的node代码逻辑,这样,我们可以自己开一个node服务器,使用webpack-dev-middle中间件进行打包、使用webpack-hot-middle中间件进行热更新,并添加服务器端渲染逻辑,即node端通过引入vue-serverer-renderer插件来渲染服务器端打包的bundle文件到客户端。

如何用?

所以在项目开发期间,其实我们是运行着两个node服务,一个做代理服务,一个提供ssr和其他的一些服务。

 1、client-server的作用:

  • 客户端打包bundle,然后提供给浏览器进行混合静态标记,在默认情况下,可以在浏览器端输出vue组件,进而生成、操作DOM,然而,服务端渲染时将同一个组件渲染成浏览器端的Html字符串,但是这些个只是字符串,而非应用程序,比如没有CSS等;该html字符串然后发送给浏览器,然后结合客户端打包,最后将静态标记‘混合’为客户端上,生成完全交互的应用程序
  • 数据同步的问题,在挂在到客户端应用程序之前,需要获取到与服务端应用程序完全相同的数据,即为window.__INITIAL_STATE__,然后通过store.replaceState(window.__INITIAL_STATE__),这样,就会替代本地store中的state,否则,客户端会因为使用与服务器端不同的状态,导致混合失败。
  • 服务器端打包代码是为了提供html静态页面,客户端代码打包时为了后期的交互与数据处理,以及服务端打包失败的的异常处理

2、entry-server的作用:

  • 前端请求过来的时候,根据getMatchedComponents方法 服务端匹配对应的URL,然后会以此寻找对应的组件或者静态资源
  • 该配置主要用于生成 bundle   vue-ssr-server-bundle.json ,生成之后,传递给createBundleRenderer ,这里会检查组件是否有 asyncData 方法,然后下一步
  • pre-fetch,服务器端的数据预获取:如果组件需要预获取数据,我们就调用自己约定好的asyncData 方法,获取到数据,并存储到服务端的store中,然后将解析完成的状态附加给上下文,并且 `template` 选项用于 renderer 时,状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。下面是一个页面简单请求的例子
  • 技术图片

     

  • 异常处理,如果在预获取数据过程中出错了,我们给一个标识  serverError,并注入HTML中,然后浏览器端可以根据此状态进行重新渲染,或者异常展示都可以。

2、由于在node环境中,无法用webpack进行CSS文件处理,所以在配置文件中我们需要对服务端渲染时,进行CSS白名单处理 

1 externals: TARGET_NODE? nodeExternals({whitelist: [/\\.css$/] }):undefined

3、我们从打包出来的代码中可以看到,是vue-ssr-server-bundle.json,而非常规的JS文件。这是因为node环境下,每次打包成js文件,你得考虑node的热部署,并且node环境也不支持sourcemap,所以引入了  vue-server-renderer 组件,这种方式类似于常规的render,支持热部署,支持source map ,在配置文件中,我们配置了该组件下的服务端渲染插件,可以使得服务端渲染输出的是json文件。

4、

在CSR模式下,我们开发过程中会进行热部署,这样会大大提高工作效率。 

 1 let bundle
 2 serverCompiler.watch({}, (err, stats) => {
 3   if (err) {
 4     throw err
 5   }
 6   stats = stats.toJson()
 7   stats.errors.forEach(error => console.error(error))
 8   stats.warnings.forEach(warn => console.warn(warn))
 9   const bundlePath = path.join(
10     webpackConfig.output.path,
11     ‘vue-ssr-server-bundle.json‘
12   )
13   bundle = JSON.parse(mfs.readFileSync(bundlePath, ‘utf-8‘))
14   console.log(‘new bundle generated‘)
15 })

 注意事项

  1.  代码执行环境不同引起的bug
    1. 服务端是没有window,document对象的,storage的实现类也甭想了。。。location更不用说了。。如果加了这些个对象的引用和操作,在服务器端会引起报错,node若报错,可想而知。。
    2. 生命周期的钩子函数  beforeCreate和created会在SSR过程中调用,但是也同时会在客户端执行。。。写代码前最好判断一下你的执行环境。
  2.   接口代理问题:可以采用DevServer进行代理转发请求,或者用axios也可以,不然存在跨域问题
  3. 服务端渲染 的时候必须路由必须使用history模式,处理好请求不在的回调
  4. cookie穿透:因为http请求是先到SSR服务器,再有SSR服务器去后端服务器器扭曲想要的数据接口,客户端请求的时候是带着cookie数据的,而SSR服务器请求后端接口的时候,却没有相应的cookie数据,因此在SSR服务器进行接口请求的时候,我们需要手动拿到客户端的cookie传给后端服务器。我们使用是axios,就可以手动设置axios请求的headers字段,达到cookie穿透的目的。
  5. 数据泄露:因为使用了vuex,如果不使用惰性加载,容易造成数据泄露的情况发生,任何需要登录获取数据的情况,建议在客户端进行。

异常处理

  1、服务器预获取数据的异常处理,参考上面的entry-server中所说,让客户端主动获取数据,再次尝试渲染

  2、在服务端数据预获取的生命周期结束后的渲染页面过程中出现的异常,包括各种操作数据的语法错误等,如对undefined取属性。编写过程要注意全局环境下的代码,是否试用,做环境判断。

缓存策略 

虽然 Vue 的服务器端渲染(SSR)相当快速,但是由于创建组件实例和虚拟 DOM 节点的开销,无法与纯基于字符串拼接(pure string-based)的模板的性能相当。在 SSR 性能至关重要的情况下,明智地利用缓存策略,可以极大改善响应时间并减少服务器负载,这是一个取舍的过程。vue服务区缓存分为页面缓存、组建缓存和接口缓存;

 页面缓存

将渲染完成的页面缓存到内存中,同时设置最大缓存数量和缓存时间。 优势:大幅度提高页面的访问速度 代价:增加服务器内存的使用

 1 const microCache = LRU({
 2   max: 100, // 最大缓存的数目
 3   maxAge: 1000 // 重要提示:条目在 1 秒后过期。
 4 })
 5 const isCacheable = req => {
 6   // 判断是否需要页面缓存
 7   if (req.url && req.url === ‘/‘) {
 8     return req.url
 9   } else {
10     return false
11   }
12 }
13 const handleRequest = async (ctx, next) => {
14   const req = ctx.req
15   const res = ctx.res
16   const cacheable = isCacheable(req)
17   if (cacheable) {
18     const hit = microCache.get(res.url)
19     if (hit) {
20       return res.end(hit)
21     }
22   }
23 ...
24 ...
25 }

 

组件缓存

使用较少,本次并未涉及

接口缓存

将通用的接口缓存到内存,减少服务端接口请求的时间

 get (url, params = {}) {
        // url = baseUrl + ‘/sa‘ + url
        url = baseUrl + url
        const key = md5(url + JSON.stringify(params))
        // 判断是否有缓存,直接返回缓存结果
        if (params.cache && microCache.get(key)) {
          return Promise.resolve(microCache.get(key))
        }
        let Cookie = ‘‘
        if (params.req && params.req.headers && params.req.headers.cookie) {
          Cookie = params.req.headers.cookie
        }
        return new Promise((resolve, reject) => {
          axios({
            url,
            params,
            headers: {
              ‘X-Requested-With‘: ‘XMLHttpRequest‘,
              ‘Cookie‘: Cookie
            },
            method: ‘get‘
          }).then(res => {
            // 判断是否需要缓存 如果需要缓存缓存数据
            if (params.cache && microCache) {
              microCache.set(key, res.data)
            }
            resolve(res.data)
          }).catch(error => {
            reject(error)
          })
        })
      },

 

  

结论

我们是否真正的需要做同构渲染,这取决因素有很多,主要有,SEO,首屏时间比较重要的产品可考虑。

运用该技术并没有想象中的轻松,对前端开发有一定的要求,编写过程要谨慎,涉及构建设置和部署的更多要求,还需要更多的服务器端负载,总之,我们可能需要的支持会更多一点。

代码呈现

 Demo已上传至github,以供参考,如若对您有帮助,给个小星星还是很开心的,传送门

 

以上是关于vue服务端渲染 同构渲染的主要内容,如果未能解决你的问题,请参考以下文章

vue服务端渲染添加缓存的方法

vue服务端渲染添加缓存的方法

vue服务器端渲染

react服务端渲染(同构)

使用 Vue 2.0 实现服务端渲染的 HackerNews

React前后端如何同构,防止重复渲染