vue-ssr在项目中的实践

写在文前

由于前端脚手架、打包工具、Node等版本的多样性,本文无法同时兼顾,文中所述皆基于以下技术栈进行。

脚手架:vue-cli3

打包工具:webpack4,集成在vue-cli3中,通过修改vue.config.js的方式进行配置

Node框架:koa2

简介

​ 服务器端渲染,即采用“同构”的策略,在服务器端对一部分前端代码进行渲染,减少浏览器对页面的渲染量。

通常服务器端渲染的优点和用途有以下几点:

1.更好的SEO

2.更快的页面加载速度

3.在服务器端完成数据的加载

​ 但需要注意,在服务器端渲染提高客户端性能的同时,也带来了更高的服务器负荷的问题。在项目开发时需要权衡其优点及缺点。

Vue项目中如何实现服务器端渲染?

在做Vue-ssr之前的一些思考

1.Vue在页面渲染时以Vue实例为基本单元,在服务器端进行渲染时,是否也应对Vue实例进行渲染?

2.用户与客户端的关系是一对一,而与服务器端的关系是多对一,如何避免多个用户之间在服务器端的数据共享的问题?

3.如何实现同构策略?即让服务器端能够运行前端的代码?

4.服务器端渲染的Vue项目,开发环境和生产环境分别应该如何部署?有何区别?

5.如何保证服务器端渲染改造后的代码仍能通过访问静态资源的方式直接访问到?

对于这些思考,将在文末进行回顾。

具体实现方案

Vue官方提供了【vue-server-renderer】包实现Vue项目的服务器渲染,安装方式如下:

npm install vue-server-renderer --save

在使用vue-server-renderer时需要注意以下一些问题:

1.vue-server-renderer版本须与vue保持一致

2.vue-server-renderer只能在node端进行运行,推荐node.js6+版本

一、最简单的实现

​ vue-server-renderer为我们提供了一个【createRenderer】方法,支持对单一Vue实例进行渲染,并输出渲染后的html字符串或node可读的stream流。

// 1.创建Vue实例
const Vue = require('vue');
const app = new Vue({
   
  template: '<div></div>',
});

// 2.引入renderer方法
const renderer = require('vue-server-renderer').createRenderer();

// 3-1.将Vue实例渲染为html字符串
renderer.renderToString(app, (err, html) => {
   });
// or
renderer.renderToString(app).then((html) => {
   }, (err) => {
   });

// 3-2.将Vue实例渲染为stream流
const renderStream = renderer.renderToStream(app);
// 通过订阅事件,在回调中进行操作
// event可取值'data'、'beforeStart'、'start'、'beforeEnd'、'end'、'error'等
renderStream.on(event, (res) => {
   });

​ 但通常情况下,我们没有必要在服务器端创建Vue实例并进行渲染,而是需要对前端的Vue项目中每个SPA的Vue实例进行渲染,基于此,vue-server-renderer为我们提供了一套如下的服务器端渲染方案。

二、完整的实现

完整的实现流程如下图所示分为【模板页】(HTML)、【客户端】(Client Bundle)、【服务器端】(Server Bundle)三个模块。三个模块功能如下:

模板页:提供给客户端和服务器端渲染的html框架,令客户端和服务器端在该框架中进行页面的渲染

客户端:仅在浏览器端执行,向模板页中注入js、css等静态资源

服务器端:仅在服务器端执行,将Vue实例渲染为html字符串,注入到模板页的对应位置中

架构

整个服务的构建流程分为以下几步:

1.通过webpack将Vue应用打包为浏览器端可执行的客户端Bundle;

2.通过webpack将Vue应用打包为Node端可执行的服务器端Bundle;

3.Node端调用服务器端Bundle渲染Vue应用,并将渲染好的html字符串以及客户端Bundle发送至浏览器;

4.浏览器端接收到后,调用客户端Bundle向页面注入静态资源,并与服务器端渲染好的页面进行匹配。

需要注意的是,客户端与服务器端渲染的内容需要匹配才能进行正常的页面加载,一些页面加载异常问题将在下文进行具体描述。

三、具体代码实现
1、Vue应用程序改造

​ SPA模式下,用户与Vue应用是一对一的关系,而在SSR模式下,由于Vue实例是在服务器端进行渲染,而服务器是所有用户共用的,用户与Vue应用的关系变为了多对一。这就导致多个用户共用同一个Vue实例,导致实例中的数据相互污染。

​ 针对这个问题,我们需要对Vue应用的入口进行改造,将Vue实例的创建改为“工厂模式”,在每次渲染的时候创建新的Vue实例,避免用户共用同一个Vue实例的情况。具体改造代码如下:

// router.js
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export function createRouter() {
   
    return new Router({
   
        mode: 'history',
        routes: [],
    });
}

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export function createStore() {
   
    return new Vuex.Store({
   
        state,
        actions: {
   },
        mutations: {
   },
        modules: {
   },
    });
}

// main.js
import Vue from 'vue';
import App from './App.vue';
import {
   createRouter} from './router';
import {
   createStore} from './store';

export function createApp() {
   
    const router = createRouter();
    const store = createStore();

    const app = new Vue({
   
        router,
        store,
        render: (h) => h(App),
    });
    return {
   app, router, store};
}

​ 需要注意的是,我们需要将vue-router、vuex等Vue实例内部使用的模块也配置为“工厂模式”,避免路由、状态等在多个Vue实例间共用。

​ 同时,由于我们在SSR过程中需要使用到客户端和服务器端两个模块,因此需要配置客户端、服务器端两个入口。

客户端入口配置如下:

// entry-client.js
import {
   createApp} from './main';

const {
   app, router, store} = createApp();

if (window.__INITIAL_STATE__) {
   
    store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
   
    app.$mount('#app');
});

​ 在上文中我们提到,客户端Bundle的功能是在浏览器端接收到服务器渲染好的html字符串后,向页面中注入静态资源以及页面的二次渲染工作,因此我们在Vue应用的客户端入口中,只需像之前一样将Vue实例挂载到指定的html标签上即可。

​ 同时,服务器端在渲染时如果有数据预取操作,会将store中的数据先注入到【window.__INITIAL_STATE__】,在客户端中,我们需要将window.__INITIAL_STATE__中的值重新赋给store。

服务器端入口配置如下:

// entry-server.js
import {
   createApp} from './main';

export default (context) => {
   
    return new Promise((resolve, reject) => {
   
        const {
   app, router, store} = createApp();

        // 设置服务器端 router 的位置
        router.push(context.url);

        // 等到 router 将可能的异步组件和钩子函数解析完
        router.onReady(() => {
   
            const matchedComponents = router.getMatchedComponents();
            // 匹配不到的路由,执行 reject 函数,并返回 404
            if (!matchedComponents.length) {
   
                return reject({
   
                    code: 404
                });
            }
            Promise.all(matchedComponents.map((Component) => {
   
                if (Component.extendOptions.a
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值