Appearance
第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。
