兄弟你再碰SSR一下,就是不给我胖虎面子!

服务器

浏览数:141

2019-9-7

AD:资源代下载服务

前言:前段时间学习了一下vue的服务端渲染,对某个不知名项目进行了SSR改造,遇到了许多神坑,撰文以记之,希望大家以后遇到类似的问题,不再迷茫!在成长的道路上,不再悲伤!

1、SSR简单入门介绍

SSR,全称server-side-render,即服务端渲染,顾名思义,就是在服务端直接“直出”渲染我们页面的数据,把数据直接填充在html中吐回前端。其实和改革开放时期的jsp渲染,以及后来node端的模板引擎jade、handlerbars等类似,都是把数据填充进了页面,不同的是,以前我们还要再服务端维护一份代码,以往的方案有以下两个缺点:

  • 1、不利于前端后分离。
  • 2、前端修改后后台需同步,带来额外的工作量。

再到后来,21世纪了,大家开始采用vue、react等应用框架来开发页面了,也不再有前面说到的“直出”了,但这毕竟也带来了其他问题,一个是爬虫爬取页面不再那么方便了,因为拉回来的是一个空的html页面,里面没有数据了,其次,数据拉取要等到资源加载结束,框架初始化完毕后才能拉取,在PV较高、对性能要求较高的业务场景下,对用户造成不好的用户体验。
所以vue和react都相应实现了SSR方案来满足这种需求,让你的框架代码可以跑在服务端,同构直出,方便SEO的同时,也缩短页面的首屏展示时间。
服务端渲染和客户端渲染时序图如下所示:
引自:https://www.jianshu.com/p/10b…

2、学习之前的疑问

在做SSR前,希望读者们尝试思考一下,如果需要你实现一套SSR的解决方案,需要完成什么工作,会遇到什么问题,诸如如何让服务端跑同一份代码?有哪些细节需要考虑到?笔者个人整理问题如下:

  • 1、SSR方案是否只优化了首屏渲染?
  • 2、SSR在服务端跑客户端代码,要初始化vue框架,要生成虚拟DOM,最后直出HTML,这个过程是否会对服务端的性能造成影响?
  • 3、客户端代码开发完后,是不是需要在服务端中保留一份完整代码给服务端跑?假如服务端和客户端是分开的两个项目,那岂不是要在服务端安装客户端需要的npm依赖包?
  • 4、根据每个页面的不同路径,都要做相应的直出?
  • 5、前后端请求要一致,前端请求有携带session,后端请求要保证一致的情况下,还需要解决session的问题
  • 6、后台直出html后,那JS、css依赖呢,由谁来填入?
  • 7、后台本质仅仅直出的是HTML字符串,所以前端vue代码初始化的时候,需要接管后台吐出来的html,重新生成虚拟DOM,以及接管后台吐回来的store数据。

如果想要知道以上问题的答案,最后揭晓!

3、SSR原理

前面问了这么多问题,是时候该进入正题了,由官方提供的示意图可以看到,我们的客户端代码,store、components等,通过两个入口文件分别进行打包,生成两个bundle,server-bundle和client-bundle,而server-bundle负责运行在服务端,生成直出的HTML,渲染在前端,而client-bundle则会被混入到HTML中,最后由node-server吐出处理好的HTML。

由于你要让你的客户端代码跑在服务端,你要进行以下操作:

  • 1、编写通用代码,服务端生成虚拟dom的时候,会执行created和beforeCreate函数,当然,你可以在这里拉请求渲染数据,但是不建议在这里做,后面会解释到。所以在初始化,从入口文件进来的每一步,你都要格外小心,避免访问window、document等客户端才有的全局对象,如果不可避免,可以通过假如if else的判断语句来判断是在客户端还是服务端,避免代码执行报错。
  • 2、对原有的store、router、app进行改造,从官网大家也可以看到,我们要避免单例模式,由于webpack打包出来最后模块是以module.exports的形式暴露给其他文件引用,如果你暴露出来的是一个对象,意味着后面的每一次引用,都会返回的是一个对象,可是不同的用户渲染的可是不同的页面,你怎么能让他们共享同一个app、store、router呢?显然不合理,所以需要对原有的store、router等进行改造,封装成工厂函数的形式,避免单例模式。(注意:router要改成history模式,要让请求走到后台,而不是用默认的hash)
  • 3、将组件的请求逻辑提取到asyncdata函数中,为什么呢?为什么我不在created里做这件事呢?原因很简单,服务端直出页面是一个同步的过程,中间渲染到某个组件后,发起了某个异步请求,最后直出了HTML了,请求还没响应,数据还没填充进html,你就返回给用户了,那岂不是根本没有起到服务端渲染数据的作用?所以提取到asyncdata函数的本质目的,是进行请求的统一入口管理,通过手动触发asyncdata函数,return promise对象,让你的应用知道数据返回了,填充好了,之后再把HTML直出吐给用户。


由官网的示例可以看到,至于为什么要传递一个store进来,而不通过this.$store进行访问,小伙子,你还是太年轻了,因为那个时候this.$store还没有挂载store对象,你只能这样传进来给函数调用了~没办法~


再看上面这段代码,通过在entry-server中,调用命中路由组件的asyncdata方法,之后再resolve应用app出去。
在这之前,把store的state赋值给context的state,是为了最后HTML直出的时候,把store的状态序列化后填写进全局的__INITAIL_STATE__变量中。所以相应的,你需要在entry-client文件中,加入以下代码,判断是否让vue接管原有的服务端直出的store数据。

说完以上3点,接下来是你要在配置文件上 以及后台路由上的修改:
1、配置webpack文件,分别打包后台bundle,和前端bundle,两种配置不同,且入口文件不同,前面说了,有两个entry文件。
2、编写通用的路由中间件,用于命中非静态资源、非API接口以外的页面接口,在这个中间件中写入你的直出逻辑,包括调用相应的API,传入相应的bundle文件,初始化fetch,让它携带上客户端带过来的session。

看到这里,是的,这篇文章并不是一篇配置教程,如果要看配置教程直接看官网即可~

4、你可能会经历的坑

这里作者整理了所有在接入SSR的过程中所遇到的问题。

  • 1、cannot read useragent of undefined
global.navigator = { 
    userAgent: 'node', 
}
  • 2、打包后台bundle的时候一直报错,ERROR:Server-side bundle should have one single entry file.Avoid using CommonsChunkPlugin in the server config。 可是我也没有使用多个入口文件啊?也没有使用CommonChunk啊?最后比较配置文件发现,是用了splitchunk导致,所以修改代码如下:

  • 3、后来又发现本地的devserver跑不起来了,页面空白了?是怎么回事呢,原来还是配置文件出了问题,配置了nodeExternal导致本地没有把依赖包打包进去,所以前端页面自然就白屏了。

  • 4、由于服务端代码和客户端代码分开了两个项目,各自维护,导致服务端代码找不到依赖。所以要么把服务端和客户端代码放一起,要么把nodeexternal配置去掉,把依赖打包好。

  • 5、Header is not defined

解决方法: 引入 node-fetch 提供Header接口

  • 6、docunment is NOT DEFINED

首先,排查自己的客户端代码有没有在执行过程中引入document对象导致在服务端执行的,其次,css的一个plugin插件也可能导致webpack打包过程中引入了document对象。

  • 7、一点要保证你的服务端代码允许的时候,window是undefined的,不然vue代码会走进客户端逻辑,调用document的方法,由于作者用了内部的一个服务端框架,导致引入了window对象而报错。
  • 8、这里作者还遇到了一个问题,在直出HTML的时候,vue-server-render源码的里执行报错undefined,在分析文件依赖的时候除了问题,数组中多了个undefined,作者手动改了源码剔除了undefined的情况,非常的 粗暴

  • 9、对服务端fetch进行改造,由于客户端发请求使用fetch的没有用axios。要请求做到一致,所以你要在加上url前缀以及加上cookie。

  • 10、对于数据拉取需要放置到asyncdata函数中,返回promise对象,其次,需要给函数注入store对象。promise resolve后才可以返回HTML,否则返回空页面。
  • 11、后来又爆了Error in callback for watcher function()… do not mutate vuex store state outside mutation handlers. 其实是由于在createStore的时候,我提取了一个对象在工厂函数外,导致这个对象被共用了,因为模块只会初始化一次,这个对象由于闭包的原因被保留了下来,被所有用户请求到来的时候共用了,所以要避免这个问题,就是要把所有的store对象涉及到的state,老老实实的放进工厂函数中,大家不共用。

5、总结

故事说到了这里,你可能已经对SSR有了更深入的思考或者了解,或者 更懵逼了,但这都没有关系。
接下来我解答一下前面发问的问题:

  • 1、SSR方案是否只优化了首屏渲染?

答:是的。

  • 2、SSR在服务端跑客户端代码,要初始化vue框架,要生成虚拟DOM,最后直出HTML,这个过程是否会对服务端的性能造成影响?

答:由于要在服务端执行vue代码,而node本身是单线程的,所以请求上来了,本身还是会造成性能影响的,所以要做好取舍。

  • 3、客户端代码开发完后,是不是需要在服务端中保留一份完整代码给服务端跑?假如服务端和客户端是分开的两个项目,那岂不是要在服务端安装客户端需要的npm依赖包?

答:生成不同的bundle文件,在生成服务端的bundle文件的时候,可以把依赖包都打进去,不配置nodeExternal。

  • 4、根据每个页面的不同路径,都要做相应的直出?

答:是的,需要为router设置history模式,让请求可以走到后台。

  • 5、前后端请求要一致,前端请求有携带session,后端请求要保证一致的情况下,还需要解决session的问题。

答:是的,需要再处理请求前,对fetch进行改写,加上urlprefix以及session。

  • 6、后台直出html后,那JS、css依赖呢,由谁来填入?

答:由于打包前端代码的时候,会生成一个json文件,提供给服务端,所以服务端直出的时候也会把相应的依赖填写进去。

  • 7、后台本质仅仅直出的是HTML字符串,所以前端vue代码初始化的时候,需要接管后台吐出来的html,重新生成虚拟DOM,以及接管后台吐回来的store数据。

答:是的。

本期讲堂到此结束,请大家踊跃发言!欢迎讨论!谢谢!
温馨提示:将会在评论中选出一个幸运儿!奖励一个价值不超过1块钱的红包哦!

作者:曾培森