关于前端:一篇文章带你搞懂vue-srr

6次阅读

共计 8137 个字符,预计需要花费 21 分钟才能阅读完成。

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 步:创立一个 renderer
const renderer = require('vue-server-renderer').createRenderer();

// 第 3 步:将 Vue 实例渲染为 HTML
const 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.js
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});
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.mjs
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">`,
};
// 提供动态服务器,能够提供浏览器加载 app.mjs,vue.esm.mjs,vue-router.esm.mjs
server.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.mjs
import 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.mjs
import 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.mjs
import 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, 然而基本原理和后面讲的一样。

正文完
 0