程序员技术栈程序员小知识

vue用法指南05(vue服务端渲染详解)

2019-03-01  本文已影响6人  Mr绍君

今天来说说vue的服务端渲染。

至于为什么要用服务端渲染,以及服务端渲染的好处?这个问题其实在官网上写的很详细,我截两张图,给大家参考一下。

在说服务端渲染之前,我们先来说说预渲染。

预渲染的使用也非常简单,不仅仅是静态的内容,异步的请求也能渲染,但是它无法实时动态的编译HTML。


华丽的分割线,下面根据官方文档来过一遍vue的服务端渲染。当然,只是单纯的想用一下服务端渲染的话,官网建议我们使用nuxt框架。(这里不做介绍,如果大家之前没有接触过的话,可以参考一下我的博客demo(https://gitee.com/yeshaojun/blog))

文章中的案例代码我到时候会放在码云上,大家可以下载参考一下(地址在最后)。

1.Vue服务端渲染的基本用法

我们先看一个最简单的案例

这里面的核心库是vue-server-renderer,它可以把vue实例对象,渲染成html。express是node的一个第三方库,作用是启动一个服务。

我们来梳理一下逻辑。

当我们访问页面的时候,会生成一个vue的实例,然后我们把这个实例传给vue-server-renderer,然后它会进行编译,在返回给我们一个html,然后我们再显示到页面上。

理清楚了逻辑之后,我们再看下一个例子。

2.Vue服务端渲染基本实现

我们来实现一个简单的vue服务端渲染。

先来看demo的结构。(依赖如果安装不上,可以把杀毒软件关掉然后删除node_modules重新试一下)

先解释一下为什么要有client和server两个文件,server负责处理vue的实例,然后将结果传给vue-server-renderer,client负责挂载到html上。(大家可以看一下之前的理逻辑的图,或者一会看代码清楚了)


我们一个个文件来分析。

build文件中是webpack的配置文件(如果大家对webpack不太熟悉,可以先看我之前的文章,或者直接拿来用就行,我这里也用的是官方的demo)

在配置文件中,有一个插件需要注意一下(VueSSRServerPlugin),它会自动生成json来对应我们的打包文件。

路由配置文件,import写法也是官方推荐的代码分割写法,可以实现懒加载优化性能。

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

const home = () => import('../views/home.vue')
const test1 = () => import('../views/test1.vue')
const test2 = () => import('../views/test2.vue')
const test3 = () => import('../views/test3.vue')

export function createRouter () {
  return new Router({
    mode: 'history',
    fallback: false,
    routes: [
      { path: '/', component: home },
      { path: '/test1', component: test1 },
      { path: '/test2', component: test2 },
      { path: '/test3', component: test3 }
    ]
  })
}

store文件夹,目前只是一个空架子,里面是没有内容的,暂时先不讲。

views文件夹下是4个页面,每个页面的内容类似,只有如下的一句话。

<template>
    <div>
        {{msg}}
    </div>
</template>

<script>
export default {
    data () {
        return {
            msg: 'this is home'
        }
    }
}
</script>

app.js文件,暴露一个创建vue实例的工厂方法。

import Vue from 'vue'
import App from './App.vue'
import { createStore } from './store'
import { createRouter } from './router'
import { sync } from 'vuex-router-sync'

export function createApp () {
  const store = createStore()
  const router = createRouter()

  sync(store, router)

  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  return { app, router, store }
}

entry-client.js这个文件也很简单,只是实现一下挂载。

import 'es6-promise/auto'
import { createApp } from './app'

const { app, router } = createApp()

router.onReady(() => {
  app.$mount('#app')
})

entry-client.js 这个文件要实现vue实例的创建,组件的加载,并把结果暴露出去。

import { createApp } from './app'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()

    // context 是express传入的请求参数
    const { url } = context
    const { fullPath } = router.resolve(url).route

    if (fullPath !== url) {
      return reject({ url: fullPath })
    }

    // 设置服务器端 router 的位置
    router.push(url)

    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // no matched routes
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
}

模板文件,注意(模板里面的注释一定要写,因为vue-ssr-server会根据这个注释来进行挂载)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{title}}</title>
</head>
<body>
  <!--vue-ssr-outlet-->
</body>
</html>

server.js文件

const fs = require('fs')
const path = require('path')
const express = require('express')
const resolve = file => path.resolve(__dirname, file)
const { createBundleRenderer } = require('vue-server-renderer')

const app = express()

const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && 1000 * 60 * 60 * 24 * 30
})

// 设置静态资源
app.use('/dist', serve('./dist', true))

function createRenderer (bundle, options) {
  return createBundleRenderer(bundle, Object.assign(options, {
    basedir: resolve('./dist'),
    runInNewContext: false
  }))
}

// 引入模板文件,fs.readFileSync读取文件内容
const templatePath = resolve('./src/index.template.html')
const template = fs.readFileSync(templatePath, 'utf-8')

// 这里只需要引入json文件,因为VueSSRServerPlugin会帮我们做映射
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')

// 官方用法,可把clien和server文件传入,vue-ssr-server会自动做处理
const renderer = createRenderer(bundle, {
  template,
  clientManifest
})

function render (req, res) {
  const context = {
    title: 'ssr demo', // default title
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      res,send(err)
    }
    res.send(html)
  })
}

app.get('*',  render)

// 端口监听
const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})

我们先来看一下结果。


我们再来理一下demo2的逻辑。(建议对着demo的图看)

第一步生成一个vue实例。
我们用工厂模式来生成vue实例,至于为什么要这么做,官方也给了解释。

第二步将实例传给vue-server-render
我们通过webpack打包entry-server.js文件,并在server.js文件中引入。

第三步接收html以及第四步显示页面
我们通过模板,以及entry-client.js来挂载

这么一来,是不是也不复杂。

3.Vue服务端渲染异步实现

我们再进一步,通过vuex把服务端渲染完成。

在store下的文件。

// actions
import axios from 'axios'
export default {
    TEST_LIST: ({ commit }) => {
     return axios.get('https://api.yeshaojun.com/api/article/list?page=1&pageSize=10').then(res => {
        commit('TEST_LIST', res.data.data)
     })   
    }
}

// mutations
export default {
    TEST_LIST: (state, item) => {
        state.list = item
    }
}

// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions'
import mutations from './mutations'
import getters from './getters'

Vue.use(Vuex)

export function createStore () {
  return new Vuex.Store({
    state: {
      list: []
    },
    actions,
    mutations,
    getters
  })
}

修改home.vue文件

<template>
    <div>
        {{msg}}
        <ul>
            <li v-for="(item,idx) in list" :key="idx">
                {{item.title}}
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data () {
        return {
            msg: 'this is home'
        }
    },
    computed: {
        list () {
            return this.$store.state.list
        }
    },
    asyncData ({ store }) {
        return store.dispatch('TEST_LIST')
    }
}
</script>

接下来,我们再来修改client和server。

// client
import Vue from 'vue'
import 'es6-promise/auto'
import { createApp } from './app'

// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
Vue.mixin({
  beforeRouteUpdate (to, from, next) {
    const { asyncData } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else {
      next()
    }
  }
})

const { app, router, store } = createApp()

// 将clien的store与sever的store同步
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  // 添加路由钩子函数,用于处理 asyncData.
  // 在初始路由 resolve 后执行,
  // 以便我们不会二次预取(double-fetch)已有的数据。
  // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // 我们只关心非预渲染的组件
    // 所以我们对比它们,找出两个匹配列表的差异组件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // 这里如果有加载指示器 (loading indicator),就触发

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // 停止加载指示器(loading indicator)

      next()
    }).catch(next)
  })
  app.$mount('#app')
})

import { createApp } from './app'

const isDev = process.env.NODE_ENV !== 'production'

export default context => {
  return new Promise((resolve, reject) => {
    const s = isDev && Date.now()
    const { app, router, store } = createApp()

    const { url } = context
    const { fullPath } = router.resolve(url).route

    if (fullPath !== url) {
      return reject({ url: fullPath })
    }

    // set router's location
    router.push(url)

    // wait until router has resolved possible async hooks
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // no matched routes
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // 对所有匹配的路由组件调用 `asyncData()`
      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
        store,
        route: router.currentRoute
      }))).then(() => {
        // 在所有预取钩子(preFetch hook) resolve 后,
        // 我们的 store 现在已经填充入渲染应用程序所需的状态。
        // 当我们将状态附加到上下文,
        // 并且 `template` 选项用于 renderer 时,
        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

我们来看一下结果。

我们再来理一下demo3的逻辑,demo3的逻辑其实跟demo2是一样的,只不过在server和client加入了判断,判断是否有异步请求,如果有,则先执行异步,然后再进行后面的操作。

因为client和server中的store的数据是不一样的,所以最后需要同步一下。

到此,vue的服务端渲染就完成了,但是其实还有很多问题,比如我们在开发的时候希望是npm run dev,启动一个服务,部署的时候再build。

vue官方有一个demo,里面有比较详细的配置。本文也是参考官方文档和官方demo写出来的,大家可以先看官方文档,对照着来学习。

官方文档:https://ssr.vuejs.org/zh/
官方demo: https://github.com/vuejs/vue-hackernews-2.0

文章中demo地址:https://gitee.com/yeshaojun/vue-ssr

如果觉的有收获,别忘了点赞哈!

上一篇下一篇

猜你喜欢

热点阅读