返回

如何用Node.js Express服务端渲染Vue模板?

vue.js

如何使用 Node.js Express 服务端渲染 Vue 模板

在构建现代 Web 应用时,兼顾 SEO 和用户体验至关重要。传统的客户端渲染(CSR)虽然能带来流畅的交互,但在 SEO 方面存在不足。服务端渲染(SSR)则能有效解决这个问题,同时提升首屏加载速度,改善用户体验。本文将带您深入了解如何使用 Node.js Express 框架服务端渲染 Vue.js 模板,并提供详细的代码示例,帮助您轻松上手 Vue SSR。

从问题出发:浏览器为何无法识别 Vue 组件?

您可能遇到过这样的情况:使用 Node.js Express 框架搭建服务器后,浏览器无法正确渲染 Vue 模板文件。经过分析,问题根源在于浏览器无法识别 Vue 单文件组件(SFC)的 MIME 类型。

Express 服务器默认将 .vue 文件识别为二进制文件 (application/octet-stream),而浏览器在加载模块脚本时,期望接收到的是 JavaScript 模块脚本 (application/javascript)。这种 MIME 类型的不匹配导致浏览器无法解析 Vue 组件文件,进而引发错误。

解决方案:借助官方工具,配置 Express 服务器

为了解决这个问题,我们需要对 Express 服务器进行配置,使其能够正确识别和处理 Vue 单文件组件。vue-server-renderervue-template-compiler 这两个官方库将成为我们的得力助手。

1. 安装必要依赖

首先,安装以下依赖包:

npm install vue vue-server-renderer vue-template-compiler --save
  • vue-server-renderer:负责在服务器端渲染 Vue 组件。
  • vue-template-compiler:用于编译 Vue 单文件组件。

2. 修改服务器端代码

修改 server.js 文件,引入 vue-server-renderer 并创建渲染器实例:

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

// ... 其他代码 ...

// 创建 Vue SSR 渲染器
const renderer = createBundleRenderer({
  runInNewContext: false, 
  template: fs.readFileSync(path.resolve(__dirname, 'public/views/index-vue.html'), 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json'),
  serverBundle: require('./dist/vue-ssr-server-bundle.json')
});

// 修改路由处理逻辑
router.get('/', async (request, response) => {
  if (tokenClass.checkToken(request.cookies.user_token) !== -1) {
    const context = {
      title: '首页',
      meta: '<meta name="description" content="这是一个示例页面">'
    };

    try {
      const html = await renderer.renderToString(context);
      response.send(html);
    } catch (error) {
      console.error(error);
      response.status(500).send('Internal Server Error');
    }
  } else {
    response.redirect('/?token_expired');
  }
});

// ... 其他代码 ...

这段代码主要完成了以下任务:

  1. 引入 vue-server-renderer 模块。
  2. 使用 createBundleRenderer 创建一个渲染器实例,指定服务器 bundle 文件路径、客户端清单文件以及模板文件。
  3. 在路由处理函数中,调用 renderer.renderToString 方法渲染 Vue 组件,并将渲染结果发送给客户端。

3. 创建 Vue SSR 配置文件

在项目根目录下创建 vue.config.js 文件,配置服务端和客户端打包:

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const nodeExternals = require('webpack-node-externals');
const merge = require('lodash.merge');

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const isProduction = process.env.NODE_ENV !== 'development';

const baseConfig = {
  outputDir: isProduction ? 'dist/prod' : 'dist',
  configureWebpack: {
    // ... 其他配置 ...
  }
};

module.exports = merge(baseConfig, {
  configureWebpack: config => {
    if (TARGET_NODE) {
      config.target = 'node';
      config.output.libraryTarget = 'commonjs2';
      config.output.filename = 'vue-ssr-server-bundle.json';
      config.externals = nodeExternals({
        whitelist: /\.css$/
      });
      config.plugins = (config.plugins || []).concat([
        new VueSSRServerPlugin()
      ]);
    } else {
      config.entry = {
        app: './src/entry-client.js'
      };
      config.output.filename = '[name].[chunkhash:8].js';
      config.plugins = (config.plugins || []).concat([
        new VueSSRClientPlugin()
      ]);
    }
  },
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        merge(options, {
          optimizeSSR: true
        });
        return options;
      });
  }
});

这段配置主要完成了以下几项工作:

  1. 引入必要的插件,包括 VueSSRServerPluginVueSSRClientPluginnodeExternals
  2. 根据不同的打包目标 (TARGET_NODE),设置不同的配置,包括打包目标、输出文件名、外部依赖等。
  3. 使用 chainWebpackvue-loader 进行配置,开启 optimizeSSR 选项以优化服务端渲染。

4. 创建服务端和客户端入口文件

创建 src/entry-server.js 作为服务端入口文件:

import { createApp } from './main';

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

    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
        store,
        route: router.currentRoute
      }))).then(() => {
        context.state = store.state;
        resolve(app);
      }).catch(reject);
    }, reject);
  });
};

创建 src/entry-client.js 作为客户端入口文件:

import { createApp } from './main';

const { app, router } = createApp();

router.onReady(() => {
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to);
    const prevMatched = router.getMatchedComponents(from);
    let asyncDataHooks = [];

    const activate = matched.filter((c, i) => {
      return prevMatched.indexOf(c) === -1;
    });

    activate.forEach(c => {
      if (c.asyncData) {
        asyncDataHooks.push(c.asyncData({ store, route: to }));
      }
    });

    Promise.all(asyncDataHooks).then(() => {
      next();
    }).catch(next);
  });

  app.$mount('#app');
});

if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js').then(registration => {
      console.log('Service worker registered successfully:', registration);
    }).catch(err => {
      console.log('Service worker registration failed:', err);
    });
  });
}

服务端入口文件主要负责创建应用程序实例,处理路由,获取数据,并将最终渲染的应用程序实例返回给渲染器。客户端入口文件则负责在客户端创建应用程序实例,处理路由,并将其挂载到 DOM 上。

5. 构建 Vue SSR 应用

package.json 中添加以下脚本:

{
  "scripts": {
    "build:client": "vue-cli-service build --target client",
    "build:server": "vue-cli-service build --target server --modern",
    "build": "npm run build:client && npm run build:server",
    "start": "node server.js"
  }
}

然后依次运行以下命令构建 Vue SSR 应用:

npm run build:client
npm run build:server

6. 启动服务器

最后,运行以下命令启动服务器:

npm start

代码示例解析

以下是一些关键代码片段的解析:

1. 创建渲染器实例:

const renderer = createBundleRenderer({
  runInNewContext: false, 
  template: fs.readFileSync(path.resolve(__dirname, 'public/views/index-vue.html'), 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json'),
  serverBundle: require('./dist/vue-ssr-server-bundle.json')
});

这段代码使用 createBundleRenderer 方法创建了一个渲染器实例。其中:

  • runInNewContext: 设置为 false,表示在与服务器进程相同的全局上下文中运行渲染器,可以提高性能。
  • template: 指定渲染使用的 HTML 模板文件。
  • clientManifest: 指定客户端构建生成的清单文件,包含了所有 JavaScript 和 CSS 文件的信息。
  • serverBundle: 指定服务器端构建生成的 bundle 文件,包含了 Vue 应用程序的代码。

2. 服务端渲染组件:

const html = await renderer.renderToString(context);
response.send(html);

这段代码调用渲染器的 renderToString 方法,将 Vue 组件渲染成 HTML 字符串,然后将 HTML 字符串发送给客户端。

3. 处理异步数据:

Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
  store,
  route: router.currentRoute
}))).then(() => {
  // ...
});

这段代码在服务端入口文件中处理异步数据。它遍历所有匹配的组件,如果组件定义了 asyncData 方法,则调用该方法获取数据,并将数据存储在 Vuex store 中。最后,等待所有异步数据获取完成后,才将渲染的应用程序实例返回给渲染器。

常见问题解答

1. 什么是服务端渲染 (SSR)?

服务端渲染是指在服务器端将 Vue 组件渲染成 HTML 字符串,然后将 HTML 字符串发送给客户端。这样做的好处是可以提高 SEO 和首屏加载速度。

2. 为什么需要服务端渲染?

  • SEO 优化 : 搜索引擎爬虫可以直接抓取到服务端渲染生成的 HTML 内容,有利于 SEO 优化。
  • 首屏加载速度更快 : 服务端渲染将 HTML 内容直接返回给客户端,浏览器无需等待 JavaScript 代码下载和执行即可渲染页面,因此首屏加载速度更快。

3. 什么是 vue-server-renderer

vue-server-renderer 是 Vue.js 官方提供的服务端渲染库,它提供了一系列 API 用于在 Node.js 环境中渲染 Vue 组件。

4. 如何处理服务端渲染中的异步数据?

可以使用组件的 asyncData 方法在服务端获取异步数据,并将数据存储在 Vuex store 中。

5. 如何调试服务端渲染?

可以使用 console.log 在服务端打印调试信息,也可以使用 Chrome DevTools 的 “Network” 面板查看网络请求。

希望本文能帮助您更好地理解和使用 Node.js Express 服务端渲染 Vue 模板。