Skip to content

第10章:Nuxt 生态集成

1. 样式解决方案(CSS 预处理器:SCSS、Less;CSS 模块化)

1.1 SCSS 预处理器

安装

bash
# 使用 npm
npm install --save-dev sass

# 使用 pnpm
pnpm add --save-dev sass

# 使用 yarn
yarn add --dev sass

使用

vue
<template>
  <div class="container">
    <h1>SCSS Example</h1>
    <p class="text">This is a paragraph</p>
  </div>
</template>

<style lang="scss" scoped>
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
  
  h1 {
    color: #333;
    font-size: 2rem;
  }
  
  .text {
    color: #666;
    font-size: 1rem;
    margin-top: 1rem;
  }
}
</style>

1.2 Less 预处理器

安装

bash
# 使用 npm
npm install --save-dev less

# 使用 pnpm
pnpm add --save-dev less

# 使用 yarn
yarn add --dev less

使用

vue
<template>
  <div class="container">
    <h1>Less Example</h1>
    <p class="text">This is a paragraph</p>
  </div>
</template>

<style lang="less" scoped>
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
  
  h1 {
    color: #333;
    font-size: 2rem;
  }
  
  .text {
    color: #666;
    font-size: 1rem;
    margin-top: 1rem;
  }
}
</style>

1.3 CSS 模块化

基本用法

vue
<template>
  <div class="container">
    <h1>CSS Modules Example</h1>
    <p :class="styles.text">This is a paragraph</p>
  </div>
</template>

<script setup>
import styles from './styles.module.css'
</script>

<style module>
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.text {
  color: #666;
  font-size: 1rem;
  margin-top: 1rem;
}
</style>

使用外部 CSS 模块

css
/* styles.module.css */
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.text {
  color: #666;
  font-size: 1rem;
  margin-top: 1rem;
}
vue
<template>
  <div class="container">
    <h1>CSS Modules Example</h1>
    <p :class="styles.text">This is a paragraph</p>
  </div>
</template>

<script setup>
import styles from './styles.module.css'
</script>

<style>
.container {
  padding: 2rem;
  background-color: #f8f9fa;
  border-radius: 8px;
}
</style>

2. 接口请求集成(Axios 封装、接口统一管理)

2.1 Axios 封装

安装

bash
# 使用 npm
npm install axios

# 使用 pnpm
pnpm add axios

# 使用 yarn
yarn add axios

创建 composables/useAxios.ts

typescript
// composables/useAxios.ts
import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
axiosInstance.interceptors.request.use(
  (config) => {
    const token = useCookie('token').value
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
axiosInstance.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    if (error.response?.status === 401) {
      // 未授权,跳转到登录页
      navigateTo('/login')
    }
    return Promise.reject(error)
  }
)

export function useAxios() {
  return axiosInstance
}

使用

vue
<template>
  <div>
    <h1>Posts</h1>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>
      <ul>
        <li v-for="post in posts" :key="post.id">
          {{ post.title }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
const axios = useAxios()
const posts = ref([])
const loading = ref(false)
const error = ref(null)

async function fetchPosts() {
  loading.value = true
  error.value = null
  try {
    posts.value = await axios.get('/posts')
  } catch (err) {
    error.value = err
  } finally {
    loading.value = false
  }
}

onMounted(fetchPosts)
</script>

2.2 接口统一管理

创建 utils/api.ts

typescript
// utils/api.ts
import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
axiosInstance.interceptors.request.use(
  (config) => {
    const token = useCookie('token').value
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
axiosInstance.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    if (error.response?.status === 401) {
      // 未授权,跳转到登录页
      navigateTo('/login')
    }
    return Promise.reject(error)
  }
)

// 接口定义
export const api = {
  // 认证相关
  auth: {
    login: (data: { email: string; password: string }) => axiosInstance.post('/auth/login', data),
    register: (data: { name: string; email: string; password: string }) => axiosInstance.post('/auth/register', data),
    logout: () => axiosInstance.post('/auth/logout')
  },
  // 文章相关
  posts: {
    getList: (params?: { page?: number; limit?: number }) => axiosInstance.get('/posts', { params }),
    getById: (id: string) => axiosInstance.get(`/posts/${id}`),
    create: (data: { title: string; body: string }) => axiosInstance.post('/posts', data),
    update: (id: string, data: { title?: string; body?: string }) => axiosInstance.put(`/posts/${id}`, data),
    delete: (id: string) => axiosInstance.delete(`/posts/${id}`)
  },
  // 用户相关
  users: {
    getProfile: () => axiosInstance.get('/users/profile'),
    updateProfile: (data: { name?: string; email?: string }) => axiosInstance.put('/users/profile', data)
  }
}

export default api

使用

vue
<template>
  <div>
    <h1>Posts</h1>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>
      <ul>
        <li v-for="post in posts" :key="post.id">
          {{ post.title }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import api from '~/utils/api'
const posts = ref([])
const loading = ref(false)
const error = ref(null)

async function fetchPosts() {
  loading.value = true
  error.value = null
  try {
    posts.value = await api.posts.getList({ page: 1, limit: 10 })
  } catch (err) {
    error.value = err
  } finally {
    loading.value = false
  }
}

onMounted(fetchPosts)
</script>

3. 权限管理(路由守卫、角色权限控制、按钮权限)

3.1 路由守卫

创建 middleware/auth.ts

typescript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('token').value
  
  if (!token && to.path !== '/login') {
    return navigateTo('/login')
  }
  
  if (token && to.path === '/login') {
    return navigateTo('/')
  }
})

在页面中使用

vue
<!-- pages/admin/index.vue -->
<template>
  <div>
    <h1>Admin Panel</h1>
    <!-- 内容 -->
  </div>
</template>

<script setup>
// 内容
</script>

<script>
export default {
  middleware: 'auth'
}
</script>

nuxt.config.ts 中配置全局中间件

typescript
export default defineNuxtConfig({
  router: {
    middleware: ['auth']
  }
})

3.2 角色权限控制

创建 composables/usePermission.ts

typescript
// composables/usePermission.ts
export function usePermission() {
  const user = useState('user', () => null)
  
  function hasRole(role: string) {
    return user.value?.roles?.includes(role) || false
  }
  
  function hasPermission(permission: string) {
    return user.value?.permissions?.includes(permission) || false
  }
  
  return {
    hasRole,
    hasPermission
  }
}

使用

vue
<template>
  <div>
    <h1>Admin Panel</h1>
    <div v-if="hasRole('admin')">
      <h2>Admin Section</h2>
      <!-- 管理员内容 -->
    </div>
    <div v-if="hasPermission('edit-posts')">
      <button @click="editPost">Edit Post</button>
    </div>
  </div>
</template>

<script setup>
const { hasRole, hasPermission } = usePermission()

function editPost() {
  // 编辑文章
}
</script>

3.3 按钮权限

创建 components/ui/AuthorizedButton.vue

vue
<template>
  <button 
    v-if="hasPermission" 
    :class="['btn', `btn-${type}`]"
    @click="$emit('click')"
  >
    <slot />
  </button>
</template>

<script setup>
defineProps({
  permission: {
    type: String,
    required: true
  },
  type: {
    type: String,
    default: 'default'
  }
})

defineEmits(['click'])

const { hasPermission } = usePermission()
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-default {
  background-color: #f8f9fa;
  color: #333;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}
</style>

使用

vue
<template>
  <div>
    <h1>Posts</h1>
    <AuthorizedButton permission="create-posts" type="primary" @click="createPost">
      Create Post
    </AuthorizedButton>
    <AuthorizedButton permission="edit-posts" @click="editPost">
      Edit Post
    </AuthorizedButton>
    <AuthorizedButton permission="delete-posts" type="danger" @click="deletePost">
      Delete Post
    </AuthorizedButton>
  </div>
</template>

<script setup>
function createPost() {
  // 创建文章
}

function editPost() {
  // 编辑文章
}

function deletePost() {
  // 删除文章
}
</script>

4. 日志与错误处理(错误捕获、日志上报、自定义错误页面)

4.1 错误捕获

创建 plugins/error-handler.ts

typescript
// plugins/error-handler.ts
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((nuxtApp) => {
  // 全局错误捕获
  nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
    console.error('Global error:', error)
    console.error('Error info:', info)
    // 可以在这里上报错误
  }
  
  // 未捕获的 Promise 错误
  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled promise rejection:', event.reason)
    // 可以在这里上报错误
  })
  
  // 未捕获的错误
  window.addEventListener('error', (event) => {
    console.error('Uncaught error:', event.error)
    // 可以在这里上报错误
  })
})

4.2 日志上报

创建 utils/logger.ts

typescript
// utils/logger.ts
export const logger = {
  log: (message: string, data?: any) => {
    if (process.env.NODE_ENV === 'development') {
      console.log(message, data)
    }
  },
  error: (message: string, error?: any) => {
    console.error(message, error)
    // 上报错误到日志服务
    // 这里可以集成第三方日志服务,如 Sentry、LogRocket 等
  },
  warn: (message: string, data?: any) => {
    console.warn(message, data)
  }
}

export default logger

使用

vue
<template>
  <div>
    <h1>Posts</h1>
    <!-- 内容 -->
  </div>
</template>

<script setup>
import logger from '~/utils/logger'

async function fetchPosts() {
  try {
    const posts = await api.posts.getList()
    logger.log('Fetched posts:', posts)
  } catch (error) {
    logger.error('Error fetching posts:', error)
  }
}

onMounted(fetchPosts)
</script>

4.3 自定义错误页面

创建 pages/error.vue

vue
<template>
  <div class="error-page">
    <h1>{{ error.statusCode }} - {{ error.statusMessage }}</h1>
    <p>{{ error.message }}</p>
    <NuxtLink to="/">Go back home</NuxtLink>
  </div>
</template>

<script setup>
const error = useError()
</script>

<style scoped>
.error-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 2rem;
  text-align: center;
}

h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
  color: #dc3545;
}

p {
  font-size: 1.2rem;
  margin-bottom: 2rem;
  color: #666;
}

a {
  color: #007bff;
  text-decoration: none;
  font-size: 1.1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #007bff;
  border-radius: 4px;
  transition: all 0.3s;
}

a:hover {
  background-color: #007bff;
  color: white;
}
</style>

使用 useError 处理错误

vue
<template>
  <div>
    <h1>Post Details</h1>
    <div v-if="error">
      <p>Error: {{ error.message }}</p>
      <button @click="clearError">Clear Error</button>
    </div>
    <div v-else-if="loading">Loading...</div>
    <div v-else>
      <h2>{{ post.title }}</h2>
      <p>{{ post.body }}</p>
    </div>
  </div>
</template>

<script setup>
const route = useRoute()
const error = useError()
const loading = ref(true)
const post = ref(null)

async function fetchPost() {
  try {
    post.value = await api.posts.getById(route.params.id)
  } catch (err) {
    error.value = err
  } finally {
    loading.value = false
  }
}

function clearError() {
  clearError()
  fetchPost()
}

onMounted(fetchPost)
</script>

5. 第三方插件集成(如富文本编辑器、图表库、支付插件)

5.1 集成富文本编辑器

安装

bash
# 使用 npm
npm install quill

# 使用 pnpm
pnpm add quill

# 使用 yarn
yarn add quill

创建 components/QuillEditor.vue

vue
<template>
  <div ref="editorRef" class="quill-editor"></div>
</template>

<script setup>
import Quill from 'quill'
import 'quill/dist/quill.snow.css'

const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['update:modelValue'])
const editorRef = ref(null)
let quill = null

onMounted(() => {
  quill = new Quill(editorRef.value, {
    theme: 'snow',
    modules: {
      toolbar: [
        ['bold', 'italic', 'underline', 'strike'],
        ['blockquote', 'code-block'],
        [{ 'header': 1 }, { 'header': 2 }],
        [{ 'list': 'ordered' }, { 'list': 'bullet' }],
        [{ 'indent': '-1' }, { 'indent': '+1' }],
        [{ 'direction': 'rtl' }],
        [{ 'size': ['small', false, 'large', 'huge'] }],
        [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
        [{ 'color': [] }, { 'background': [] }],
        [{ 'font': [] }],
        [{ 'align': [] }],
        ['clean']
      ]
    }
  })
  
  quill.root.innerHTML = props.modelValue
  
  quill.on('text-change', () => {
    emit('update:modelValue', quill.root.innerHTML)
  })
})

watch(() => props.modelValue, (newValue) => {
  if (quill && newValue !== quill.root.innerHTML) {
    quill.root.innerHTML = newValue
  }
})
</script>

<style scoped>
.quill-editor {
  height: 300px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
</style>

使用

vue
<template>
  <div>
    <h1>Create Post</h1>
    <form @submit.prevent="handleSubmit">
      <div>
        <label for="title">Title</label>
        <input type="text" id="title" v-model="form.title" />
      </div>
      <div>
        <label for="content">Content</label>
        <QuillEditor v-model="form.content" />
      </div>
      <button type="submit">Submit</button>
    </form>
  </div>
</template>

<script setup>
const form = ref({
  title: '',
  content: ''
})

function handleSubmit() {
  // 提交表单
  console.log(form.value)
}
</script>

5.2 集成图表库

安装

bash
# 使用 npm
npm install echarts

# 使用 pnpm
pnpm add echarts

# 使用 yarn
yarn add echarts

创建 components/ECharts.vue

vue
<template>
  <div ref="chartRef" class="chart" :style="{ width: width, height: height }"></div>
</template>

<script setup>
import * as echarts from 'echarts'

const props = defineProps({
  options: {
    type: Object,
    required: true
  },
  width: {
    type: String,
    default: '100%'
  },
  height: {
    type: String,
    default: '400px'
  }
})

const chartRef = ref(null)
let chart = null

onMounted(() => {
  chart = echarts.init(chartRef.value)
  chart.setOption(props.options)
  
  window.addEventListener('resize', () => {
    chart.resize()
  })
})

watch(() => props.options, (newOptions) => {
  if (chart) {
    chart.setOption(newOptions)
  }
}, { deep: true })

onUnmounted(() => {
  if (chart) {
    chart.dispose()
  }
  window.removeEventListener('resize', () => {
    chart.resize()
  })
})
</script>

<style scoped>
.chart {
  margin: 20px 0;
}
</style>

使用

vue
<template>
  <div>
    <h1>Chart Example</h1>
    <ECharts :options="chartOptions" />
  </div>
</template>

<script setup>
const chartOptions = ref({
  title: {
    text: 'Sales Data'
  },
  tooltip: {
    trigger: 'axis'
  },
  xAxis: {
    type: 'category',
    data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
  },
  yAxis: {
    type: 'value'
  },
  series: [{
    data: [120, 200, 150, 80, 70, 110],
    type: 'bar'
  }]
})
</script>

5.3 集成支付插件

安装

bash
# 使用 npm
npm install stripe

# 使用 pnpm
pnpm add stripe

# 使用 yarn
yarn add stripe

创建 utils/payment.ts

typescript
// utils/payment.ts
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20'
})

export const payment = {
  createPaymentIntent: async (amount: number, currency: string = 'usd') => {
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency
    })
    return paymentIntent.client_secret
  },
  // 其他支付相关方法
}

export default payment

使用

vue
<template>
  <div>
    <h1>Checkout</h1>
    <div v-if="loading">Loading...</div>
    <div v-else>
      <div class="product">
        <h2>Product Name</h2>
        <p>Price: ${{ product.price }}</p>
      </div>
      <button @click="handleCheckout">Checkout</button>
      <div id="stripe-element"></div>
    </div>
  </div>
</template>

<script setup>
import { loadStripe } from '@stripe/stripe-js'
import payment from '~/utils/payment'

const product = ref({ price: 1000 })
const loading = ref(false)

async function handleCheckout() {
  loading.value = true
  try {
    const clientSecret = await payment.createPaymentIntent(product.value.price)
    const stripe = await loadStripe(process.env.STRIPE_PUBLIC_KEY)
    const result = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: document.getElementById('stripe-element')
      }
    })
    if (result.error) {
      console.error(result.error)
    } else {
      console.log('Payment successful')
    }
  } catch (error) {
    console.error(error)
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  // 初始化 Stripe 元素
  // 这里需要根据 Stripe 文档进行初始化
})
</script>

小结

本章介绍了 Nuxt.js 的生态集成,包括样式解决方案、接口请求集成、权限管理、日志与错误处理以及第三方插件集成等内容。通过本章的学习,你应该已经掌握了如何在 Nuxt.js 中集成各种生态工具和插件,以及如何处理错误和日志。

在接下来的章节中,我们将学习 Nuxt.js 的基础实战项目、企业级进阶实战等内容,帮助你更深入地理解和使用 Nuxt.js。

© 2026 编程马·菜鸟教程 版权所有