美文网首页
vue vue-server-renderer实现SSR(服务端

vue vue-server-renderer实现SSR(服务端

作者: w_wx_x | 来源:发表于2019-05-07 10:38 被阅读0次

    参考文章:
    简书:https://www.jianshu.com/p/c6a07755b08d
    掘金:https://github.com/tiodot/vnews
    文章中叙述的代码地址:https://github.com/ShuangWW/vue-server-renderer

    项目一

    1.项目搭建

    mkdir ssr1                                       // 新建文件夹ssr1
    cd ssr1                                          // 进入文件夹ssr1
    cnpm init                                       // 初始化
    cnpm i vue vue-server-renderer --save           // 安装插件vue、vue-server-renderer
    cnpm i express --save                           // 安装插件express
    

    2.在根目录下创建server.js文件

    const Vue = require('vue')
    const express = require('express')()
    const renderer = require('vue-server-renderer').createRenderer()
    
    
    const app = new Vue({
        template:'<div>hello world</div>'
    })
    
    express.get('/',(req,res)=>{
        renderer.renderToString(app,(err,html)=>{
            if(err){
                return res.state(500).end('运行时错误')
            }
            res.send(`
                <!DOCTYPE html>
                <html lang="en">
                    <head>
                        <meta charset="UTF-8">
                        <title>Vue2.0 SSR渲染页面</title>
                    </head>
                    <body>
                        ${html}
                    </body>
                </html>        
            `)
        })
    })
    
    express.listen(8080,()=>{
        console.log('服务器已经启动!')
    })
    

    3.输入node server.js启动服务,在浏览器输入http://localhost:8080访问

    页面.png
    控制台.png
    项目目录结构.png

    项目二

    1.项目搭建,并创建如下目录结构

    mkdir ssr2                                       // 新建文件夹ssr2
    cd ssr2                                          // 进入文件夹ssr2
    cnpm init                                       // 初始化
    cnpm i vue vue-server-renderer --save           // 安装插件vue、vue-server-renderer
    cnpm i express --save                           // 安装插件express
    
    项目目录.png

    2.将package.json文件修改如下配置,然后cnpm i安装相关需要的依赖包

    {
      "name": "ssr2",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "server": "webpack --config ./webpack/webpack.server.js",
        "client": "webpack --config ./webpack/webpack.client.js"
      },
      "author": "",
      "license": "ISC",
      "dependencies": {
        "axios": "^0.16.0",
        "babel": "^6.23.0",
        "babel-plugin-transform-runtime": "^6.23.0",
        "babel-polyfill": "^6.26.0",
        "babel-preset-env": "^1.7.0",
        "body-parser": "^1.18.3",
        "compression": "^1.7.2",
        "express": "^4.15.4",
        "express-http-proxy": "^1.2.0",
        "gulp": "^3.9.1",
        "gulp-shell": "^0.6.5",
        "http-proxy-middleware": "^0.18.0",
        "less": "^3.0.4",
        "less-loader": "^4.1.0",
        "shell": "^0.5.0",
        "superagent": "^3.8.3",
        "vue": "^2.2.2",
        "vue-meta": "^1.5.0",
        "vue-router": "^2.2.0",
        "vue-server-renderer": "^2.2.2",
        "vue-ssr-webpack-plugin": "^3.0.0",
        "vuex": "^2.2.1",
        "vuex-router-sync": "^4.2.0"
      },
      "devDependencies": {
        "babel-core": "^6.26.3",
        "babel-loader": "^6.4.1",
        "babel-preset-es2015": "^6.24.1",
        "css-loader": "^0.28.4",
        "style-loader": "^0.18.2",
        "vue-loader": "^11.1.4",
        "vue-template-compiler": "^2.2.4",
        "webpack": "^2.7.0"
      }
    }
    

    3.src文件夹

    // App.vue
    
    <template>
        <div>
            <h2>欢迎来到SSR渲染页面</h2>
            <router-view></router-view>
        </div>
    </template>
    
    <script>
    export default {
    }
    </script>
    
    <style>
    </style>
    
    // route.js
    
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    export default function createRouter() {
        let router = new VueRouter({
            // 要记得增加mode属性,因为#后面的内容不会发送至服务器,服务器不知道请求的是哪一个路由
            mode: 'history',
            routes: [
                {
                    alias: '/',
                    path: '/home',
                    component: require('./views/home.vue')
                },
                {
                    path: '/animal',
                    component: require('./views/animal.vue')
                },
                {
                    path: '/people',
                    component: require('./views/people.vue')
                }
            ]
        })
    
        return router
    }
    
    // main.js
    
    import Vue from 'vue'
    import createRouter from './route.js'
    import App from './App.vue'
    
    // 导出一个工厂函数,用于创建新的vue实例(可以隔离开各个客户端的请求,每次客户端的请求,都会创建一个新的vue实例)
    export function createApp() {
        const router = createRouter()
        const app = new Vue({
            router,
            render: h => h(App)
        })
    
        return app
    }
    
    // home.vue
    
    <template>
        <div>
            home
        </div>
    </template>
    
    <script>
    export default {
    }
    </script>
    
    <style scoped>
    </style>
    
    // animal.vue
    
    <template>
        <div>
            animal
        </div>
    </template>
    
    <script>
    export default {
    }
    </script>
    
    <style scoped>
    </style>
    
    // people.vue
    
    <template>
        <div>
            people
        </div>
    </template>
    
    <script>
    export default {
    }
    </script>
    
    <style scoped>
    </style>
    

    4..babelrc文件夹用于配置babel

    {
          "presets": [
                "babel-preset-env"
          ],
          "plugins": [
                "transform-runtime"
          ]
    }
    

    5.server.js文件

    /* server.js */
    const express = require('express')()
    const renderer = require('vue-server-renderer').createRenderer()
    const createApp = require('./dist/bundle.server.js')['default']
    
    // 响应路由请求
    express.get('*', (req, res) => {
        const context = { url: req.url }
    
        // 创建vue实例,传入请求路由信息
        createApp(context).then(app => {
            renderer.renderToString(app, (err, html) => {
                if (err) { return res.state(500).end('运行时错误') }
                res.send(`
                    <!DOCTYPE html>
                    <html lang="en">
                        <head>
                            <meta charset="UTF-8">
                            <title>Vue2.0 SSR渲染页面</title>
                        </head>
                        <body>
                            ${html}
                        </body>
                    </html>
                `)
            })
        }, err => {
            if(err.code === 404) { res.status(404).end('所请求的页面不存在') }
        })
    })
    
    
    // 服务器监听地址
    express.listen(8080, () => {
        console.log('服务器已启动!')
    })
    

    6.entry-server.js

    import { createApp } from '../src/main'
    
    export default context => {
        return new Promise((resolve, reject) => {
            const app = createApp()
    
            // 更改路由
            app.$router.push(context.url)
    
            // 获取相应路由下的组件
            const matchedComponents = app.$router.getMatchedComponents()
    
            // 如果没有组件,说明该路由不存在,报错404
            if (!matchedComponents.length) { return reject({ code: 404 }) }
    
            resolve(app)
        })
    
    }
    

    7.webpack.server.js

    const path = require('path');
    const projectRoot = path.resolve(__dirname, '..');
    
    
    module.exports = {
        // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
        target: 'node',
        entry: ['babel-polyfill', path.join(projectRoot, 'entry/entry-server.js')],
        output: {
            libraryTarget: 'commonjs2',
            path: path.join(projectRoot, 'dist'),
            filename: 'bundle.server.js',
        },
        module: {
            rules: [{
                    test: /\.vue$/,
                    loader: 'vue-loader',
                },
                {
                    test: /\.js$/,
                    loader: 'babel-loader',
                    include: projectRoot,
                    exclude: /node_modules/,
                    options: {
                        presets: ['es2015']
                    }
                },
                {
                    test: /\.less$/,
                    loader: "style-loader!css-loader!less-loader"
                }
            ]
        },
        plugins: [],
        resolve: {
            alias: {
                'vue$': 'vue/dist/vue.runtime.esm.js'
            }
        }
    }
    

    8.打包文件并开启服务器(如下命令),浏览器输入localhost:8080

    cnpm run server
    node server
    
    image.png image.png

    在此基础上,增加客户端渲染部分
    新建 entry>entry-client.js(客户端入口文件)
    新建 webpack>webpack.client.js(客户端配置文件)

    /* entry-client.js */
    import { createApp } from '../src/main'
    
    const app = createApp()
    
    // 绑定app根元素
    window.onload = function() {
           app.$mount('#app')
    }
    
    /* webpack.client.js */
    const path = require('path');
    const projectRoot = path.resolve(__dirname, '..');
    
    module.exports = {
        entry: ['babel-polyfill', path.join(projectRoot, 'entry/entry-client.js')],
        output: {
            path: path.join(projectRoot, 'dist'),
            filename: 'bundle.client.js',
        },
        module: {
            rules: [{
                    test: /\.vue$/,
                    loader: 'vue-loader'
                },
                {
                    test: /\.js$/,
                    loader: 'babel-loader',
                    include: projectRoot,
                    exclude: /node_modules/,
                    options: {
                        presets: ['es2015']
                    }
                }
            ]
        },
        plugins: [],
        resolve: {
            alias: {
                'vue$': 'vue/dist/vue.runtime.esm.js'
            }
        }
    };
    

    更改server.js文件

    const exp = require('express')
    const express = exp()
    const renderer = require('vue-server-renderer').createRenderer()
    const createApp = require('./dist/bundle.server.js')['default']
    
    // 设置静态文件目录
    express.use('/', exp.static(__dirname + '/dist'))
    
    const clientBundleFileUrl = '/bundle.client.js'
    
    // 响应路由请求
    express.get('*', (req, res) => {
        const context = { url: req.url }
    
        // 创建vue实例,传入请求路由信息(head里面添加脚本,用于引入单页面应用)
        createApp(context).then(app => {
            renderer.renderToString(app, (err, html) => {
                if (err) { return res.state(500).end('运行时错误') }
                res.send(`
                    <!DOCTYPE html>
                    <html lang="en">
                        <head>
                            <meta charset="UTF-8">
                            <title>Vue2.0 SSR渲染页面</title>
                            <script src="${clientBundleFileUrl}"></script>
                        </head>
                        <body>
                            <div id="app">${html}</div>
                        </body>
                    </html>
                `)
            })
        }, err => {
            if(err.code === 404) { res.status(404).end('所请求的页面不存在') }
        })
    })
    
    // 服务器监听地址
    express.listen(8080, () => {
        console.log('服务器已启动!')
    })
    

    更改app.vue,添加路由

    <template>
        <div>
            <h2>欢迎来到SSR渲染页面</h2>
            <router-link to="/home">home</router-link>
            <router-link to="/animal">animal</router-link>
            <router-link to="/people">people</router-link>
            <router-view></router-view>
        </div>
    </template>
    
    <script>
    export default {
    }
    </script>
    
    <style>
    </style>
    

    打包开启服务,并在浏览器输入地址localhost:8080

    cnpm run server
    cnpm run client
    node server
    
    image.png

    当点击切换路由时,浏览器并未再次请求服务器,一切都在本地完成

    项目三

    基于项目二的基础上进行添加动态从服务器获取数据

    1.修改entry.server.js文件。
    增加一个Promise.all函数,遍历请求下的组件,看是否含有serverRequest函数来判断是否需要服务端请求数据,若需要则执行此函数并传入一个store参数

    // entry.server.js
    import { createApp } from '../src/main'
    
    export default context => {
        return new Promise((resolve, reject) => {
            const app = createApp()
    
            // 更改路由
            app.$router.push(context.url)
    
            // 获取相应路由下的组件
            const matchedComponents = app.$router.getMatchedComponents()
    
            // 如果没有组件,说明该路由不存在,报错404
            if (!matchedComponents.length) { return reject({ code: 404 }) }
    
            // 遍历路由下所以的组件,如果有需要服务端渲染的请求,则进行请求
            Promise.all(matchedComponents.map(component => {
                if (component.serverRequest) {
                    return component.serverRequest(app.$store)
                }
            })).then(() => {
                resolve(app)
            }).catch(reject)
        })
    
    }
    

    2.创建store.js文件,通过axios来发起请求

    import Vue from 'vue'
    import Vuex from 'vuex'
    import axios from 'axios'
    
    Vue.use(Vuex)
    
    export default function createStore() {
          let store =  new Vuex.Store({
                state: {
                      homeInfo: ''
                },
                actions: {
                      getHomeInfo({ commit }) {
                            return axios.get('http://localhost:8080/api/getHomeInfo').then((res) => {
                                  commit('setHomeInfo', res.data)
                            })
                      }
                },
                mutations: {
                      setHomeInfo(state, res) {
                            state.homeInfo = res
                      }
                }
          })
    
          return store
    }
    

    3.修改main.js文件,将store引入vue实例

    import Vue from 'vue'
    import createRouter from './route.js'
    import App from './App.vue'
    import createStore from './store'
    
    // 导出一个工厂函数,用于创建新的vue实例
    export function createApp() {
        const router = createRouter()
        const store = createStore()
        const app = new Vue({
            router,
            store,
            render: h => h(App)
        })
    
        return app
    }
    

    4.修改server.js,增加请求接口

    const exp = require('express')
    const express = exp()
    const renderer = require('vue-server-renderer').createRenderer()
    const createApp = require('./dist/bundle.server.js')['default']
    
    // 设置静态文件目录
    express.use('/', exp.static(__dirname + '/dist'))
    
    // 客户端打包地址
    const clientBundleFileUrl = '/bundle.client.js'
    
    // getHomeInfo请求
    express.get('/api/getHomeInfo', (req, res) => {
        res.send('SSR发送请求')
    })
    
    // 响应路由请求
    express.get('*', (req, res) => {
        const context = { url: req.url }
    
        // 创建vue实例,传入请求路由信息
        createApp(context).then(app => {
            renderer.renderToString(app, (err, html) => {
                if (err) { return res.state(500).end('运行时错误') }
                res.send(`
                    <!DOCTYPE html>
                    <html lang="en">
                        <head>
                            <meta charset="UTF-8">
                            <title>Vue2.0 SSR渲染页面</title>
                            <script src="${clientBundleFileUrl}"></script>
                        </head>
                        <body>
                            <div id="app">${html}</div>
                        </body>
                    </html>
                `)
            })
        }, err => {
            if(err.code === 404) { res.status(404).end('所请求的页面不存在') }
        })
    })
    
    // 服务器监听地址
    express.listen(8080, () => {
        console.log('服务器已启动!')
    })
    

    5.修改home.vue文件

    <!-- home.vue -->
    <template>
      <div>
        home
        <div>{{ homeInfo }}</div>
    </div>
    </template>
    
    <script>
        export default {
            serverRequest(store) {
                return store.dispatch('getHomeInfo')
            },
            computed: {
                homeInfo() {
                  return this.$store.state.homeInfo
              }
          }
      }
    </script>
    
    <style scoped>
    
    </style>
    

    6.编译并运行(localhost:8080)

    cnpm run server
    cnpm run client
    node server
    
    运行结果.png
    请求结果.png

    服务端和客户端是两个vue实例各自进行自己的渲染,然后拼接在一起的。
    通过serverRequest发出的请求只有在服务端的vue实例可以拿到这个store数据
    客户端的vue实例是拿不到的

    请求返回的html文件中,是有'SSR发送请求的'字样的,但是页面为什么不显示?
    这是因为客户端的vue实例脚本加载成功后,homeInfo被客户端的homeInfo属性覆盖,而客户端的homeInfo是没有值的,是个空属性,因此不显示

    如何解决上述问题呢?

    1.修改server.js
    新加一个script标签,创建一个全局对象,值是state的值,将服务器端请求得出的结果传给客户端
    <script>window.INITIAL_STATE = ${state}</script>

    // server.js
    const exp = require('express')
    const express = exp()
    const renderer = require('vue-server-renderer').createRenderer()
    const createApp = require('./dist/bundle.server.js')['default']
    
    // 设置静态文件目录
    express.use('/', exp.static(__dirname + '/dist'))
    
    const clientBundleFileUrl = '/bundle.client.js'
    
    // getHomeInfo请求
    express.get('/api/getHomeInfo',(req,res)=>{
        res.send('SSR发送请求')
    })
    
    // 响应路由请求
    express.get('*', (req, res) => {
        const context = { url: req.url }
    
        /* 创建vue实例,传入请求路由信息
                新加一个script标签,创建一个全局对象,值是state的值,将服务器端请求得出的结果传给客户端
                head里面添加脚本,用于引入单页面应用,<script src="${clientBundleFileUrl}"></script>
        */
        createApp(context).then(app => {
            let state = JSON.stringify(context.state)
    
            renderer.renderToString(app, (err, html) => {
                if (err) { return res.state(500).end('运行时错误') }
                res.send(`
                    <!DOCTYPE html>
                    <html lang="en">
                        <head>
                            <meta charset="UTF-8">
                            <title>Vue2.0 SSR渲染页面</title>
                            <script>window.__INITIAL_STATE__ = ${state}</script>
                            <script src="${clientBundleFileUrl}"></script>
                        </head>
                        <body>
                            <div id="app">${html}</div>
                        </body>
                    </html>
                `)
            })
        }, err => {
            if(err.code === 404) { res.status(404).end('所请求的页面不存在') }
        })
    })
    
    // 服务器监听地址
    express.listen(8080, () => {
        console.log('服务器已启动!')
    })
    

    2.修改entry-client.js文件

    /* entry-client.js */
    import { createApp } from '../src/main'
    
    const app = createApp()
    
    // 同步服务端信息
    if(window.__INITIAL_STATE__){
        app.$store.replaceState(window.__INITIAL_STATE__)
    }
    
    // 绑定app根元素
    window.onload = function() {
           app.$mount('#app')
    }
    

    3.编译并运行

    image.png
    综上,项目三已经实现了完整的服务端渲染项目

    项目四

    类似于官方提供的demo,只不过内容源换成了掘金

    相关文章

      网友评论

          本文标题:vue vue-server-renderer实现SSR(服务端

          本文链接:https://www.haomeiwen.com/subject/hiacoqtx.html