一、为什么应用SSR ?

在传统vue单页面利用中,页面的渲染都是由js实现,如下图所示,在服务端返回的html文件中,body中只有一个div标签和一个script标签,页面其余的dom构造都将由bundle.js生成,而后挂载到<div id="app"></div>上。这让搜索引擎爬虫抓取工具无奈爬取页面的内容,如果 SEO 对你的站点很重要,则你可能须要服务器端渲染(SSR)解决此问题。


除了SEO,应用SSR还能放慢首屏的出现速度,因为服务端间接返回渲染好的页面html,不须要js就能看到残缺渲染的页面。比起单页利用通常比拟大的js文件,这部分代码量很小,所以首屏的达到工夫会更快,白屏的工夫更短。

当然,SSR的应用也有一些局限性,首先,开发条件受限,在服务端渲染中,created和beforeCreate之外的生命周期钩子不可用。其次,更多的服务器端负载,在服务端中渲染残缺的应用程序,显然会比仅仅提供动态文件的服务器更加占用 CPU 资源。此外,SSR在部署方面有更多要求。与能够部署在任何动态文件服务器上的齐全动态单页面应用程序(SPA)不同,服务器渲染应用程序,须要处于Node.js的运行环境。所以波及到SSR技术选型的时候,要综合思考它的优缺点,看看是否有必要应用。

二、根底性能实现

SSR的实质就服务端返回渲染好的html文档。咱们先在我的项目根目录启动一个服务器,而后返回一个html文档。这里咱们应用koa作为服务端框架。

//server.jsconst Koa = require('koa')const router = require('koa-router')()const koa = new Koa()koa.use(router.routes())router.get('/',(ctx)=>{  ctx.body = `<!DOCTYPE html>      //要返回给客户端的html  <html lang="en">    <head><title>Vue SSR</title></head>    <body>      <div>This is a server render page</div>    </body>  </html>`})koa.listen(9000, () => {  console.log('server is listening in 9000');})

在命令行启动服务器: node server.js, 而后在浏览器拜访http://localhost:9000/,服务端回返回的内容如下,浏览会依据这段html,渲染出页面。

vue-server-renderer

当然,要返回的html字符串能够是由vue模板生成的,这就须要用到vue-server-renderer,它会基于Vue实例生成html字符串,是Vue SSR的外围。在下面的server.js中稍作批改:

const Koa = require('koa')const router = require('koa-router')()const koa = new Koa()koa.use(router.routes())const Vue = require('Vue')     //导入Vue,用于创立Vue实例const renderer = require('vue-server-renderer').createRenderer()  //创立一个 renderer 实例const app = new Vue({          //创立Vue实例  template: `<div>{{msg}}</div>`,  data(){    return {      msg: 'This is renderred by vue-server-renderer'    }  }})router.get('/',(ctx)=>{  //调用renderer实例的renderToString办法,将Vue实例渲染成字符串  //该办法承受两个参数,第一个是Vue实例,第二个是一个回调函数,在渲染实现后执行  renderer.renderToString(app, (err, html) => {   //渲染失去的字符串作为回调函数的第二个参数传入    ctx.body = `<!DOCTYPE html>    <html lang="en">      <head><title>Vue SSR</title></head>      <body>        ${html}    //将渲染失去的字符串拼接到要返回的后果中      </body>    </html>`  })})koa.listen(9000, () => {  console.log('server is listening in 9000');})

重启服务器,再拜访:

这样,咱们就实现了一个极其根底的Vue SSR。然而不太具备实操性,咱们在理论我的项目开发时,是不可能这样写的,咱们会模块化地搭建我的项目,而后通过打包工具打包成一个或多个js文件。

正式一点的应用

搭建一个模块化的vue我的项目

咱们模块化地搭建一个简略地vue我的项目,用vue-router治理路由。

// 打包入口文件 src/main.jsimport Vue from 'vue'import App from './App.vue'import router from './router'Vue.config.productionTip = falsenew Vue({  el: '#app',  router,  render: h => h(App)})
//  src/App.vue<template>  <div id="app">    <div id="nav">      <router-link to="/">Home</router-link> |      <router-link to="/about">About</router-link>    </div>    <router-view/>  </div></template><style lang="less">#app{  margin: 0 auto;  width: 700px;  #nav{    margin-bottom: 20px;    text-align: center;  }}</style>
//  src/router/index.jsimport Vue from 'vue'import VueRouter from 'vue-router'import Home from '../views/Home.vue'import About from '../views/About.vue'Vue.use(VueRouter)const routes = [  {    path: '/',    name: 'Home',    component: Home  },  {    path: '/about',    name: 'About',    component: About  }]export default new VueRouter({  mode: 'history',  routes})
// src/views/Home.vue<template>  <div class="home">    <h1>This is home page</h1>  </div></template>
// src/views/About.vue<template>  <div class="about">    <h1>This is an about page</h1>  </div></template>

src/main.js作为打包入口文件,依照客户端单页面的形式打包,而后在浏览器关上,渲染后果如下:

将我的项目革新成服务端渲染

咱们接下来就把下面这个demo革新成服务端渲染。

次要的革新点:服务端渲染须要Vue实例,每一次客户端申请页面,服务端渲染都是用一个新的Vue实例,不同的用户不能拜访同一个Vue实例。所以服务端须要一个生成Vue实例的工厂函数,每次渲染由这个工厂函数生成Vue实例。

新建一个专门用于服务端渲染的入口文件entry.server.js:

import { createApp } from './main'export default context => {  //生成Vue实例的工厂函数,  return new Promise((resolve, reject) => {    const app = createApp()    const router = app.$router    const { url } = context    //context蕴含服务端须要传递给Vue实例的一些数据,比方这里的路由    const { fullPath } = router.resolve(url).route    if(fullPath !== url){  //判断以后路由在Vue实例中是否存在      return reject({        url: fullPath      })    }    router.push(url)      //设置Vue实例的以后路由    router.onReady(() => {      const matchedComponents = router.getMatchedComponents()  //判断以后路由是否有对应组件      if(!matchedComponents.length){        return reject({          code: 404        })      }      resolve(app)    //返回Vue实例    }, reject)  })}

src/main.js革新成如下:

import Vue from 'vue'import App from './App.vue'import { createRouter } from './router'Vue.config.productionTip = falseexport function createApp(){  const router = createRouter()  const app = new Vue({    router,    render: h => h(App)  })  return app}

基于entry.server.js打包的webpack配置,也要作一些批改:

target: 'node',entry: './src/entry.server.js',output: {  path: path.join(__dirname, '../dist'),  filename: 'bundle.server.js',  libraryTarget: 'commonjs2'},

而后,在服务端,咱们就能够通过打包后的bundle.server.js进行服务端渲染了。

//server.js作如下扭转:const renderer = require('vue-server-renderer').createRenderer({   //基于模板创立一个 renderer 实例  template: require('fs').readFileSync('./index.template.html', 'utf-8')})const app = require('./dist/bundle.server.js').default    //导入Vue实例工厂函数router.get('/(.*)', async (ctx, next) => {  const context = {                   //获取路由,用于传递给Vue实例    url: ctx.url  }  let htmlStr  await app(context).then( res => {    //生成Vue实例,并传递给renderer实例生成字符串    renderer.renderToString(res, context, (err,html)=>{      if(!err){        htmlStr = html      }    })  })  ctx.body = htmlStr});


能够看到,这里咱们曾经实现了服务端的渲染,页面dom构造呈现在了由服务器返回的html文档中。

客户端激活

咱们曾经窥见了一点在理论我的项目应用SSR的曙光,然而这只是第一步。当初每次点击Home/About都会从服务端申请html资源,单页面的前端路由劣势并没有施展。接下来咱们将加上一步客户端激活,让网页利用同时具备单页面的劣势。这也是Vue SSR的官网流程。

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的动态 HTML,使其变为由 Vue 治理的动静 DOM 的过程。 激活原理参考官网。

操作起来很简略,就是在返回的html页面中,加上client bundle,用于在客户端治理以后html。上面咱们来打包生成它。

新建一个entry.client.js

import { createApp } from './main'const app = createApp()const router = app.$routerrouter.onReady(() => {  app.$mount('#app')      //服务端渲染默认会生成一个id为app的div})

打包的webpack配置:

entry: './src/entry.client.js',output: {  path: path.join(__dirname, '../dist'),  filename: 'bundle.client.js'},

打包实现后,就是把bundle.client.js加到html中,之前咱们是基于模板渲染:

const renderer = require('vue-server-renderer').createRenderer({   //基于模板创立一个 renderer 实例  template: require('fs').readFileSync('./index.template.html', 'utf-8')})

所以只须要把bundle.client.js加到index.template.html就能够了。

//index.template.html<!DOCTYPE html><html lang="en">  <head><title>Vue SSR</title></head>  <body>    <!--vue-ssr-outlet-->  </body>  <script src="bundle.client.js"></script></html>

重启服务,再拜访,就能够看到,点击Home/About切换路由时,不会再从服务器申请html文档了。

三、申请数据

在理论我的项目中,页面往往是由从接口申请的数据填充渲染进去的,上面咱们将用申请的数据来渲染页面。为了不便(省事),就不另外写数据接口了,咱们去申请豆瓣的电影排行前20的数据。

具体思路,就是如果一个组件须要申请数据,当它是服务端渲染时,咱们在服务端申请数据,当客户端以SPA的路由切换形式应用该组件时,也能在客户端发送ajax申请数据。

咱们将借助vuex来实现。 因为它将数据挂载在vue实例上,传递拜访数据真的很不便。

在服务器端申请数据

回顾咱们客户端渲染惯例的申请数据的场景,在created或mounted钩子函数中发送ajax申请,申请胜利后把返回数据写到实例的data中。SSR的申请数据不能这样,因为ajax申请是异步申请,申请收回去之后,数据还没返回,后端就曾经渲染完了,ajax申请的数据无奈填充到页面中。

所以咱们间接由服务端发送申请获取数据,也就是一个服务器向另一个服务器发送http申请,和客户端向服务器发送申请不同,这里咱们应用axios,这两种它都反对。

对于每个须要申请数据的组件,咱们将在组件上暴露出一个自定义静态方法asyncData,因为此函数会在组件实例化之前调用,所以它无法访问 this。须要将 store 和路由信息作为参数传递进去。

//Home.vue<template>  <div class="movie-list">    <div v-for="(item, index) in list" class="movie">      <img class="cover" :src="item.cover">      <p>        <span class="title">{{item.title}}</span>        <span class="rate">{{item.rate}}</span>      </p>    </div>  </div></template><script>  export default {    name: 'MovieList',    asyncData ({ store,route }) {    //自定义静态方法asyncData      return store.dispatch('getTopList')         },        /*****    在这里,执行asyncData,就会调用getTopList办法去申请数据    并将数据更新到vue实例的$store.state中    actions: {                  getTopList (store) {              return top20().then((res) => {          store.commit('setTopList', res.data.subjects)        })      }    }    *****/        computed: {      list () {        return this.$store.state.topList      }    },    created () {      if(!this.$store.state.topList){        this.$store.dispatch('getTopList')      }    }  }</script>

entry.server.js 中,咱们通过路由取得了与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,就调用这个办法。而后咱们须要将解析实现的状态,附加到渲染上下文(render context)中。

import { createApp } from './main'export default context => {  return new Promise((resolve, reject) => {    const app = createApp()    const router = app.$router    const store = app.$store    ...    router.onReady(() => {      const matchedComponents = router.getMatchedComponents()      if(!matchedComponents.length){        return reject({          code: 404        })      }      Promise.all(matchedComponents.map(Component => {        if (Component.asyncData) {          //如果组件暴露出 asyncData,就调用这个办法          //在本例中,就会去申请豆瓣数据,并把数据更新到app.$store.state          return Component.asyncData({              store,            route: router.currentRoute          })        }      })).then(() => {        context.state = store.state  //将app.$store.state赋值给渲染上下文context.state,前面同步数据到客户端的时候会用到。        resolve(app)      }).catch(reject)    }, reject)  })  })}

当在数据更新到app.$store.state后,服务端渲染的html中就有数据了。可是页面是空白的,并且发送了ajax申请。起因是当客户端激活其实通过了二次渲染,也就是当bundle.client.js加载并执行后,页面由bundle.client.js再次渲染,通常来说,渲染后果会跟之前一样,所以觉察不到。

防止客户端反复申请数据

这里是跨域,所以ajax申请没有胜利。如果不是跨域,页面也能呈现内容的,是由客户端发送ajax取得的数据渲染而得。但在服务端曾经申请的数据,在客户端应该防止反复申请,怎么同步数据到客户端?

当应用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,主动嵌入到最终的 HTML 中。而在客户端激活时,在挂载到应用程序之前,客户端的vm.$store 就应该获取到window.__INITIAL_STATE__ 状态。

1.在server.js中,为renderer.renderToString办法增加第二个参数context,context.state 将作为 window.__INITIAL_STATE__ 状态,主动嵌入到最终的 HTML 中。

router.get('/(.*)', async (ctx, next) => {  const context = {    url: ctx.url  }  let htmlStr  await app(context).then( res => {    renderer.renderToString(res, context, (err,html)=>{  //增加第二个参数context      if(!err){        htmlStr = html      }    })  })  ctx.body = htmlStr});

  1. 批改entry.client.js

    import { createApp } from './main'const app = createApp()const router = app.$routerconst store = app.$storeif (window.__INITIAL_STATE__) {   //如果window.__INITIAL_STATE__有内容,就存到app.$store中  store.replaceState(window.__INITIAL_STATE__)}router.onReady(() => {  app.$mount('#app')})

    这样就防止了客户端反复申请数据,最终成果如下,能够看到客户端没有发送ajax申请了。

这个我的项目搭建很繁难,次要是整顿一下Vue SSR的原理、流程,理论开发能够抉择nuxt.js这种比拟成熟的框架。

我的项目地址: https://github.com/alasolala/...

参考资料

解密Vue SSR

手把手教你搭建SSR(vue/vue-cli + express)

Vue SSR Guide