服务端渲染的基本模型

const http = require('http')
let str = "hello, SSR"
http.createServer(function(req, res){
    req.write('<h1>' + str + '</h1>');
    req.end();
}).listen(8090)

所谓服务端渲染, 其实一直都有, java, python, php 都有渲染模板来做服务端渲染, 简单来讲就是前发起请求, 服务器去数据库请求数据, 服务器拿到数据以后, 并不是直接返回, 而是根据相应的设计, 将数据拼接成一个渲染模板, 返回给前端, 前端直接渲染或经过简单处理之后渲染.

Vue 服务端渲染基本模板

<!--template.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>Title</title>
</head>
<body>
    <p>title</p>
    <!--vue-ssr-outlet-->
</body>
</html>

里面的那一行注释就是告诉 serverRenderer 将模板插入到这里

// server.js
const http = require("http")
const Vue = require('vue')
const serverRender = require('vue-server-renderer')
const app = new Vue({
    template: `<div>{{ title }}</div>`,
    data: {
        title: 'server side render'
    }
})
const render = serverRender.createRenderer({
    template: require('fs').readFileSync('./template.html', 'utf8')
})
let server = http.createServer(function (req, res) {
    render.renderToString(app, {
        // src 可以使用三括号插值语法将其放入渲染模板中 {{{ src }}}
        // 如果使用双括号, 将会被作为文本直接放在 document 中
        src: '<script>console.log("src")</script>',
        init: ''
        
    },(err, html) => {
        if(!err) {
            res.end(html)
        }
    })

})

server.listen(8090, function () {
    console.log('server is runing at 8090');
})

更为具体的实例肯定不会如此简陋,因为这也做不了什么事.
下面会有一个 express + vue + webpack 按照 vue 文档上的 ssr 写的实例.

先放个大招:


ssr.png

按照这个流程
我们看看目录结构:


目录.png

首先肯定需要使用到webpack, 编写webpack配置文件如下:

const path = require('path') // 方便使用路径
const root = path.resolve(__dirname, '..'); // root 就是根路径 ssr 了
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
    mode: 'development',
    entry: path.join(root, 'entry/entry.server.js'), // ssr/entry/entry-server.js
    output: {
        libraryTarget: "commonjs2", // 将导出的文件放到 module.exports 上
        path: path.join(root, 'dist'),   // 输出到 ssr/dist
        filename: 'server.bundle.js'     // 定义输出文件名
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                // use: ['vue-loader']
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/ // 忽略 node_modules 中的 js
            }
        ]
    },
    plugins: [new VueLoaderPlugin()]
}

配置好webpack, 就开始写代码了
肯定是需要一个开发路径src的, 在里面写vue的组件等等, 另外,main.js也在这里定义.

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

export default function () {
    return new Vue({
        template:  `<App />`,
        components: {
            App
        }
    })
}

它的主要作用就是生成一个最外层的vue实例, 作为服务端渲染vue的入口.
然后看看 App.vue

// App.vue
<template>
    <div id="app">
        <h1>Server Side Render</h1>
        <test />
    </div>
</template>

<script>
    import test from './components/test.vue'
    export default {
        name: "App",
        components: {
            test
        }
    }
</script>

<style scoped>

</style>

App.vue里, 引入了一个组件test.这意味着和在客户端使用vue没什么区别.
但是其实是有区别的. 不如它会失去vue应有的特性. 这个后面会说.

// test.vue  注意, 这里在浏览器渲染之后, 理想中的双向数据绑定不会生效
<template>
    <div class="test">
        <input type="text" v-model="val">
        <p>{{ val }}</p>
    </div>
</template>

<script>
    export default {
        name: "test",
        data () {
            return {
                val: 'hello, ssr'
            }
        }
    }
</script>
<style scoped>
</style>

后面就应该是服务端的入口登场了, entry.server.js 是服务端的入口

// entry.server.js
import createApp from '../src/main.js'

export default function () {
    return createApp()
}

最后就是在server.js里面, 使用它, 为服务端渲染创建一个vue实例, 再使用服务端的渲染插件vue-server-renderer, 将我们创建的vue实例生成字符串, 返回给浏览器, 由浏览器去将其渲染出来.

// server.js
const express = require('express')
const serverRender = require('vue-server-renderer')
const createApp = require('./dist/server.bundle.js')['default']  // default 才能拿到生成实例的函数
console.log(createApp);  // [Function]

const render = serverRender.createRenderer({
    template: require('fs').readFileSync('./template.html', 'utf8')  // fs 读取 html 模板
})

const server = express()

server.get('*', (req, res) => {
    let app = createApp()
    render.renderToString(app, (err, html) => {
       res.end(html)   // 渲染成字符串返回给浏览器
    })
})

server.listen(8090, function () {
    console.log('server is runing at 8090');
})

使用 webpack 打包, 使用node执行server.js, 在浏览器打开localhost:8090, 看到如下画面:

test.png

到目前为止, 大招图的上半部分已经通了:

ssr_half.png

此时, vue的特性依然没有. 只是一个死的不能再死的页面.
接下来要做的事情, 就是要让 vue活过来.那么这就是客户端的事情了
entry目录中, 添加一个 entry.client.js
import createApp from '../src/main.js'
let app = createApp()
// 页面构建完成, 将 app 挂载
window.onload = function () {
    app.$mount('#app')
}

当然, 也要写一个webpack客户端的配置文件, 基本跟服务端的一样, 只是名字不同.

// webpack.client.js
const path = require('path') // 方便使用路径
const root = path.resolve(__dirname, '..'); // root 就是根路径 ssr 了
const VueLoaderPlugin = require('vue-loader/lib/plugin')
entry = path.join(root, 'dist');
console.log(entry);


module.exports = {
    mode: 'development',
    entry: path.join(root, 'entry/entry.client.js'), // ssr/entry/entry-server.js
    output: {
        path: path.join(root, 'dist'),   // 输出到 ssr/dist
        filename: 'client.bundle.js'     // 定义输出文件名
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                // use: ['vue-loader']
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/ // 忽略 node_modules 中的 js
            }
        ]
    },
    plugins: [new VueLoaderPlugin()]
}

到了这里, 我们来想一下, 为什么浏览器页面的vue是死的? 以为没有vue.js文件, 2333....
这个时候, 三括号插值语法就派上用场了.
先在template.html里面, 添加一行: {{{ load }}}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>Title</title>
</head>
<body>
    <p>title</p>
    <!--vue-ssr-outlet-->
    {{{ load }}}
</body>
</html>

意思是去加载一个js文件, 注意, 双花括号不会去加载, 直接被当成字符串了.
然后 server.js里面

const express = require('express')
const path = require('path')
const serverRender = require('vue-server-renderer')
const createApp = require('./dist/server.bundle.js')['default']
console.log(createApp);

const render = serverRender.createRenderer({
    template: require('fs').readFileSync('./template.html', 'utf8')
})

const server = express()

console.log(path.resolve('/client.bundle.js'));
// 客户端请求这个文件, 将这个文件返回, 路径不一样, 可以偷偷的给它改了嘛
// 请求文件写绝对路径, 返回文件随意
server.get('/client.bundle.js', (req, res) => {
    res.sendFile(path.resolve('./dist/client.bundle.js'))
})
server.get('*', (req, res) => {
    let app = createApp()
    render.renderToString(app, {
        //  绝对路径
        load: '<script src="/client.bundle.js"></script>'
    }, (err, html) => {
       res.end(html)
    })
})

server.listen(8090, function () {
    console.log('server is runing at 8090');
})

然后打包运行, 就会看到:


err.png

为啥?
这是因为, 在导入的时候 import Vue from 'vue' 使用的其实是vue/dist/vue.common.js, 所以会少一些东西, 将其改为vue/dist/vue.js就好了.这怎么改呢.
main.js中, 修改.
然后:

r64.png

可算是活过来了.
但是这么修改, 意味着在每一个地方都需要这么修改.
webpack里面修改一下, 两个文件中分别添加一个对象.
resolve: {
        alias: {
            'vue': 'vue/dist/vue.js'
        }
    }

这样基本的服务端渲染就算是完成了.

使用 vue-router

像正常使用vue-router一样, 写好 router 配置文件, 注入App组件的配置对象. 然后router-link, router-view 一气呵成.

image.png

使用 vuex

也和往常使用一样, 使用store.

image.png