服务端渲染的本质是:生成应用程序的“快照”。将Vue及对应库运行在服务端,此时,Web Server Frame实际上是作为代理服务器去访问接口服务器来预拉取数据,从而将拉取到的数据作为Vue组件的初始状态。
服务端渲染的原理是:虚拟DOM。在Web Server Frame作为代理服务器去访问接口服务器来预拉取数据后,这是服务端初始化组件需要用到的数据,此后,组件的beforeCreate和created生命周期会在服务端调用,初始化对应的组件后,Vue启用虚拟DOM形成初始化的HTML字符串。之后,交由客户端托管。实现前后端同构应用。
我所在的项目使用Koa作为Web Server Frame,项目使用koa-webpack进行开发环境的构建。如果是在产品环境下,会生成vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,包含对应的Bundle,提供客户端和服务端引用,而在开发环境下,一般情况下放在内存中。使用memory-fs模块进行读取。
随着各大前端框架的诞生和演变,
SPA
开始流行,单页面应用的优势在于可以不重新加载整个页面的情况下,通过ajax
和服务器通信,实现整个Web
应用拒不更新,带来了极致的用户体验。然而,对于需要SEO
、追求极致的首屏性能的应用,前端渲染的SPA
是糟糕的。好在Vue 2.0
后是支持服务端渲染的,零零散散花费了两三周事件,通过改造现有项目,基本完成了在现有项目中实践了Vue
服务端渲染。关于Vue服务端渲染的原理、搭建,官方文档已经讲的比较详细了,因此,本文不是抄袭文档,而是文档的补充。特别是对于如何与现有项目进行很好的结合,还是需要费很大功夫的。本文主要对我所在的项目中进行
Vue
服务端渲染的改造过程进行阐述,加上一些个人的理解,作为分享与学习。概述
本文主要分以下几个方面:
Koa
的Web Server Frame
上配置服务端渲染?Webpack
配置vue-router
分割代码;什么是服务端渲染?服务端渲染的原理是什么?
上面这段话是源自Vue服务端渲染文档的解释,用通俗的话来说,大概可以这么理解:
HTML
字符串,客户端接收到对应的HTML
字符串,能立即渲染DOM
,最高效的首屏耗时。此外,由于服务端直接生成了对应的HTML
字符串,对SEO
也非常友好;Vue
及对应库运行在服务端,此时,Web Server Frame
实际上是作为代理服务器去访问接口服务器来预拉取数据,从而将拉取到的数据作为Vue
组件的初始状态。DOM
。在Web Server Frame
作为代理服务器去访问接口服务器来预拉取数据后,这是服务端初始化组件需要用到的数据,此后,组件的beforeCreate
和created
生命周期会在服务端调用,初始化对应的组件后,Vue
启用虚拟DOM
形成初始化的HTML
字符串。之后,交由客户端托管。实现前后端同构应用。如何在基于
Koa
的Web Server Frame
上配置服务端渲染?基本用法
需要用到
Vue
服务端渲染对应库vue-server-renderer
,通过npm
安装:最简单的,首先渲染一个
Vue
实例:与服务器集成:
使用页面模板:
当你在渲染
Vue
应用程序时,renderer
只从应用程序生成HTML
标记。在这个示例中,我们必须用一个额外的HTML
页面包裹容器,来包裹生成的HTML
标记。为了简化这些,你可以直接在创建
renderer
时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中:然后,我们可以读取和传输文件到
Vue renderer
中:Webpack配置
然而在实际项目中,不止上述例子那么简单,需要考虑很多方面:路由、数据预取、组件化、全局状态等,所以服务端渲染不是只用一个简单的模板,然后加上使用
vue-server-renderer
完成的,如下面的示意图所示:如示意图所示,一般的
Vue
服务端渲染项目,有两个项目入口文件,分别为entry-client.js
和entry-server.js
,一个仅运行在客户端,一个仅运行在服务端,经过Webpack
打包后,会生成两个Bundle
,服务端的Bundle
会用于在服务端使用虚拟DOM
生成应用程序的“快照”,客户端的Bundle
会在浏览器执行。因此,我们需要两个
Webpack
配置,分别命名为webpack.client.config.js
和webpack.server.config.js
,分别用于生成客户端Bundle
与服务端Bundle
,分别命名为vue-ssr-client-manifest.json
与vue-ssr-server-bundle.json
,关于如何配置,Vue
官方有相关示例vue-hackernews-2.0开发环境搭建
我所在的项目使用
Koa
作为Web Server Frame
,项目使用koa-webpack进行开发环境的构建。如果是在产品环境下,会生成vue-ssr-client-manifest.json
与vue-ssr-server-bundle.json
,包含对应的Bundle
,提供客户端和服务端引用,而在开发环境下,一般情况下放在内存中。使用memory-fs
模块进行读取。渲染中间件配置
产品环境下,打包后的客户端和服务端的
Bundle
会存储为vue-ssr-client-manifest.json
与vue-ssr-server-bundle.json
,通过文件流模块fs
读取即可,但在开发环境下,我创建了一个appSSR
模块,在发生代码更改时,会触发Webpack
热更新,appSSR
对应的bundle
也会更新,appSSR
模块代码如下所示:通过引入
appSSR
模块,在开发环境下,就可以拿到clientManifest
和ssrBundle
,项目的渲染中间件如下:如何对现有项目进行改造?
基本目录改造
使用
Webpack
来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用Webpack
支持的所有功能。一个基本项目可能像是这样:
app.js
是我们应用程序的「通用entry
」。在纯客户端应用程序中,我们将在此文件中创建根Vue
实例,并直接挂载到DOM
。但是,对于服务器端渲染(SSR
),责任转移到纯客户端entry
文件。app.js
简单地使用export
导出一个createApp
函数:避免创建单例
如
Vue SSR
文档所述:如上代码所述,
createApp
方法通过返回一个返回值创建Vue
实例的对象的函数调用,在函数createVueInstance
中,为每一个请求创建了Vue
,Vue Router
,Vuex
实例。并暴露给entry-client
和entry-server
模块。在客户端
entry-client.js
只需创建应用程序,并且将其挂载到DOM
中:服务端
entry-server.js
使用default export
导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配和数据预取逻辑:在服务端用
vue-router
分割代码与
Vue
实例一样,也需要创建单例的vueRouter
对象。对于每个请求,都需要创建一个新的vueRouter
实例:同时,需要在
entry-server.js
中实现服务器端路由逻辑,使用router.getMatchedComponents
方法获取到当前路由匹配的组件,如果当前路由没有匹配到相应的组件,则reject
到404
页面,否则resolve
整个app
,用于Vue
渲染虚拟DOM
,并使用对应模板生成对应的HTML
字符串。在服务端预拉取数据
在
Vue
服务端渲染,本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。服务端Web Server Frame
作为代理服务器,在服务端对接口服务发起请求,并将数据拼装到全局Vuex
状态中。另一个需要关注的问题是在客户端,在挂载到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
目前较好的解决方案是,给路由匹配的一级子组件一个
asyncData
,在asyncData
方法中,dispatch
对应的action
。asyncData
是我们约定的函数名,表示渲染组件需要预先执行它获取初始数据,它返回一个Promise
,以便我们在后端渲染的时候可以知道什么时候该操作完成。注意,由于此函数会在组件实例化之前调用,所以它无法访问this
。需要将store
和路由信息作为参数传递进去:举个例子:
在
entry-server.js
中,我们可以通过路由获得与router.getMatchedComponents()
相匹配的组件,如果组件暴露出asyncData
,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文中。客户端托管全局状态
当服务端使用模板进行渲染时,
context.state
将作为window.__INITIAL_STATE__
状态,自动嵌入到最终的HTML
中。而在客户端,在挂载到应用程序之前,store
就应该获取到状态,最终我们的entry-client.js
被改造为如下所示:常见问题的解决方案
至此,基本的代码改造也已经完成了,下面说的是一些常见问题的解决方案:
window
、location
对象:对于旧项目迁移到
SSR
肯定会经历的问题,一般为在项目入口处或是created
、beforeCreate
生命周期使用了DOM
操作,或是获取了location
对象,通用的解决方案一般为判断执行环境,通过typeof window
是否为'undefined'
,如果遇到必须使用location
对象的地方用于获取url
中的相关参数,在ctx
对象中也可以找到对应参数。vue-router
报错Uncaught TypeError: _Vue.extend is not _Vue function
,没有找到_Vue
实例的问题:通过查看
Vue-router
源码发现没有手动调用Vue.use(Vue-Router);
。没有调用Vue.use(Vue-Router);
在浏览器端没有出现问题,但在服务端就会出现问题。对应的Vue-router
源码所示:hash
路由的参数由于
hash
路由的参数,会导致vue-router
不起效果,对于使用了vue-router
的前后端同构应用,必须换为history
路由。cookie
的问题:由于客户端每次请求都会对应地把
cookie
带给接口侧,而服务端Web Server Frame
作为代理服务器,并不会每次维持cookie
,所以需要我们手动把cookie
透传给接口侧,常用的解决方案是,将ctx
挂载到全局状态中,当发起异步请求时,手动带上cookie
,如下代码所示:当发起异步请求时,手动带上
cookie
,项目中使用的是Axios
:connect ECONNREFUSED 127.0.0.1:80
的问题原因是改造之前,使用客户端渲染时,使用了
devServer.proxy
代理配置来解决跨域问题,而服务端作为代理服务器对接口发起异步请求时,不会读取对应的webpack
配置,对于服务端而言会对应请求当前域下的对应path
下的接口。解决方案为去除
webpack
的devServer.proxy
配置,对于接口请求带上对应的origin
即可:vue-router
配置项有base
参数时,初始化时匹配不到对应路由的问题在官方示例中的
entry-server.js
:原因是设置服务器端
router
的位置时,context.url
为访问页面的url
,并带上了base
,在router.push
时应该去除base
,如下所示:小结
本文为笔者通过对现有项目进行改造,给现有项目加上
Vue
服务端渲染的实践过程的总结。首先阐述了什么是
Vue
服务端渲染,其目的、本质及原理,通过在服务端使用Vue
的虚拟DOM
,形成初始化的HTML
字符串,即应用程序的“快照”。带来极大的性能优势,包括SEO
优势和首屏渲染的极速体验。之后阐述了Vue
服务端渲染的基本用法,即两个入口、两个webpack
配置,分别作用于客户端和服务端,分别生成vue-ssr-client-manifest.json
与vue-ssr-server-bundle.json
作为打包结果。最后通过对现有项目的改造过程,包括对路由进行改造、数据预获取和状态初始化,并解释了在Vue
服务端渲染项目改造过程中的常见问题,帮助我们进行现有项目往Vue
服务端渲染的迁移。