今天来说说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
如果觉的有收获,别忘了点赞哈!
网友评论