MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue服务端渲染(SSR)的实现细节

2023-12-314.9k 阅读

Vue 服务端渲染(SSR)的基本概念

什么是服务端渲染

在传统的前端开发中,页面通常是在浏览器端通过 JavaScript 动态渲染生成的。这意味着浏览器需要先下载 HTML 骨架,然后再加载 JavaScript 脚本,解析并执行脚本后才能呈现出完整的页面内容。而服务端渲染(Server - Side Rendering,简称 SSR)则是在服务器端将 Vue 组件渲染为 HTML 字符串,然后将完整的 HTML 页面发送到浏览器。这样,浏览器在收到页面时已经是一个完全渲染好的 DOM 结构,无需等待额外的 JavaScript 执行来生成页面内容,大大提高了首屏加载速度,对于 SEO 也更为友好,因为搜索引擎爬虫可以直接获取到完整的页面内容。

SSR 与客户端渲染的对比

  1. 首屏加载速度:客户端渲染需要先加载 HTML 骨架和 JavaScript 脚本,然后在浏览器端执行脚本渲染页面,这个过程会导致首屏加载时间较长。而 SSR 由于在服务器端已经生成了完整的 HTML,浏览器直接接收并展示,首屏加载速度更快。
  2. SEO 友好度:搜索引擎爬虫在抓取页面时,更倾向于解析完整的 HTML 内容。客户端渲染的页面在爬虫抓取时可能只能获取到初始的 HTML 骨架,无法获取到动态渲染的内容,不利于 SEO。SSR 生成的页面包含完整的内容,对 SEO 更为友好。
  3. 交互体验:客户端渲染在脚本加载并执行后,可以实现非常流畅的交互体验,因为所有的逻辑都在客户端处理。而 SSR 在首次加载时虽然速度快,但后续的交互可能需要更多的处理,例如重新渲染部分组件,这可能会导致一些性能问题,不过通过合理的优化可以改善。

Vue SSR 的实现原理

构建流程

  1. 客户端构建:与常规的 Vue 项目构建类似,通过 webpack 等工具将 Vue 组件、JavaScript、CSS 等资源打包成适合在浏览器端运行的文件。这个过程主要是将 Vue 组件编译成浏览器可执行的 JavaScript 代码,并处理样式等资源。
  2. 服务端构建:同样使用 webpack 进行构建,但配置与客户端构建有所不同。服务端构建会将 Vue 组件渲染为 Node.js 模块,这个模块可以在服务器端运行并生成 HTML 字符串。在服务端构建中,webpack 会将 Vue 组件中的模板、样式等资源进行处理,使其能够在 Node.js 环境中正确渲染。

渲染过程

  1. 服务器端渲染:当客户端请求一个页面时,服务器接收到请求后,会加载通过服务端构建生成的 Vue 应用模块。然后,服务器使用 Vue 的渲染函数将组件渲染为 HTML 字符串。在这个过程中,Vue 会按照组件的生命周期钩子函数顺序执行相关逻辑,例如 createdmounted 等钩子函数中的数据获取操作,确保在渲染前数据已经准备好。
  2. 客户端激活:服务器将渲染好的 HTML 发送到客户端后,客户端需要将这个静态的 HTML 激活为一个可交互的 Vue 应用。这一步通过重新创建 Vue 实例,并将服务器端渲染生成的 HTML 作为初始状态进行挂载来实现。客户端会复用服务器端渲染的 DOM 结构,然后在其上绑定事件监听器等,使其具备交互能力。

环境准备

安装 Node.js

Vue SSR 需要在 Node.js 环境下运行,因此首先要确保系统中安装了 Node.js。可以从 Node.js 官方网站(https://nodejs.org/)下载并安装适合系统的版本。安装完成后,通过在命令行中输入 node -v 来验证是否安装成功,如果输出版本号,则说明安装正确。

初始化 Vue 项目

  1. 使用 Vue CLI 创建一个新的 Vue 项目。在命令行中执行以下命令:
vue create my - ssr - project

按照提示选择项目的配置,例如选择 Vue 2.x 还是 Vue 3.x 等。这里以 Vue 2.x 为例进行讲解。 2. 进入项目目录:

cd my - ssr - project

安装依赖

  1. 安装 Vue SSR 相关依赖。在项目目录下执行:
npm install vue - server - renderer vue - router express
  • vue - server - renderer:用于在服务器端渲染 Vue 组件为 HTML。
  • vue - router:用于处理路由,在 SSR 中同样需要管理页面路由。
  • express:一个流行的 Node.js web 服务器框架,用于搭建服务器接收客户端请求并返回渲染后的页面。

服务端渲染的配置

服务器端构建配置

  1. 在项目根目录下创建 webpack.server.js 文件,用于配置服务端的 webpack 构建。以下是一个基本的配置示例:
const path = require('path');
const VueSSRServerPlugin = require('vue - server - renderer/server - plugin');

module.exports = {
    target:'server',
    entry: './src/entry - server.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename:'server - bundle.js',
        libraryTarget: 'commonjs2'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue - loader'
            },
            {
                test: /\.js$/,
                loader: 'babel - loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname,'src')
        }
    },
    plugins: [
        new VueSSRServerPlugin()
    ]
};
  • target:'server':指定这是一个针对服务器端的构建。
  • entry: './src/entry - server.js':指定服务端的入口文件,后续会详细介绍入口文件的内容。
  • output 配置:指定输出路径和文件名,libraryTarget: 'commonjs2' 确保输出的模块可以在 Node.js 环境中使用。
  • module.rules:配置了对 .vue.js 文件的加载器,vue - loader 用于处理 Vue 组件,babel - loader 用于将 ES6+ 代码转换为 Node.js 可执行的代码。
  • resolve.alias:设置别名,方便在项目中引用 src 目录下的文件。
  • new VueSSRServerPlugin():这个插件会生成一个可以在服务器端运行的 bundle 文件。

客户端构建配置

在项目根目录下创建 webpack.client.js 文件,用于配置客户端的 webpack 构建。基本配置如下:

const path = require('path');
const VueSSRClientPlugin = require('vue - server - renderer/client - plugin');

module.exports = {
    target: 'web',
    entry: './src/entry - client.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'client - bundle.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue - loader'
            },
            {
                test: /\.js$/,
                loader: 'babel - loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname,'src')
        }
    },
    plugins: [
        new VueSSRClientPlugin()
    ]
};
  • target: 'web':指定这是一个针对浏览器端的构建。
  • entry: './src/entry - client.js':指定客户端的入口文件。
  • output 配置:指定输出路径和文件名,publicPath: '/' 设置资源的公共路径。
  • 同样配置了 module.rulesresolve.aliasnew VueSSRClientPlugin()VueSSRClientPlugin 会生成一些辅助文件,用于客户端激活。

配置 Babel

在项目根目录下创建 .babelrc 文件,用于配置 Babel 转码规则。以下是一个基本的配置:

{
    "presets": [
        [
            "@babel/preset - env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}

这个配置使用 @babel/preset - env 预设,将 ES6+ 代码转换为适合当前 Node.js 版本运行的代码。

编写入口文件

服务端入口文件(entry - server.js)

src 目录下创建 entry - server.js 文件,内容如下:

import Vue from 'vue';
import App from './App.vue';
import router from './router';

export default context => {
    return new Promise((resolve, reject) => {
        const app = new Vue({
            router,
            render: h => h(App)
        });
        resolve(app);
    });
};

这个文件导出一个函数,该函数接收一个 context 参数(通常用于传递请求相关的信息)。函数返回一个 Promise,在 Promise 中创建一个 Vue 实例,并将其 resolve。routerApp 组件被引入并用于创建 Vue 实例,render 函数指定了根组件为 App

客户端入口文件(entry - client.js)

src 目录下创建 entry - client.js 文件,内容如下:

import Vue from 'vue';
import App from './App.vue';
import router from './router';

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

// 激活客户端
if (window.__INITIAL_STATE__) {
    app.$store.replaceState(window.__INITIAL_STATE__);
}
app.$mount('#app');

客户端入口文件首先创建一个 Vue 实例,与服务端入口类似。然后,如果存在 window.__INITIAL_STATE__(这个状态是在服务端渲染时注入到 HTML 中的),则将其替换到 Vuex 的状态中。最后,将 Vue 实例挂载到页面的 #app 元素上。

路由配置

src/router 目录下创建 index.js 文件,用于配置路由。以下是一个简单的示例:

import Vue from 'vue';
import Router from 'vue - router';
import Home from '@/components/Home.vue';
import About from '@/components/About.vue';

Vue.use(Router);

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            path: '/about',
            name: 'About',
            component: About
        }
    ]
});

这里使用 vue - router 配置了两个路由,/ 对应 Home 组件,/about 对应 About 组件。mode: 'history' 使用 HTML5 的 History API 来实现无哈希的路由。

服务器端实现

创建 Express 服务器

在项目根目录下创建 server.js 文件,用于搭建 Express 服务器并处理 SSR。内容如下:

const express = require('express');
const { createBundleRenderer } = require('vue - server - renderer');
const serverBundle = require('./dist/server - bundle.js');
const clientManifest = require('./dist/client - manifest.json');
const app = express();

const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template: require('fs').readFileSync(path.join(__dirname, 'index.template.html'), 'utf - 8'),
    clientManifest
});

app.use(express.static(path.join(__dirname, 'dist')));

app.get('*', (req, res) => {
    const context = {
        url: req.url
    };
    renderer.renderToString(context, (err, html) => {
        if (err) {
            if (err.code === 404) {
                res.status(404).end('Page Not Found');
            } else {
                res.status(500).end('Internal Server Error');
            }
        } else {
            res.end(html);
        }
    });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});
  • 引入 expressvue - server - renderer 等模块,并加载服务端 bundle 文件和客户端 manifest 文件。
  • 使用 createBundleRenderer 创建一个渲染器,runInNewContext: false 表示在当前 Node.js 上下文环境中运行,template 配置了 HTML 模板文件,clientManifest 用于客户端激活。
  • 使用 express.static 中间件来处理静态资源,将 dist 目录作为静态资源目录。
  • app.get('*') 中,为每个请求创建一个 context 对象,包含请求的 URL。然后使用渲染器的 renderToString 方法将 Vue 应用渲染为 HTML 字符串。如果渲染过程中出现错误,根据错误类型返回相应的 HTTP 状态码和错误信息;如果渲染成功,则将 HTML 返回给客户端。
  • 最后,服务器监听指定端口,默认是 3000 端口。

HTML 模板

在项目根目录下创建 index.template.html 文件,作为服务器端渲染的 HTML 模板。内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale=1.0">
    <title>Vue SSR Example</title>
    <!-- 引入客户端构建生成的 CSS 等资源 -->
    <% for (var i in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[i] %>" rel="stylesheet">
    <% } %>
</head>
<body>
    <div id="app"><%= appHtml %></div>
    <!-- 注入服务端渲染生成的状态 -->
    <script>
        window.__INITIAL_STATE__ = <%= JSON.stringify(initialState) %>
    </script>
    <!-- 引入客户端构建生成的 JavaScript 资源 -->
    <% for (var i in htmlWebpackPlugin.files.js) { %>
    <script src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
    <% } %>
</body>
</html>

这个模板文件使用了 EJS 模板语法(可以通过安装 html - webpack - plugin 并在 webpack 配置中使用来支持)。<%= appHtml %> 会被服务端渲染生成的 HTML 内容替换,window.__INITIAL_STATE__ 用于注入服务端渲染时的初始状态,以便客户端激活时使用。模板中还引入了客户端构建生成的 CSS 和 JavaScript 资源。

数据获取与状态管理

在 SSR 中获取数据

  1. 生命周期钩子函数:在 Vue 组件中,可以利用 createdmounted 钩子函数来获取数据。例如,在 Home 组件中:
export default {
    data() {
        return {
            posts: []
        };
    },
    created() {
        // 模拟数据请求
        fetch('https://api.example.com/posts')
          .then(response => response.json())
          .then(data => {
                this.posts = data;
            });
    }
};

在服务端渲染时,created 钩子函数中的数据请求会在服务器端执行,确保在渲染前数据已经准备好。

  1. 异步组件数据获取:对于异步组件,可以使用 asyncData 方法来获取数据。首先安装 @nuxtjs/async - data 插件(虽然它是 Nuxt.js 相关的,但在普通 Vue SSR 项目中也可借鉴)。然后在异步组件中:
import { asyncData } from '@nuxtjs/async - data';

export default {
    data() {
        return {
            user: null
        };
    },
    asyncData({ $axios }) {
        return $axios.get('https://api.example.com/user')
          .then(response => {
                return {
                    user: response.data
                };
            });
    }
};

asyncData 方法会在组件渲染前执行,并且可以在服务端和客户端共享数据获取逻辑。

使用 Vuex 进行状态管理

  1. 安装 Vuex:在项目目录下执行 npm install vuex
  2. 创建 Vuex store:在 src/store 目录下创建 index.js 文件,内容如下:
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        }
    },
    actions: {
        incrementAsync({ commit }) {
            setTimeout(() => {
                commit('increment');
            }, 1000);
        }
    }
});

export default store;
  1. 在 SSR 中使用 Vuex:在服务端入口文件 entry - server.js 中:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

export default context => {
    return new Promise((resolve, reject) => {
        const app = new Vue({
            router,
            store,
            render: h => h(App)
        });
        resolve(app);
    });
};

在客户端入口文件 entry - client.js 中:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

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

if (window.__INITIAL_STATE__) {
    app.$store.replaceState(window.__INITIAL_STATE__);
}
app.$mount('#app');

在服务端渲染时,Vuex 的状态会被填充,并且可以通过 window.__INITIAL_STATE__ 传递给客户端,客户端在激活时可以复用这些状态,确保服务端和客户端状态的一致性。

优化与注意事项

性能优化

  1. 代码拆分:在客户端和服务端构建中,通过配置 webpack 的代码拆分功能,将代码按照路由、功能模块等进行拆分,减少初始加载的文件大小。例如,在 webpack.client.js 中可以使用 splitChunks 配置:
module.exports = {
    //...其他配置
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
};
  1. 缓存:在服务器端,可以对一些不经常变化的数据进行缓存,例如在获取数据的 API 调用时,可以使用 node - cache 等库进行缓存。这样在相同请求时,可以直接从缓存中获取数据,减少数据获取的时间。
  2. 预渲染:对于一些静态页面,可以使用预渲染工具(如 prerender - spa - plugin)在构建时生成静态 HTML 文件。这样,这些页面可以直接作为静态资源被服务器返回,进一步提高加载速度。

注意事项

  1. 生命周期钩子函数的差异:在服务端渲染时,一些生命周期钩子函数(如 mounted)可能不会像在客户端那样完全执行,因为服务端渲染主要关注的是生成 HTML。因此,需要确保在 created 等钩子函数中完成数据获取等关键操作。
  2. 第三方库的兼容性:部分第三方库可能不兼容服务端渲染环境,例如一些依赖于浏览器全局对象(如 windowdocument)的库。在使用第三方库时,需要检查其是否支持 SSR,或者通过一些方法(如条件加载)使其在服务端和客户端都能正常工作。
  3. 状态管理的一致性:确保服务端和客户端的 Vuex 状态一致非常重要。在服务端渲染时填充的状态,需要正确地传递给客户端并在客户端激活时复用,否则可能会出现页面显示不一致的问题。

通过以上详细的步骤和技术细节,我们可以实现一个完整的 Vue 服务端渲染应用,充分发挥 SSR 的优势,提升应用的性能和用户体验。在实际开发中,还需要根据项目的具体需求和场景进行进一步的优化和调整。