Vue SSR与Webpack/Vite Bundle Renderer:如何将组件编译为优化的服务端渲染代码

Vue SSR与Webpack/Vite Bundle Renderer:将组件编译为优化的服务端渲染代码

大家好,今天我们来深入探讨 Vue SSR(Server-Side Rendering)中一个至关重要的环节:如何利用 Webpack 或 Vite 的 Bundle Renderer 将 Vue 组件编译为优化的服务端渲染代码。 我们将从原理入手,结合实际代码示例,逐步讲解不同方案的实现方式、优缺点以及优化策略。

一、SSR 的核心需求与 Bundle Renderer 的作用

在传统的客户端渲染 (CSR) 模式下,浏览器下载 HTML、CSS 和 JavaScript 文件后,由 JavaScript 负责渲染页面。 这会导致首次渲染时间较长,对 SEO 不友好。 SSR 则是在服务器端预先渲染好 HTML,直接发送给浏览器,从而优化首屏加载速度和 SEO。

那么,如何将 Vue 组件转化为服务器端可执行的 HTML 字符串呢? 这就是 Bundle Renderer 的作用。 Bundle Renderer 负责读取编译后的 JavaScript 包(Bundle),执行 Vue 应用的渲染逻辑,并将结果转化为 HTML。

二、Webpack Bundle Renderer:历史悠久且稳定的选择

Webpack Bundle Renderer 是 Vue 官方推荐的 SSR 方案。它通过 vue-server-renderer 包提供。 其核心流程可以概括为:

  1. 编写服务端入口文件: 这个文件负责创建 Vue 实例,配置路由、状态管理等。
  2. Webpack 构建: 使用 Webpack 将服务端入口文件及其依赖打包成一个 JavaScript Bundle。 通常会生成 vue-ssr-server-bundle.jsonvue-ssr-client-manifest.json 这两个重要文件。
  3. Bundle Renderer 创建: 在服务器端,使用 vue-server-renderer 包的 createBundleRenderer 方法,加载 vue-ssr-server-bundle.json,创建一个 Bundle Renderer 实例。
  4. 执行渲染: 使用 Bundle Renderer 的 renderToString 方法,执行渲染,得到 HTML 字符串。

2.1 代码示例:Webpack SSR 的基本配置

首先,我们需要安装必要的依赖:

npm install vue vue-server-renderer webpack webpack-cli vue-loader vue-template-compiler --save-dev
npm install express --save

2.1.1 服务端入口 (server.js):

const express = require('express');
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer');

const app = express();

const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync('./index.template.html', 'utf-8');

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false, // 推荐
  template, // (可选)页面模板
  clientManifest // (可选)客户端构建 manifest
});

app.use('/dist', express.static('./dist'));

app.get('*', (req, res) => {
  const context = {
    url: req.url,
    title: 'Vue SSR Example',
    meta: `
      <meta name="description" content="A simple Vue SSR example">
    `
  };

  renderer.renderToString(context, (err, html) => {
    if (err) {
      console.error(err);
      res.status(500).end('Internal Server Error');
      return;
    }
    res.end(html);
  });
});

app.listen(3000, () => {
  console.log('Server started at http://localhost:3000');
});

2.1.2 客户端入口 (src/entry-client.js):

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';

const router = createRouter();

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

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

2.1.3 服务端入口 (src/entry-server.js):

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';

export function createApp(context) {
  const router = createRouter();

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

  return new Promise((resolve, reject) => {
    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      resolve(app);
    }, reject);
  });
}

2.1.4 路由配置 (src/router.js):

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

Vue.use(VueRouter);

export function createRouter() {
  return new VueRouter({
    mode: 'history',
    routes: [
      { path: '/', component: Home },
      { path: '/about', component: About }
    ]
  });
}

2.1.5 App 组件 (src/App.vue):

<template>
  <div id="app">
    <h1>{{ title }}</h1>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Vue SSR Example'
    };
  }
};
</script>

2.1.6 Webpack 配置 (webpack.server.js):

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  target: 'node',
  entry: './src/entry-server.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'vue-ssr-server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  devtool: 'source-map',
  resolve: {
    extensions: ['.js', '.vue', '.json']
  },
  externals: nodeExternals({
    whitelist: /.css$/ // 允许处理 CSS 文件
  }),
  module: {
    rules: [
      {
        test: /.vue$/,
        use: 'vue-loader'
      },
      {
        test: /.js$/,
        use: 'babel-loader'
      },
      {
        test: /.css$/,
        use: ['vue-style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
};

2.1.7 Webpack 配置 (webpack.client.js):

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

module.exports = {
  entry: './src/entry-client.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'vue-ssr-client-bundle.js'
  },
  resolve: {
    extensions: ['.js', '.vue', '.json']
  },
  module: {
    rules: [
      {
        test: /.vue$/,
        use: 'vue-loader'
      },
      {
        test: /.js$/,
        use: 'babel-loader'
      },
      {
        test: /.css$/,
        use: ['vue-style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new WebpackManifestPlugin({
      fileName: 'vue-ssr-client-manifest.json'
    })
  ]
};

2.1.8 页面模板 (index.template.html):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{ title }}</title>
  <!--vue-ssr-outlet-->
</head>
<body>
  <div id="app"><!--vue-ssr-outlet--></div>
  <!--vue-ssr-outlet-->
</body>
</html>

2.2 Webpack SSR 的优点与缺点

  • 优点:
    • 成熟稳定,社区支持广泛。
    • 配置灵活,可定制性强。
    • 适用于大型复杂项目。
  • 缺点:
    • 配置相对复杂,学习曲线较陡峭。
    • 构建速度可能较慢,尤其是在大型项目中。
    • 需要手动处理客户端和服务端的代码分割。

三、Vite Bundle Renderer:现代化的快速构建方案

Vite 是一个基于 ES modules 的前端构建工具,以其极速的冷启动和热更新而闻名。 Vue 3 官方也推荐使用 Vite 构建 SSR 应用。

Vite SSR 的核心流程与 Webpack 类似,但由于 Vite 的构建方式不同,在配置和实现上有一些差异:

  1. 编写服务端入口文件: 与 Webpack 类似,需要创建 Vue 实例并配置路由等。
  2. Vite 构建: Vite 使用 Rollup 进行生产环境构建,将服务端入口文件及其依赖打包成一个 JavaScript Bundle。
  3. Bundle Renderer 创建: 使用 vue-server-renderer 包的 createBundleRenderer 方法,加载 Vite 构建生成的 Bundle。
  4. 执行渲染: 使用 Bundle Renderer 的 renderToString 方法,执行渲染,得到 HTML 字符串。

3.1 代码示例:Vite SSR 的基本配置

首先,我们需要创建一个 Vite 项目并安装必要的依赖:

npm create vite@latest my-vue-ssr-app --template vue
cd my-vue-ssr-app
npm install vue vue-server-renderer express @vitejs/plugin-vue --save

3.1.1 服务端入口 (server.js):

import express from 'express';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createBundleRenderer } from 'vue-server-renderer';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();

async function createServer() {
  const template = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');
  const manifest = require('./dist/manifest.json');
  const serverBundle = require('./dist/server/entry-server.js');

  const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest: manifest
  });

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

  app.get('*', async (req, res) => {
    const context = {
      url: req.url,
      title: 'Vite SSR Example',
      meta: `
        <meta name="description" content="A simple Vite SSR example">
      `
    };

    try {
      const html = await renderer.renderToString(context);
      res.setHeader('Content-Type', 'text/html');
      res.end(html);
    } catch (err) {
      console.error(err);
      res.status(500).end('Internal Server Error');
    }
  });

  app.listen(3000, () => {
    console.log('Server started at http://localhost:3000');
  });
}

createServer();

3.1.2 客户端入口 (src/entry-client.js):

import { createApp } from './main';

const { app, router } = createApp();

router.isReady().then(() => {
  app.mount('#app');
});

3.1.3 服务端入口 (src/entry-server.js):

import { createApp } from './main';

export async function render(url, manifest) {
  const { app, router } = createApp();

  router.push(url);
  await router.isReady();

  const appContent = await import('vue-server-renderer').then(module => module.renderToString(app));

  return {
    html: appContent
  };
}

3.1.4 主文件 (src/main.js):

import { createSSRApp } from 'vue';
import App from './App.vue';
import { createRouter } from './router';

export function createApp() {
  const app = createSSRApp(App);
  const router = createRouter();
  app.use(router);
  return { app, router };
}

3.1.5 路由配置 (src/router.js):

import { createRouter as createVueRouter, createWebHistory, createMemoryHistory } from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';

export function createRouter() {
  return createVueRouter({
    history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
    routes: [
      { path: '/', component: Home },
      { path: '/about', component: About }
    ]
  });
}

3.1.6 App 组件 (src/App.vue):

<template>
  <div id="app">
    <h1>{{ title }}</h1>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const title = ref('Vite SSR Example');
    return { title };
  }
};
</script>

3.1.7 Vite 配置 (vite.config.js):

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    ssrManifest: true,
    ssr: true,
    outDir: 'dist/server'
  }
});

3.1.8 页面模板 (index.html):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
  {{{ meta }}}
</head>
<body>
  <div id="app"><!--ssr-outlet--></div>
  <script type="module" src="/src/entry-client.js"></script>
</body>
</html>

3.2 Vite SSR 的优点与缺点

  • 优点:
    • 构建速度极快,开发效率高。
    • 配置相对简单,易于上手。
    • 原生支持 ES modules。
    • 热更新速度快。
  • 缺点:
    • 生态相对较新,社区支持不如 Webpack 广泛。
    • 某些高级 SSR 特性可能需要手动配置。
    • 生产环境构建依赖 Rollup,需要熟悉 Rollup 的配置。

四、Bundle Renderer 的配置与优化

无论是 Webpack 还是 Vite,Bundle Renderer 的配置都会直接影响 SSR 的性能和效果。 以下是一些关键配置项和优化策略:

配置项/策略 描述
runInNewContext 设置为 false 可以避免每次渲染都创建新的 V8 上下文,提高性能。 但需要注意代码隔离,避免全局变量污染。
template 提供 HTML 模板,Bundle Renderer 会将渲染结果插入到模板中的 <!--vue-ssr-outlet--> 占位符。
clientManifest 客户端构建的 manifest 文件,包含了客户端资源的依赖关系。 Bundle Renderer 可以根据 manifest 文件自动注入 CSS 和 JavaScript 链接。
缓存策略 对渲染结果进行缓存,可以显著提高重复请求的响应速度。 可以使用内存缓存、Redis 等缓存方案。
代码分割 将代码分割成更小的 chunk,可以减少首屏加载时间。 Webpack 和 Vite 都支持代码分割,需要合理配置。
资源预加载与预取 使用 <link rel="preload"><link rel="prefetch"> 标签,可以提前加载关键资源,优化页面加载速度。
Stream 渲染 使用 renderToStream 方法进行流式渲染,可以逐步将 HTML 内容发送给客户端,提高首字节到达时间 (TTFB)。
错误处理 在渲染过程中捕获错误,并进行适当的处理,例如返回错误页面或重定向。

五、总结:选择合适的 SSR 方案并持续优化

Webpack Bundle Renderer 和 Vite Bundle Renderer 都是优秀的 Vue SSR 解决方案。 Webpack 更加成熟稳定,适用于大型复杂项目;Vite 则以其极速的构建速度和简洁的配置而备受青睐,适合快速开发和构建现代化的 SSR 应用。

无论选择哪种方案,都需要深入理解其原理和配置,并根据实际项目需求进行优化,才能充分发挥 SSR 的优势,提升用户体验和 SEO 效果。 持续监控服务器性能,分析渲染瓶颈,并不断改进代码和配置,是构建高性能 Vue SSR 应用的关键。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注