• 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

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    不积跬步无以至千里
  • 相关阅读:
    合理配置SQL Server的最大内存
    理解内存----优化SQL Server内存配置
    Systems Performance: Enterprise and the Cloud 读书笔记系列
    google perftools分析程序性能
    源代码分析-构建调用图
    Intel VTune性能分析器基础
    代微机原理与接口技术(第3版)课程配套实验包和电子课件
    【MySQL性能优化】MySQL常见SQL错误用法
    Linux 内核分析 -- Linux内核学习总结
    《Linux就该这么学》 软件库
  • 原文地址:https://www.cnblogs.com/lyt0207/p/13061797.html
Copyright © 2020-2023  润新知