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

Vue Router 服务端渲染(SSR)中的路由管理注意事项

2024-08-313.4k 阅读

Vue Router 服务端渲染(SSR)基础概念

在探讨 Vue Router 在服务端渲染(SSR)中的路由管理注意事项之前,我们先来回顾一下 SSR 的基本概念。SSR 是一种将 Vue 应用在服务器端渲染成 HTML 字符串,然后将其发送到客户端的技术。这样做的好处是首屏加载速度更快,并且对搜索引擎友好。

Vue Router 是 Vue.js 官方的路由管理器,它使得构建单页应用(SPA)变得更加容易。在 SSR 环境下,Vue Router 同样扮演着重要的角色,负责管理应用的路由,决定在服务器端渲染哪个组件。

路由配置的差异

在客户端渲染(CSR)的 Vue 应用中,路由配置通常相对简单。例如,一个基本的 Vue Router 配置可能如下:

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

Vue.use(Router);

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

然而,在 SSR 环境下,虽然大部分配置保持一致,但有一些细节需要注意。首先,由于 SSR 是在服务器端进行渲染,服务器端并没有像浏览器那样的原生 history 对象。因此,在 SSR 中使用 history 模式时,需要额外的服务器配置来处理。例如,在 Node.js 中使用 Express 作为服务器框架时,需要配置如下:

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

app.get('*', (req, res) => {
  const context = { url: req.url };
  renderer.renderToString(context, (err, html) => {
    if (err) {
      res.status(500).send('Internal Server Error');
      return;
    }
    res.send(html);
  });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

这里的 app.get('*', ...) 表示捕获所有的请求,然后交给 Vue Server Renderer 进行处理。

路由匹配与数据预取

在 SSR 中,路由匹配的过程需要更加严谨。因为服务器端没有像客户端那样的动态交互环境,一旦路由匹配错误,可能会导致渲染出错误的页面。

动态路由匹配

对于动态路由,例如 /user/:id,在 SSR 中不仅要确保路由能正确匹配,还要考虑如何在服务器端获取到相应的数据。假设我们有一个 User 组件,需要根据 id 获取用户信息并渲染。

import Vue from 'vue';
import Router from 'vue-router';
import User from './views/User.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/user/:id',
      name: 'user',
      component: User
    }
  ]
});

User 组件中,可以使用 asyncData 方法(在 Nuxt.js 等 SSR 框架中有类似的概念)来在服务器端获取数据。

<template>
  <div>
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    };
  },
  async asyncData({ params }) {
    const response = await fetch(`https://api.example.com/users/${params.id}`);
    const user = await response.json();
    return { user };
  }
};
</script>

这样,在服务器端渲染 User 组件时,就会先调用 asyncData 方法获取数据,然后再进行渲染。

嵌套路由

嵌套路由在 SSR 中也需要特别注意。例如,我们有一个文章详情页面,文章有评论等子路由。

import Vue from 'vue';
import Router from 'vue-router';
import Article from './views/Article.vue';
import Comment from './views/Comment.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/article/:id',
      name: 'article',
      component: Article,
      children: [
        {
          path: 'comment',
          name: 'comment',
          component: Comment
        }
      ]
    }
  ]
});

在服务器端渲染时,要确保父组件和子组件的数据预取都能正确进行。父组件 Article 可能需要获取文章内容,子组件 Comment 可能需要获取文章的评论列表。同样,可以通过 asyncData 等方法来实现。

<!-- Article.vue -->
<template>
  <div>
    <h1>{{ article.title }}</h1>
    <p>{{ article.content }}</p>
    <router - view></router - view>
  </div>
</template>

<script>
export default {
  data() {
    return {
      article: null
    };
  },
  async asyncData({ params }) {
    const response = await fetch(`https://api.example.com/articles/${params.id}`);
    const article = await response.json();
    return { article };
  }
};
</script>
<!-- Comment.vue -->
<template>
  <div>
    <h2>Comments</h2>
    <ul>
      <li v - for="comment in comments" :key="comment.id">{{ comment.text }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      comments: []
    };
  },
  async asyncData({ params }) {
    const response = await fetch(`https://api.example.com/articles/${params.id}/comments`);
    const comments = await response.json();
    return { comments };
  }
};
</script>

路由与状态管理

在 SSR 中,路由与状态管理(如 Vuex)紧密相关。当路由发生变化时,可能需要更新 Vuex 中的状态,同时,在服务器端渲染时,也需要根据 Vuex 中的状态来决定如何渲染页面。

路由变化触发状态更新

例如,当用户从首页导航到商品列表页时,我们可能需要更新 Vuex 中的 currentCategory 状态,以显示当前商品所属的类别。

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import ProductList from './views/ProductList.vue';
import store from './store';

Vue.use(Router);

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/products/:category',
      name: 'product - list',
      component: ProductList
    }
  ]
});

router.afterEach((to, from) => {
  if (to.name === 'product - list') {
    store.commit('SET_CURRENT_CATEGORY', to.params.category);
  }
});

export default router;

服务器端状态同步

在服务器端渲染时,需要确保 Vuex 中的状态与客户端保持一致。这通常通过在服务器端将初始状态序列化,然后传递给客户端来实现。

// server.js
const express = require('express');
const app = express();
const { createBundleRenderer } = require('vue - server - renderer');
const serverBundle = require('./dist/vue - ssr - server - bundle.json');
const clientManifest = require('./dist/vue - ssr - client - manifest.json');
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  clientManifest
});
const { createStore } = require('./store');

app.get('*', (req, res) => {
  const store = createStore();
  const context = { url: req.url, store };
  renderer.renderToString(context, (err, html) => {
    if (err) {
      res.status(500).send('Internal Server Error');
      return;
    }
    const initialState = store.state;
    res.send(`
      <!DOCTYPE html>
      <html>
        <head>
          <title>My SSR App</title>
        </head>
        <body>
          <div id="app">${html}</div>
          <script>
            window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
          </script>
          <script src="/dist/vue - ssr - client - bundle.js"></script>
        </body>
      </html>
    `);
  });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

在客户端,需要在 Vue 实例创建之前将初始状态注入到 Vuex 中。

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

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

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

路由过渡与动画

在 SSR 中处理路由过渡与动画也有一些特殊之处。虽然客户端的路由过渡和动画可以正常工作,但在服务器端渲染时,需要注意一些问题。

服务器端渲染与过渡效果

在服务器端渲染时,由于没有真实的 DOM 环境,一些基于 DOM 操作的过渡效果可能无法正常工作。例如,使用 CSS 的 transitionanimation 属性实现的过渡效果,在服务器端渲染时可能不会被正确渲染。

为了解决这个问题,可以采用一些服务器端友好的过渡方案。例如,使用 Vue 的 keep - alive 组件结合 activateddeactivated 钩子函数来实现过渡效果。

<template>
  <div>
    <keep - alive>
      <router - view v - on:activated="onActivate" v - on:deactivated="onDeactivate"></router - view>
    </keep - alive>
  </div>
</template>

<script>
export default {
  methods: {
    onActivate() {
      // 进入路由时的动画逻辑
      this.$el.style.opacity = '0';
      setTimeout(() => {
        this.$el.style.opacity = '1';
      }, 100);
    },
    onDeactivate() {
      // 离开路由时的动画逻辑
      this.$el.style.opacity = '1';
      setTimeout(() => {
        this.$el.style.opacity = '0';
      }, 100);
    }
  }
};
</script>

这样,通过 JavaScript 来控制过渡效果,而不是依赖纯 CSS 的过渡,在服务器端渲染时也能正常工作。

过渡效果的性能优化

在 SSR 中,过渡效果的性能优化也很重要。过多的过渡效果可能会导致服务器端渲染时间变长,影响首屏加载速度。因此,要尽量简化过渡效果,避免复杂的动画计算。

同时,可以采用一些懒加载的方式来加载过渡效果相关的资源。例如,如果使用了第三方的动画库,可以在需要的时候动态加载,而不是在服务器端渲染时就全部加载。

路由错误处理

在 SSR 环境下,路由错误处理同样不可忽视。由于服务器端渲染的特殊性,错误处理不当可能会导致整个页面渲染失败。

404 错误处理

当用户请求一个不存在的路由时,需要返回 404 错误页面。在 Vue Router 中,可以通过 router.onError 方法来全局捕获路由错误,并进行相应的处理。

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import NotFound from './views/NotFound.vue';

Vue.use(Router);

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    }
  ]
});

router.onError((error, to, from) => {
  if (error.name === 'NavigationDuplicated') {
    // 导航重复错误,例如多次点击相同链接
    return;
  }
  // 其他错误,返回 404 页面
  router.push({ name: 'not - found' });
});

router.addRoutes([
  {
    path: '*',
    name: 'not - found',
    component: NotFound
  }
]);

export default router;

在服务器端,需要确保当返回 404 页面时,HTTP 状态码也设置为 404。例如,在 Express 中:

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

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

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

其他路由错误处理

除了 404 错误,还有一些其他类型的路由错误,如导航守卫抛出的错误等。同样可以通过 router.onError 来捕获并处理。

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth &&!isAuthenticated()) {
    // 模拟需要认证的路由,用户未认证时抛出错误
    throw new Error('Unauthorized');
  }
  next();
});

router.onError((error, to, from) => {
  if (error.message === 'Unauthorized') {
    router.push({ name: 'login' });
  }
});

这样,当用户试图访问需要认证的路由但未认证时,会被重定向到登录页面。

路由与 SEO

在 SSR 中,路由对于 SEO 有着重要的影响。合理的路由设计可以提高搜索引擎对页面的抓取和理解。

友好的 URL 结构

搜索引擎更喜欢友好的 URL 结构。例如,使用有意义的单词作为路径,而不是使用无意义的数字或字符。

// 不好的 URL 结构
{
  path: '/p/12345',
  name: 'product',
  component: Product
}

// 好的 URL 结构
{
  path: '/products/apple - iphone - 14',
  name: 'product',
  component: Product
}

这样,搜索引擎更容易理解页面的内容。

元数据与路由

在 Vue Router 中,可以通过 meta 字段来设置路由的元数据,这些元数据可以用于设置页面的标题、描述等 SEO 相关信息。

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

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: {
        title: 'Home Page',
        description: 'This is the home page of our website.'
      }
    },
    {
      path: '/about',
      name: 'about',
      component: About,
      meta: {
        title: 'About Us',
        description: 'Learn more about our company and team.'
      }
    }
  ]
});

在组件中,可以根据这些元数据来动态设置页面的标题和描述。

<template>
  <div>
    <h1>{{ $route.meta.title }}</h1>
    <meta :name="description" :content="$route.meta.description">
  </div>
</template>

<script>
export default {
  data() {
    return {
      description: 'description'
    };
  }
};
</script>

这样,搜索引擎在抓取页面时,可以获取到更准确的页面信息,从而提高页面的搜索排名。

总结

在 Vue Router 的服务端渲染(SSR)中,路由管理涉及多个方面,从基本的路由配置、路由匹配与数据预取,到与状态管理的结合、过渡与动画、错误处理以及 SEO 优化等。每个环节都需要仔细考虑,以确保应用在服务器端和客户端都能正常运行,提供良好的用户体验。通过合理的路由设计和处理,可以充分发挥 SSR 的优势,提高应用的性能和可访问性。在实际开发中,还需要根据项目的具体需求和场景,灵活运用这些注意事项,不断优化和完善路由管理。