Vue SSR

浏览器渲染和服务端渲染

服务端渲染:在服务端将对应数据请求完,在后端拼装好页面返回给前端

好处:利于SEO优化,减少首屏加载时间

缺点:占用大量内存和cpu,一些生命周期不能用, 没有beforemounted、mounted生命周期

客户端渲染可能会出现白屏。

一、基本用法

1.安装插件:

npm install vue npm install vuex npm install vue-router npm install vue-server-renderer

npm install koa(node框架) koa-router(后端路由) koa-static(后端返回的静态页面)

2.逻辑代码

const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统

const Vue = require('vue')
const vm = new Vue({
    //服务端没有el
    data(){
        return {
            name: 'zf',
            age:10
        }
    },
    //渲染模板
    template:`
    <div>
        <p>{{name}}</p>
        <span>{{age}}</span>
    </div>
    `
})
const VueServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const render = VueServerRender.createRenderer();//创建一个渲染器  
router.get('/',async (ctx) =>{
    // ctx.body = 'hello2225555'
    ctx.body = await render.renderToString(vm) //渲染成一个字符串
}) 

app.use(router.routes()) //使用路由系统
app.listen(5000) //监听3000端口

查看网页源码:

  <div data-server-rendered="true"><p>zf</p> <span>10</span></div>

 

可以看到是一串字符串

但我们前端网页都是有head标签的,可以把渲染好的字符串插入到一个单独的html文件中

创建template.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>
<!--vue-ssr-outlet--> 这句话表示模板占位,特别重要,一定要加,不加会报错
const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统

const Vue = require('vue')
const vm = new Vue({
    //服务端没有el
    data(){
        return {
            name: 'zf',
            age:10
        }
    },
    //渲染模板
    template:`
    <div>
        <p>{{name}}</p>
        <span>{{age}}</span>
    </div>
    `
})

const fs = require('fs')
const path = require('path')

//使用html
const template = fs.readFileSync(path.resolve(__dirname,'template.html'),'utf8') //同步读取html模板
console.log(template)
const VueServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const render = VueServerRender.createRenderer({
    template
});//创建一个渲染器  
router.get('/',async (ctx) =>{
    // ctx.body = 'hello2225555'
    ctx.body = await render.renderToString(vm) //渲染成一个字符串
}) 

app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口

渲染的结果:

 

 二、纯浏览器渲染

1.目录结构:

 2.mian.js

import Vue from 'vue'
import App from './App.vue'
let vm = new Vue({
    el:'#app',
    render:h=>h(App)

})

3.App.vue

<template>
    <div>
        <bar></bar>
        <foo></foo>
    </div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
    components:{
        Bar,
        Foo
    }
}
</script>

4.index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>浏览器渲染</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

5.bar.vue

<template>
    <div>
        bar
    </div>
</template>
<script>
export default {
    name:'Bar'
}
</script>
<style scoped>
div {
    background: red;
}
</style>

6.foo.vue

<template>
    <div>
        foo <button @click="show()">点击</button>
    </div>
</template>
<script>
export default {
    name:'Foo',
    methods:{
        show(){
            alert(1)
        }
    }
}
</script>
<style scoped>

</style>

7.webpack.config.js

//打包默认文件
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    entry:path.resolve(__dirname,'src/main.js'), //打包入口文件
    output:{
        filename:'bundle.js', //打包后的文件名
        path:path.resolve(__dirname,'./dist') //打包后的文件存放地址
    },
    module:{
        rules:[
            {
                test:/\.js/, //匹配js 使用babel-loader进行转义
                use:{
                    loader:'babel-loader',
                    options:{
                        presets:['@babel/preset-env']
                    }
                }
            },
            {
                test:/\.vue/, //匹配.vue 文件使用vue-loader进行转义
                use:'vue-loader'
            },
            {
                test:/\.css/,  //匹配css 使用style-loader进行转义
                use:['vue-style-loader','css-loader']
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
            template:path.resolve(__dirname,'public/index.html')
        }),
        new VueLoaderPlugin()
    ]
}

8.package.json

{
  "name": "vue_ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev":"webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "html-webpack-plugin": "^4.3.0",
    "koa": "^2.12.0",
    "koa-router": "^9.0.1",
    "koa-static": "^5.0.0",
    "nodemon": "^2.0.4",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.2",
    "vue-router": "^3.3.2",
    "vue-server-renderer": "^2.6.11",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.11",
    "vuex": "^3.4.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  }
}
"dev":"webpack-dev-server", 内存中运行
 "build":"webpack" 打包并输出

9.渲染结果

 

 查看网页源代码:

可以看到,浏览器渲染的结果就是html文件中只有一个div,一个js文件,默认会通过这个js文件来渲染展示页面,其他的html元素并不会出现在在网页中的,所以浏览器渲染不利于SEO优化。

三、服务端渲染

 

 

 

1、Vue SSR构建流程:

app.js入口文件

app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去做了。

 

两个entry

接下里我们来看Client entry和Server entry,这两者分别是客户端的入口和服务端的入口。Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用

 

webpack打包构建

然后我们的服务端代码和客户端代码通过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。

综上所述:服务端渲染SSR,要生成两份代码,既可以在服务端运行也可以在客户端运行,以防在SSR的过程中出现问题还可以在客户端渲染,保证用户可以看到页面。
所以就要有两个webpack配置文件,一个用于浏览器端渲染weboack.client.config.js,一个用于服务端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。同时,也要有一个server来提供http服务,我这里用的是koa
目录结构:

 

 在浏览器端渲染中,每个客户都会在他们的客户端得到一个新的应用程序,浏览器端渲染也是,我没希望每次返回给用户的是一个新的app实例,以免交叉请求造成状态污染。所以要将app.js封装成一个工厂函数,每次调用都会产生一个新的实例。

app.js

import Vue from 'vue'
import App from './App.vue'


export default function () {
    let app = new Vue({
        // el:'#app', //客户端需要 服务端不需要
        render:h=>h(App)
    })
    return {app}
} 

浏览器端的根组件,将其挂载:

client-entry.js

import createApp from './app'

const {app} = createApp() 
//客户端挂载 
app.$mount('#app')

 clinet.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>浏览器渲染</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

服务器端要返回一个新的实例,该实例可以接收一个context参数,同时每次都会返回一个新的根组件,

server-entry.js

//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'

//服务端渲染要求打包后的结果返回一个函数

export default (context) =>{
    const {app} = createApp()

    return app
}

server.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>服务端渲染</title>
</head>
<body>
     <!--vue-ssr-outlet-->
</body>
</html>

App.vue

<template>
    <div id='app'>
        <bar></bar>
        <foo></foo>
    </div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
    components:{
        Bar,
        Foo
    }
}
</script>

webpack.base.js

//打包默认文件
const path = require('path')
// const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    // entry:path.resolve(__dirname,'src/main.js'), //打包入口文件
    output:{
        filename:'[name].bundle.js', //打包后的文件名
        path:path.resolve(__dirname,'../dist') //打包后的文件存放地址
    },
    module:{
        rules:[
            {
                test:/\.js/, //匹配js 使用babel-loader进行转义
                use:{
                    loader:'babel-loader',
                    options:{
                        presets:['@babel/preset-env']
                    }
                }
            },
            {
                test:/\.vue/, //匹配.vue 文件使用vue-loader进行转义
                use:'vue-loader'
            },
            {
                test:/\.css/,  //匹配css 使用style-loader进行转义
                use:['vue-style-loader','css-loader']
            }
        ]
    },
    plugins:[
        // new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
        //     template:path.resolve(__dirname,'public/index.html')
        // }),
        new VueLoaderPlugin()
    ]
}

入口文件不是同一个了,所以要抽离出去,打包后的html文件也不是同一个了,所以,HtmlWebapckPlugin也要分离出去。

 webpack.client.js

const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = merge(base,{
    entry:{
        client:path.resolve(__dirname,'../src/client-entry.js')
    },
    plugins:[
        new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
            filename:'client.html',
            template:path.resolve(__dirname,'../public/client.html')
        }),
    ]
})

这里定义了客户端渲染的入口文件,并且规定将打包后的client.bundle.js引入到client.html文件

webpack.server.js

const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = merge(base,{
    entry:{
        server:path.resolve(__dirname,'../src/server-entry.js')
    },
    target:'node', //输出的文件是给node用的,不需要打包node自带的模块。
    output:{
        libraryTarget:'commonjs2' //表示以node 的 形式打包输出  module.exports = 导出入口函数
    },
    plugins:[
        new HtmlWebpackPlugin ({ 
            filename:'server.html',
            template:path.resolve(__dirname,'../public/server.html'), 
            excludeChunks:['server'] //表示 服务端不需要把 打包后的 server.bundle.js 引入到 html中 
        }), 
    ]
}) 

这里定义了服务端渲染的入口文件,并且规定不需要将打包后的server.bundle.js引入到server.html文件中,而是要引入client.buldle.js,同时要指定打包输出的文件是给node服务端渲染用的,output 指定打包输出的时候是以module.exports形式导出

打包结果如下:

 package.json

{
  "name": "vue_ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "client:dev": "webpack-dev-server --config build/webpack.client.js",
    "client:build": "webpack --config build/webpack.client.js",
    "server:build":"webpack --config build/webpack.server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "html-webpack-plugin": "^4.3.0",
    "koa": "^2.12.0",
    "koa-router": "^9.0.1",
    "koa-static": "^5.0.0",
    "nodemon": "^2.0.4",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.2",
    "vue-router": "^3.3.2",
    "vue-server-renderer": "^2.6.11",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.11",
    "vuex": "^3.4.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^4.2.2"
  }
}

打包命令:

"client:dev": "webpack-dev-server --config build/webpack.client.js",

"client:build": "webpack --config build/webpack.client.js",

"server:build":"webpack --config build/webpack.server.js"

服务端渲染相关代码

const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')
const ServerRender = require('vue-server-renderer') //vue 的服务端渲染包

const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统

// const Vue = require('vue')
// const vm = new Vue({
//     //服务端没有el
//     data(){
//         return {
//             name: 'zf',
//             age:10
//         }
//     },
//     //渲染模板
//     template:`
//     <div>
//         <p>{{name}}</p>
//         <span>{{age}}</span>
//     </div>
//     `
// })


//使用html
const template = fs.readFileSync(path.resolve(__dirname,'dist/server.html'),'utf8') //同步读取html模板
const ServerBundle = fs.readFileSync(path.resolve(__dirname,'dist/server.bundle.js'),'utf8') //同步读取打包后的js文件

console.log(template)

let render = ServerRender.createBundleRenderer(ServerBundle,{ //将这个打包后的js文件 渲染成一个字符串
    template
});//创建一个渲染器  
router.get('/',async (ctx) =>{
    // ctx.body = 'hello2225555'
    // ctx.body  = await render.renderToString() //这样写css不生效
    ctx.body = await new Promise ((resolve,reject) =>{ //必须写成回调的方式 css才能解析成功 
        render.renderToString((err,data) =>{
            if(err){
                console.log(err)
                reject(err)
            }else {
                resolve(data)
            }
        })
    })
}) 
app.use(static(path.resolve(__dirname,'dist'))) //静态资源访问目录
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口

ServerBundle 是打包后的服务端代码,表示要将这个js文件渲染成一个字符串,插入到template模板中。

vue-server-renderer插件,有两个方法可以做渲染,一个是createRenderer,一个是createBundleRenderer。createRenderer无法接收为服务端打包出的server.bundle.js文件,所以这里只能用createBundleRenderer

使用createRenderercreateBundleRenderer返回的renderer函数包含两个方法renderToStringrenderToStream,我们这里用的是renderToString成功后直接返回一个完整的字符串,renderToStream返回的是一个Node流。renderToString支持Promise。

至此配置完成

打包:

npm run client:build

npm run server:build

启动客户端:

npm run client:dev

启动服务端:

nodemon server.js 

查看结果

浏览器端渲染:

 

 查看源代码:

 

 客户端渲染:

 

查看网页源代码:

 

data-server-rendered="true"属性表示是服务端渲染,但是这时候点击按钮并不生效,因为后端渲染出的字符串只是可以在页面展示的静态html,但是并没有js动态功能,如果要点击页面某个元素,并不会生效,所以要手动引入客户端打包后的js文件client.bundle.js ,并且要自行添加 id=“app”

App.vue

<template>
    <div id='app'>
        <bar></bar>
        <foo></foo>
    </div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
    components:{
        Bar,
        Foo
    }
}
</script>

server.html:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>浏览器渲染</title>
    </head>
    <body>
        <!--vue-ssr-outlet-->
        <!-- 后端渲染出的字符串只是可以在页面展示的静态html,但是并没有js动态功能,如果要点击页面某个元素,并不会生效,所以要手动引入这个客户端打包后的js文件 -->
        <script src="client.bundle.js"></script> 
    </body>
</html>

 此时再次点击便可生效,这个功能在官网叫客户端激活

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。参考官网链接:https://ssr.vuejs.org/zh/guide/hydration.html

 继续完善------------------------------------------------------------------------------------------

修改配置文件,让前端代码修改之后可以自动打包自动更新,并且服务端渲染自动引入客户端的打包文件,而不是手动引入,官网叫Bundle Renderer 指引,接下来我们实现一个这个功能

2、Bundle Renderer 指引

 官网链接:https://ssr.vuejs.org/zh/guide/bundle-renderer.html#%E4%BD%BF%E7%94%A8%E5%9F%BA%E6%9C%AC-ssr-%E7%9A%84%E9%97%AE%E9%A2%98

webpack.client.js

const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const VueServerRenderer = require('vue-server-renderer/client-plugin')

module.exports = merge(base,{
    entry:{
        client:path.resolve(__dirname,'../src/client-entry.js')
    },
    plugins:[
        new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
            filename:'client.html',
            template:path.resolve(__dirname,'../public/client.html')
        }),
        new VueServerRenderer() //会生成一个文件叫vue-ssr-client-manifest.json
    ]
})

webpack.server.js

const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const VueServerRenderer = require('vue-server-renderer/server-plugin')

module.exports = merge(base,{
    entry:{
        server:path.resolve(__dirname,'../src/server-entry.js')
    },
    target:'node', //输出的文件是给node用的,不需要打包node自带的模块。
    output:{
        libraryTarget:'commonjs2' //表示以node 的 形式打包输出  module.exports = 导出入口函数
    },
    plugins:[
        new VueServerRenderer(), //会生成一个文件叫vue-ssr-server-bundle.json
        new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
            filename:'server.html',
            template:path.resolve(__dirname,'../public/server.html'), 
            excludeChunks:['server'] //表示 服务端不需要把 打包后的 server.bundle.js 引入到 html中 
        }), 
        
    ]
}) 

打包后生成两个文件

 

 在服务端渲染代码中引入这个文件

server.js

const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')
const ServerRender = require('vue-server-renderer') //vue 的服务端渲染包

const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统


//使用html
const template = fs.readFileSync(path.resolve(__dirname,'dist/server.html'),'utf8') //同步读取html模板
// const ServerBundle = fs.readFileSync(path.resolve(__dirname,'dist/server.bundle.js'),'utf8') //同步读取打包后的js文件
const ServerBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')

console.log(template)

let render = ServerRender.createBundleRenderer(ServerBundle,{ //将这个打包后的js文件 渲染成一个字符串
    template,
    clientManifest //表示自动引入客户端打包结果,不需要手动引入客户端打包文件了
});//创建一个渲染器  
router.get('/',async (ctx) =>{
    // ctx.body = 'hello2225555'
    // ctx.body  = await render.renderToString() //这样写css不生效
    ctx.body = await new Promise ((resolve,reject) =>{ //必须携程回调的方式 css才能解析成功 
        render.renderToString((err,data) =>{
            if(err){
                console.log(err)
                reject(err)
            }else {
                resolve(data)
            }
        })
    })
}) 
app.use(static(path.resolve(__dirname,'dist'))) //静态资源访问目录
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口

在服务端渲染的时候会自动引入打包后的客户端文件,而不用再手动引入

 路由配置:

新建router.js:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

//一函数的形式导出 防止函数被共用
export default () =>{
    return new VueRouter({
        mode:'history',
        routes:[
            {
                path:'/',
                component:() => import('./components/foo.vue')
            },
            {
                path:'/bar',
                component:() => import('./components/bar.vue')
            }
        ]
    })
}

路由实例要以函数的形式导出,这样每次请求都可以返回一个新的实例,防止被共用。

修改app.js文件:

import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'

export default function () {
    let router = createRouter() //每次调用创建一个新的router
    let app = new Vue({
        router, //前端渲染直接注入即可
        // el:'#app', //客户端需要 服务端不需要
        render:h=>h(App)
    })
    return {app,router} //服务端渲染返回
} 

每次渲染都会创建一个新的router实例,前端渲染直接注入即可,后端渲染的时候返回这个router实例

服务端渲染代码server.js

//当访问其他路径的时候 需要把路径回传到打包入口文件server-entry.js,让它跳转到那个页面再返回整个实例
router.get('*',async (ctx) =>{
    try{
        ctx.body = await new Promise ((resolve,reject) =>{
            render.renderToString({url:ctx.path},(err,data) =>{
                if(err){
                    console.log(err)
                    reject(err)
                }else {
                    resolve(data)
                }
            })
        })
    }catch(e){
        console.log(e)
        ctx.body = 'page not found' 
    }
})

我们的服务器代码使用了一个 * 处理程序,它接受任意 URL。这允许我们将访问的 URL 传递到我们的 Vue 应用程序中,然后对客户端和服务器复用相同的路由配置!

也就是说,当访问除了 ‘/’ 以外的其他路径时,比如:loacolhost:5000/bar , 首先拿到这个路径 /bar,然后将这个路径回传到server-entry.js文件,对应代码 {url:ctx.path},在这里进行跳转,然后返回跳转后的整个实例。

修改server-entry.js:

//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'

//服务端渲染要求打包后的结果返回一个函数

export default (context) =>{
    // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
    return new Promise((resolve,reject) => {
        const {app,router} = createApp()

        router.push(context.url) 
router.onReady(()
=> { //获取匹配路由 const matchedComponents = router.getMatchedComponents() // 匹配不到的路由,执行 reject 函数,并返回 404 if(!matchedComponents.length){ return reject({code:404}) } // Promise 应该 resolve 应用程序实例,以便它可以渲染 resolve(app) },reject) }) }

router.push表示路由的跳转,如果匹配不到的路由,执行 reject 函数,并返回 404,然后返回整个app进行渲染

四、Vuex+Vue-router-SSR

在上面的例子中我们都是同步渲染,那如果SSR需要获取一些异步数据该怎么处理?

服务端渲染和浏览器端渲染经过的生命周期不同,在服务器端,只会经历beforeCreatecreated两个生命周期,因为服务器端渲染是在后端拼接好了字符串直接输出,不会有dom结构的渲染,就不会有beforeMountmounted。

我们先来想一下,在纯浏览器渲染的Vue项目中,我们是怎么获取异步数据并渲染到组件中的?一般是在created或者mounted生命周期里发起异步请求,然后在成功回调里执行this.data = xxxVue监听到数据发生改变,走后面的Dom Diff,打patch,做DOM更新。

那么服务端渲染可不可以也这么做呢?答案是不行的。

  1. mounted里肯定不行,因为SSR都没有mounted生命周期,所以在这里肯定不行。
  2. beforeCreate里发起异步请求是否可以呢,也是不行的。因为请求是异步的,可能还没有等接口返回,服务端就已经把html字符串拼接出来了。

所以,参考一下官方文档,我们可以得到以下思路:

  1. 在渲染前,要预先获取所有需要的异步数据,然后存到Vuexstore中。
  2. 在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。
  3. store中的数据设置到window.__INITIAL_STATE__属性中。
  4. 在浏览器环境中,通过Vuexwindow.__INITIAL_STATE__里面的数据注入到相应组件中。

正常情况下,通过这几个步骤,服务端吐出来的html字符串相应组件的数据都是最新的,所以第4步并不会引起DOM更新,但如果出了某些问题,吐出来的html字符串没有相应数据,Vue也可以在浏览器端通过`Vuex注入数据,进行DOM更新。

1.创建store.js

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

export default () => {
    let store = new Vuex.Store({
        state:{
            name:''
        },
        mutations:{
            changeName(state){
                state.name = 'leah'
            }
        },
        actions:{
            changeName({commit}){ //模拟数据请求
                return new Promise ((resolve,reject)=>{
                    setTimeout(() => {
                        commit('changeName')
                        resolve()
                    },3000)
                })
            }
        }
    })
    //表示浏览器渲染才会走这个逻辑
    if(typeof window !== 'undefined'){ // 服务端环境没有window属性,只有前端渲染才有window属性
        if(window.__INITIAL_STATE){
            store.replaceState(window.__INITIAL_STATE) //用服务端渲染结果替换当前state
        }
    }
    return store
}

在actions中模拟数据请求,最重要的是对当前执行环境的判断,因为服务端渲染会默认将获取的结果保存在 window.__INITIAL_STATE这个属性上,这个时候就要对store中的状态做一个替换,保证统一。

2.app.js

import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
import createStore from './store'

export default function () {
    let router = createRouter() //每次调用创建一个新的router
    let app = new Vue({
        store,
        router, //前端渲染直接注入即可
        // el:'#app', //客户端需要 服务端不需要
        render:h=>h(App)
    })
    return {app,router,store} //服务端渲染返回
} 

同样的前端渲染直接注入store即可,服务端渲染需要导出store

3.bar.vue

<template>
    <div>
        bar {{$store.state.name}}
    </div>
</template>
<script>
export default {
    name:'Bar',
    //服务端渲染没有这个钩子
    mounted(){
        this.$store.dispatch('changeName')
    },
    //规定这个方法只可以在服务端渲染中使用,并且只能在页面级组件中使用
    asyncData(){ //这个请求是在服务端发的
        return store.dispatch('changeName') //返回一个promise
    }
}
</script>
<style scoped>
div {
    background: red;
}
</style>

如果是前端渲染可以通过mounted生命周期拿到异步数据,如果是服务端渲染就需要走asyncData()这个方法,他会返回一个promise,

4.server-entry.js

//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'

//服务端渲染要求打包后的结果返回一个函数

export default (context) =>{
    // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
    return new Promise((resolve,reject) => {
        const {app,router} = createApp()

        router.push(context.url) 
         router.onReady(() => {
             //获取匹配路由
             const matchedComponents = router.getMatchedComponents()
             // 匹配不到的路由,执行 reject 函数,并返回 404
             if(!matchedComponents.length){
                 return reject({code:404})
             }

             Promise.all(matchedComponents.map(comp => { 
                return comp.asyncData && comp.asyncData(store) //返回页面级组件
            })).then(()=>{
                context.state = store.state //将服务端调用Vuex渲染的结果放到当前的上下文中
                // Promise 应该 resolve 应用程序实例,以便它可以渲染
                resolve(app)
            },err =>{
                reject(err)
            }) 
             
             
         },reject)
    })
}

在所有的组件中找到具有asyncData方法的组件并返回,并且将服务端调用Vuex的结果保存在上下文中,context.state = store.state作用是,当使用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。

至此,Vue SSR就完结了

好文推荐:https://segmentfault.com/a/1190000016637877

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2020-06-09 18:19  leahtao  阅读(129)  评论(0编辑  收藏  举报