Vue SSR 服务端渲染

服务端渲染的基本模型

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容