Nuxt.js 错误处理机制

学习目标

通过本章节的学习,你将能够:

  • 掌握 Nuxt.js 中创建自定义错误页面的方法
  • 理解如何捕获和处理应用中的错误
  • 学会创建和配置 404 页面
  • 了解服务器端和客户端错误的处理方式
  • 掌握错误处理的最佳实践

核心知识点讲解

错误处理的重要性

在任何应用中,错误处理都是一项重要的功能。良好的错误处理可以:

  • 提高应用的稳定性和可靠性
  • 改善用户体验,避免用户看到技术性错误信息
  • 帮助开发者快速定位和修复问题
  • 提供有意义的错误反馈给用户

Nuxt.js 中的错误处理机制

Nuxt.js 提供了多种错误处理机制,包括:

  1. 错误页面:用于显示应用中的错误信息
  2. 错误捕获:用于捕获和处理应用中的错误
  3. 404 页面:用于处理不存在的页面请求
  4. 服务器错误处理:用于处理服务器端的错误
  5. 客户端错误处理:用于处理客户端的错误

创建自定义错误页面

Nuxt.js 提供了一个默认的错误页面,但你可以创建自定义的错误页面来提供更好的用户体验。

步骤

  1. layouts 目录下创建 error.vue 文件
  2. 实现自定义的错误页面组件

示例

<template>
  <div class="error-page">
    <h1 v-if="error.statusCode === 404">页面不存在</h1>
    <h1 v-else>服务器错误</h1>
    <p v-if="error.statusCode === 404">抱歉,你访问的页面不存在。</p>
    <p v-else>抱歉,服务器发生了错误。</p>
    <nuxt-link to="/">返回首页</nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'ErrorPage',
  props: ['error'],
  layout: 'blank' // 使用空白布局,避免错误页面本身出现布局错误
}
</script>

<style scoped>
.error-page {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  text-align: center;
}

h1 {
  font-size: 36px;
  margin-bottom: 20px;
  color: #ff4d4f;
}

p {
  font-size: 18px;
  margin-bottom: 30px;
  color: #666;
}

a {
  display: inline-block;
  padding: 10px 20px;
  background-color: #1890ff;
  color: white;
  text-decoration: none;
  border-radius: 4px;
}

a:hover {
  background-color: #40a9ff;
}
</style>

错误捕获和处理

客户端错误捕获

在客户端,你可以使用 error 方法来捕获和处理错误:

<template>
  <div>
    <h1>用户列表</h1>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">
      <p>{{ error.message }}</p>
      <button @click="retry">重试</button>
    </div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'UserListPage',
  data() {
    return {
      users: [],
      loading: true,
      error: null
    }
  },
  async fetch() {
    this.loading = true
    this.error = null
    try {
      this.users = await this.$axios.$get('/api/users')
    } catch (error) {
      this.error = error
      // 可以在这里添加错误日志记录
      console.error('获取用户列表失败:', error)
    } finally {
      this.loading = false
    }
  },
  methods: {
    retry() {
      this.fetch()
    }
  }
}
</script>

服务器端错误捕获

在服务器端,你可以在 asyncData 方法中捕获错误:

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <div v-html="post.content"></div>
  </div>
</template>

<script>
export default {
  name: 'PostPage',
  async asyncData({ params, $axios, error }) {
    try {
      const post = await $axios.$get(`/api/posts/${params.id}`)
      return { post }
    } catch (err) {
      // 使用 Nuxt.js 提供的 error 方法处理错误
      error({
        statusCode: err.response ? err.response.status : 500,
        message: err.message || '获取文章失败'
      })
      return {}
    }
  }
}
</script>

404 页面

Nuxt.js 会自动处理 404 错误,当用户访问不存在的页面时,会显示错误页面并将 error.statusCode 设置为 404。

你可以在错误页面中根据 error.statusCode 来显示不同的内容:

<template>
  <div class="error-page">
    <div v-if="error.statusCode === 404" class="not-found">
      <h1>404 - 页面不存在</h1>
      <p>抱歉,你访问的页面不存在。</p>
      <p>可能的原因:</p>
      <ul>
        <li>URL 输入错误</li>
        <li>页面已被移除</li>
        <li>链接已失效</li>
      </ul>
    </div>
    <div v-else class="server-error">
      <h1>服务器错误</h1>
      <p>抱歉,服务器发生了错误。</p>
      <p>错误代码:{{ error.statusCode }}</p>
      <p v-if="error.message">{{ error.message }}</p>
    </div>
    <nuxt-link to="/">返回首页</nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'ErrorPage',
  props: ['error']
}
</script>

<style scoped>
.error-page {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  text-align: center;
}

.not-found h1 {
  font-size: 48px;
  margin-bottom: 20px;
  color: #ff4d4f;
}

.server-error h1 {
  font-size: 36px;
  margin-bottom: 20px;
  color: #faad14;
}

p {
  font-size: 18px;
  margin-bottom: 15px;
  color: #666;
}

ul {
  text-align: left;
  max-width: 400px;
  margin: 0 auto 30px;
  color: #666;
}

li {
  margin-bottom: 10px;
}

a {
  display: inline-block;
  padding: 10px 20px;
  background-color: #1890ff;
  color: white;
  text-decoration: none;
  border-radius: 4px;
  margin-top: 20px;
}

a:hover {
  background-color: #40a9ff;
}
</style>

服务器端错误处理

在 Nuxt.js 中,你可以通过以下方式处理服务器端错误:

  1. nuxt.config.js 中配置错误处理
  2. 使用中间件处理错误
  3. 在 API 路由中处理错误

示例:在 nuxt.config.js 中配置错误处理

export default {
  serverMiddleware: [
    // 自定义服务器中间件,用于处理服务器端错误
    '~/serverMiddleware/errorHandler.js'
  ]
}

示例:创建服务器中间件处理错误

// serverMiddleware/errorHandler.js
export default function errorHandler(req, res, next) {
  try {
    next()
  } catch (error) {
    console.error('服务器错误:', error)
    res.status(500).json({
      error: {
        message: '服务器内部错误',
        statusCode: 500
      }
    })
  }
}

客户端错误处理

在客户端,你可以通过以下方式处理错误:

  1. 使用 Vue 的错误处理机制
  2. 使用全局错误处理器
  3. 使用第三方错误监控服务

示例:在 plugins 目录下创建全局错误处理器

// plugins/errorHandler.js
export default function({ app, error: nuxtError }) {
  // Vue 全局错误处理器
  app.config.errorHandler = (err, vm, info) => {
    console.error('Vue 错误:', err)
    console.error('错误信息:', info)
    // 可以在这里集成错误监控服务,如 Sentry
  }

  // 处理未捕获的 Promise 错误
  window.addEventListener('unhandledrejection', (event) => {
    console.error('未处理的 Promise 错误:', event.reason)
  })

  // 处理未捕获的错误
  window.addEventListener('error', (event) => {
    console.error('未捕获的错误:', event.error)
  })
}

注册插件

// nuxt.config.js
export default {
  plugins: [
    '~/plugins/errorHandler.js'
  ]
}

实用案例分析

案例一:电商网站的错误处理

场景描述:在电商网站中,需要处理各种错误情况,如商品不存在、库存不足、支付失败等,以提供良好的用户体验。

实现方案

<template>
  <div class="product-page">
    <div v-if="loading" class="loading">
      <div class="spinner"></div>
      <p>加载中...</p>
    </div>
    <div v-else-if="error" class="error">
      <h2>{{ error.message }}</h2>
      <p v-if="error.statusCode === 404">该商品不存在或已下架</p>
      <p v-else-if="error.statusCode === 403">你没有权限查看该商品</p>
      <p v-else>加载商品信息失败,请稍后重试</p>
      <button class="retry-btn" @click="retry">重试</button>
      <nuxt-link to="/" class="home-btn">返回首页</nuxt-link>
    </div>
    <div v-else class="product-info">
      <h1>{{ product.name }}</h1>
      <div class="price">¥{{ product.price }}</div>
      <div class="stock" :class="{ 'out-of-stock': product.stock === 0 }">
        库存:{{ product.stock }} 件
      </div>
      <p class="description">{{ product.description }}</p>
      <button 
        class="add-to-cart" 
        :disabled="product.stock === 0"
        @click="addToCart"
      >
        {{ product.stock === 0 ? '库存不足' : '加入购物车' }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ProductPage',
  data() {
    return {
      product: null,
      loading: true,
      error: null
    }
  },
  async fetch() {
    this.loading = true
    this.error = null
    try {
      this.product = await this.$axios.$get(`/api/products/${this.$route.params.id}`)
    } catch (error) {
      this.error = {
        statusCode: error.response ? error.response.status : 500,
        message: error.message || '加载商品信息失败'
      }
      // 记录错误日志
      console.error('获取商品信息失败:', error)
    } finally {
      this.loading = false
    }
  },
  methods: {
    retry() {
      this.fetch()
    },
    async addToCart() {
      if (this.product.stock === 0) return
      
      try {
        await this.$axios.$post('/api/cart/add', {
          productId: this.product.id,
          quantity: 1
        })
        this.$toast.success('加入购物车成功')
      } catch (error) {
        this.$toast.error('加入购物车失败,请稍后重试')
        console.error('加入购物车失败:', error)
      }
    }
  }
}
</script>

<style scoped>
.product-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 400px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 20px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.error {
  background-color: #fff2f0;
  border: 1px solid #ffccc7;
  border-radius: 4px;
  padding: 30px;
  text-align: center;
  margin: 40px 0;
}

.error h2 {
  color: #ff4d4f;
  margin-bottom: 15px;
}

.error p {
  color: #666;
  margin-bottom: 20px;
}

.retry-btn, .home-btn {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 4px;
  text-decoration: none;
  margin: 0 10px;
}

.retry-btn {
  background-color: #1890ff;
  color: white;
  border: none;
  cursor: pointer;
}

.retry-btn:hover {
  background-color: #40a9ff;
}

.home-btn {
  background-color: #f0f0f0;
  color: #333;
}

.home-btn:hover {
  background-color: #e0e0e0;
}

.product-info {
  display: flex;
  gap: 40px;
}

.price {
  font-size: 24px;
  font-weight: bold;
  color: #ff4d4f;
  margin: 20px 0;
}

.stock {
  margin-bottom: 20px;
  padding: 5px 10px;
  border-radius: 4px;
  background-color: #f6ffed;
  color: #52c41a;
}

.out-of-stock {
  background-color: #fff2f0;
  color: #ff4d4f;
}

.description {
  line-height: 1.6;
  margin-bottom: 30px;
}

.add-to-cart {
  background-color: #1890ff;
  color: white;
  border: none;
  padding: 12px 24px;
  font-size: 16px;
  border-radius: 4px;
  cursor: pointer;
}

.add-to-cart:hover:not(:disabled) {
  background-color: #40a9ff;
}

.add-to-cart:disabled {
  background-color: #f0f0f0;
  color: #999;
  cursor: not-allowed;
}
</style>

案例二:API 错误处理

场景描述:在调用 API 时,需要处理各种错误情况,如网络错误、服务器错误、认证错误等,并向用户提供有意义的错误反馈。

实现方案

// plugins/api.js
import axios from 'axios'

export default function({ app, store, error: nuxtError, redirect }) {
  // 创建 axios 实例
  const api = axios.create({
    baseURL: process.env.API_BASE_URL || '/api',
    timeout: 10000
  })

  // 请求拦截器
  api.interceptors.request.use(
    (config) => {
      // 添加认证 token
      const token = store.state.auth.token
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }
      return config
    },
    (error) => {
      return Promise.reject(error)
    }
  )

  // 响应拦截器
  api.interceptors.response.use(
    (response) => {
      return response.data
    },
    (error) => {
      // 处理常见错误
      if (error.response) {
        switch (error.response.status) {
          case 401:
            // 未认证,重定向到登录页
            store.commit('auth/LOGOUT')
            redirect('/login')
            break
          case 403:
            // 禁止访问
            nuxtError({
              statusCode: 403,
              message: '你没有权限执行此操作'
            })
            break
          case 404:
            // 资源不存在
            nuxtError({
              statusCode: 404,
              message: '请求的资源不存在'
            })
            break
          case 500:
            // 服务器错误
            nuxtError({
              statusCode: 500,
              message: '服务器内部错误,请稍后重试'
            })
            break
          default:
            // 其他错误
            nuxtError({
              statusCode: error.response.status,
              message: error.response.data.message || '请求失败'
            })
        }
      } else if (error.request) {
        // 网络错误
        nuxtError({
          statusCode: 0,
          message: '网络错误,请检查你的网络连接'
        })
      } else {
        // 其他错误
        nuxtError({
          statusCode: 500,
          message: error.message || '请求失败'
        })
      }
      return Promise.reject(error)
    }
  )

  // 将 api 实例添加到 Vue 原型
  app.config.globalProperties.$api = api
  // 将 api 实例添加到上下文
  app.provide('api', api)
}

使用示例

<template>
  <div class="login-page">
    <h1>登录</h1>
    <form @submit.prevent="login">
      <div class="form-group">
        <label for="email">邮箱</label>
        <input 
          type="email" 
          id="email" 
          v-model="form.email" 
          required
        >
      </div>
      <div class="form-group">
        <label for="password">密码</label>
        <input 
          type="password" 
          id="password" 
          v-model="form.password" 
          required
        >
      </div>
      <div v-if="error" class="error-message">
        {{ error }}
      </div>
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
  </div>
</template>

<script>
export default {
  name: 'LoginPage',
  data() {
    return {
      form: {
        email: '',
        password: ''
      },
      loading: false,
      error: null
    }
  },
  methods: {
    async login() {
      this.loading = true
      this.error = null
      try {
        const response = await this.$api.post('/auth/login', this.form)
        this.$store.commit('auth/LOGIN', response.token)
        this.$router.push('/')
      } catch (error) {
        this.error = error.response?.data?.message || '登录失败'
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

<style scoped>
.login-page {
  max-width: 400px;
  margin: 0 auto;
  padding: 40px 20px;
}

.form-group {
  margin-bottom: 20px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.error-message {
  background-color: #fff2f0;
  color: #ff4d4f;
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 20px;
}

button {
  width: 100%;
  padding: 12px;
  background-color: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}

button:hover:not(:disabled) {
  background-color: #40a9ff;
}

button:disabled {
  background-color: #f0f0f0;
  color: #999;
  cursor: not-allowed;
}
</style>

错误处理最佳实践

1. 错误分类

  • 用户错误:如输入无效、操作不当等,应向用户提供清晰的错误信息
  • 系统错误:如服务器故障、网络错误等,应向用户提供友好的错误提示
  • 编程错误:如代码 bug、逻辑错误等,应记录详细的错误信息并修复

2. 错误反馈

  • 用户界面:显示友好的错误信息,避免显示技术性错误
  • 日志记录:记录详细的错误信息,便于调试和分析
  • 监控告警:对于严重错误,应设置监控和告警机制

3. 错误恢复

  • 重试机制:对于网络错误等临时性问题,可提供重试选项
  • 降级方案:当某些功能不可用时,提供替代方案
  • 自动恢复:对于一些轻微错误,可尝试自动恢复

4. 性能考虑

  • 错误处理代码应简洁高效:避免在错误处理中执行复杂操作
  • 避免过多的错误日志:只记录必要的错误信息
  • 考虑错误处理的性能开销:尤其是在高并发场景下

总结

错误处理是 Nuxt.js 应用中一项重要的功能。通过本章节的学习,你已经掌握了:

  • 在 Nuxt.js 中创建自定义错误页面的方法
  • 如何捕获和处理应用中的错误
  • 如何创建和配置 404 页面
  • 服务器端和客户端错误的处理方式
  • 错误处理的最佳实践

良好的错误处理可以提高应用的稳定性和可靠性,改善用户体验,帮助开发者快速定位和修复问题。在实际开发中,你应该根据应用的具体需求,选择合适的错误处理策略,并不断优化和完善错误处理机制。

« 上一篇 Nuxt.js 元信息管理 下一篇 » Nuxt.js 国际化支持