0、写在后面

vue的文档曾经写的很好了,然而官网文档的例子波及webpack的打包,使得咱们对vue-ssr的实现细节了解起来变得艰难,因而小编依据本人学习的教训,总结了这篇对于vue-ssr的文章,和官网文档互为补充。文章次要波及三个方面:

  • 实现服务端渲染
  • 不应用webpack实现残缺的服务端渲染栗子,了解vue-ssr的细节;
  • 应用webpack打包,利用于理论生产;

特地阐明:
(1) 为了抓住内容的骨干,代码中的例子没有退出错误处理。
(2) 安装包和环境阐明。

"node":"12.13.1","express": "^4.17.1","vue": "^2.6.12","vue-router": "^3.5.1","vue-server-renderer": "^2.6.12"

1、为什么应用服务器端渲染 (SSR)?

  • 更好的 SEO,因为搜索引擎爬虫抓取工具能够间接查看齐全渲染的页面;
  • 快的内容达到工夫 (time-to-content),特地是对于迟缓的网络状况或运行迟缓的设施;

2、将vue实例转化为HTML字符串

源码:demo01

// server.js// 第 1 步:创立一个 Vue 实例const Vue = require('vue');const app = new Vue({    template:`<div>hello world</div>`});// 第 2 步:创立一个 rendererconst renderer = require('vue-server-renderer').createRenderer();// 第 3 步:将 Vue 实例渲染为 HTMLconst html = await renderer.renderToString(app);// html后果为字符串:<div data-server-rendered="true">hello world</div>

3、应用express搭建node服务

3.1 配合express

源码:demo02

const Vue = require('vue');const server = require('express')();const renderer = require('vue-server-renderer').createRenderer();server.get('*', async (req, res) => {    const app = new Vue({        template:`<div>hello world</div>`    });    const html = await renderer.renderToString(app);    res.end(`        <!DOCTYPE html>        <html lang="en">            <head><title>Hello</title></head>            <body>${html}</body>        </html>    `)});const port = 3000;server.listen(port, () => console.log(`http://127.0.0.1:${port}`));

运行node server.js, 拜访http://127.0.0.1:3000, 就能够失去拜访后果。

3.2 应用模板

源码:demo02

// server.jsconst Vue = require('vue');const fs = require('fs');const server = require('express')();// 读取模板const template = fs.readFileSync('./template.html', 'utf-8');const { createRenderer } = require('vue-server-renderer');// 生成带有模板的渲染器const renderer = createRenderer({ template });server.get('*', async (req, res) => {    const app = new Vue({        template:`<div>hello world</div>`    });    const html = await renderer.renderToString(app);    res.end(html);});const port = 3000;server.listen(port, () => console.log(`http://127.0.0.1:${port}`));
// template.html<!DOCTYPE html><html lang="en">    <head>        <title>Hello</title>    </head>    <body>        <!--vue-ssr-outlet-->    </body></html>

这个栗子的成果和3.1一样,只是应用了带有template模板的渲染器。

3.3 传递context

源码:demo04

// temelate.html<html>  <head>    <!-- 应用双花括号(double-mustache)进行 HTML 本义插值(HTML-escaped interpolation) -->    <title>{{ title }}</title>    <!-- 应用三花括号(triple-mustache)进行 HTML 不本义插值(non-HTML-escaped interpolation) -->    {{{ meta }}}  </head>  <body>    <!--vue-ssr-outlet-->  </body></html>
const Vue = require('vue');const fs = require('fs');const server = require('express')();// 读取模板const template = fs.readFileSync('./template.html', 'utf-8');const { createRenderer } = require('vue-server-renderer');// 生成带有模板的渲染器const renderer = createRenderer({ template });const context = {    title: 'ssr',    meta: `<meta charset="utf-8">`,};server.get('*', async (req, res) => {    const app = new Vue({        template: `<div>hello {{str}}</div>`,        data(){            return {                str: req.url            }        }    });    // 渲染app, 并给temlate传递上下文context    const html = await renderer.renderToString(app, context);    res.end(html);});const port = 3000;server.listen(port, () => console.log(`http://127.0.0.1:${port}`));

后面的这么多步骤,只是将vue的实例渲染并失去了一个残缺的HTML页面,后果如图

后果仅仅是失去了一个领有HTML字符串的页面,没有javascript,即便咱们在vue实例app中写了点击事件@click等于methods中的一个办法,也会被疏忽。那么如何才可能vue实例失去残缺的渲染呢,请持续看。

4、客户端激活

咱们曾经实现了将一个vue实例渲染成HTML字符串,并配合template.html模板生成一个残缺的HTML页面。剩下的就是讲服务端生成vue实例的代码通过script脚本的模式退出到咱们生成的HTML页面中,咱们的script脚本达到浏览器后,会生成一个与服务端雷同的客户端vue实例,客户端实例通过app.$mount('#app')挂载,而后顺利地接管了带有有data-server-rendered="true"属性的DOM元素,这是运行在浏览器vue实例的createdmounted钩子会顺次执行。

特地阐明:因为没有应用webpack,咱们只能抉择esmodule模块来实现服务器和浏览器通用的代码。应用 node --experimental-modules xxx.mjs就能够在node.js中运行esmodule了。因而咱们把所有的js的扩展名都改成了.mjs,并且应用server.use(express.static('./'))提供动态服务器,为浏览器载app.mjs,vue.esm.mjs,vue-router.esm.mjs等文件。在HTML中应用<script type="module"></script>加载浏览器代码。其中vue.esm.mjs,vue-router.esm.mjs两个文件是从vue的npm安装包中拷贝进去的,并且在代码的结尾增加var process = { env:{ NODE_ENV: 'development' } }mock了环境变量,保障vue代码的失常运行。

4.1 残缺的服务端渲染

源码:demo05

// server.mjsimport fs from 'fs';import express from 'express';import { createApp } from './app.mjs';const server = express();const template = fs.readFileSync('./template.html', 'utf-8');const renderer = vueServerRenderer.createRenderer({ template });// 能够与 Vue 应用程序实例共享 context 对象,容许模板插值中的组件动静地注册数据。const context = {    title: 'hello wrold',    meta: `<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">`,};// 提供动态服务器,能够提供浏览器加载app.mjs,vue.esm.mjs,vue-router.esm.mjsserver.use(express.static('./'));server.get('*', async (req, res) => {    const { app } = createApp();    const html = await renderer.renderToString(app, context);    res.end(html);});const port = 3000;server.listen(port, () => console.log(`http://127.0.0.1:${port}`));
// app.mjsimport Vue from './vue.esm.mjs';Vue.config.devtools = true;export const createApp = (context = {}) => {    const app = new Vue({        template: `<div id="app">            <span @click="handleClick">hello {{str}}</span>        </div>`,        data() {            return {                str: 'Jack'            }        },        created() {            console.log('created');        },        mounted() {            console.log('mounted');        },        methods:{            handleClick(){                this.str = 'Rose';            }        }    });    return { app }}
<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    {{{meta}}}    <title>{{title}}</title></head><body>    <!-- vue实例渲染的字符串插在这里 -->    <!--vue-ssr-outlet-->    <script type='module'>        import { createApp } from './app.mjs';        const { app } = createApp();        // 与服务端雷同的vue实例app,挂载后接管服务端渲染的HTML        app.$mount('#app', true);    </script></body></html>

4.2 应用vue-router的服务端渲染

源码:demo06
如果咱们间接在浏览器地址栏申请127.0.0.1:3000/foo,此时浏览器会向服务器申请页面,服务器依据路由匹配一个实现的app实例,渲染成残缺的HTML页面返回前端。如果应用<router-link to='/foo'>foo</router-link>从bar路由跳转到foo路由,浏览器不会向服务器发动申请(此时是客户端接管)。

// server.mjsimport vueServerRenderer from 'vue-server-renderer';import fs from 'fs';import express from 'express';import { createApp } from './app.mjs';const server = express();const template = fs.readFileSync('./template.html', 'utf-8');const renderer = vueServerRenderer.createRenderer({ template });// 能够与 Vue 应用程序实例共享 context 对象,容许模板插值中的组件动静地注册数据。const context = {    title: 'hello wrold',    meta: `<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">`,};server.use(express.static('./'));server.get('*', async (req, res) => {    const matchApp = context => {        // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,        // 以便服务器可能期待所有的内容在渲染前,        // 就曾经准备就绪。        return new Promise((resolve, reject) => {            const { app, router } = createApp()            // 设置服务器端 router 的地位            router.push(context.url);            // 等到 router 将可能的异步组件和钩子函数解析完            router.onReady(() => {                const matchedComponents = router.getMatchedComponents()                // 匹配不到的路由,执行 reject 函数,并返回 404                if (!matchedComponents.length) {                    return reject({ code: 404 })                }                // Promise 应该 resolve 应用程序实例,以便它能够渲染                resolve(app)            }, reject)        })    }    context.url = req.url;    const app = await matchApp(context);    const html = await renderer.renderToString(app, context);    res.end(html);});const port = 3000;server.listen(port, () => console.log(`http://127.0.0.1:${port}`));
// app.mjsimport Vue from './vue.esm.mjs';import VueRouter from './vue-router.esm.mjs'Vue.config.devtools = true;Vue.use(VueRouter);export const createRouter = (context = {}) => {    const Foo = { template: '<div>foo</div>', mounted(){console.log('foo mounted')} }    const Bar = { template: '<div>bar</div>', mounted(){console.log('bar mounted')} }    const routes = [        { path: '/foo', component: Foo },        { path: '/bar', component: Bar }    ]    return new VueRouter({        mode: "history",        routes    })}export const createApp = (context = {}) => {    const router = createRouter(context);    const app = new Vue({        router,        template: `<div id="app">            <router-link to="/foo">foo</router-link>            <router-link to="/bar">bar</router-link>            <span>hello {{str}}</span>            <router-view></router-view>        </div>`,        data() {            return {                str: 'Jack'            }        },        created() {            console.log('created');        },        mounted() {            console.log('mounted');        }    });    return { app, router }}
// template.html<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    {{{meta}}}    <title>{{title}}</title></head><body>    <!-- vue实例渲染的字符串插在这里 -->    <!--vue-ssr-outlet-->    <script type='module'>        import { createApp } from './app.mjs';        const { app, router } = createApp();        // 与服务端雷同的vue实例app,挂载后接管服务端渲染的HTML        router.onReady(() => app.$mount('#app', true))    </script></body></html>

4.3 客户端数据预取

源码:demo07

这是官网文档阐明。

vue路由会匹配路由对应的组件,调用组件的asyncData办法抓取数据渲染组件,并返回一个promise。待promise实现后失去一个残缺的App实例,将App实例渲染成残缺页面返回给浏览器。当<router-link to='/foo'>foo</router-link>从bar路由跳转到foo路由时,渲染则是客户端实现的。客户端vue通过router.beforeResolve拦挡路由,而后调用asyncData 办法,返回的promise实现后初始化渲染,而后调用next计入指标路由页面。

4.4 应用webpack配置服务端渲染

源码:demo08
本栗子应用了vue-cli应用vue.config.js配置打包,通过npm run build:clientnpm run build:server别离实现服务端和客户端的构建,失去通用的代码,client资源表vue-ssr-client-manifest.json和server资源表vue-ssr-server-bundle.json, 然而基本原理和后面讲的一样。