组合式API在Nuxt.js中的应用
学习目标
通过本章节的学习,你将能够:
- 了解组合式API的基本概念和设计理念
- 掌握组合式API在Nuxt.js中的使用方法
- 理解组合式API与选项式API的区别
- 了解组合式API的优势和适用场景
- 掌握组合式API的最佳实践
核心知识点
组合式API的基本概念
组合式API(Composition API)是Vue 3中引入的一种新的API风格,它允许开发者使用函数式的方式组织组件逻辑,而不是通过选项式API(Options API)的方式。
设计理念
组合式API的设计理念主要包括:
- 逻辑复用:通过组合函数的方式复用逻辑,而不是通过混入(mixin)或高阶组件
- 类型推断:更好的TypeScript支持,提供更准确的类型推断
- 代码组织:按功能组织代码,而不是按选项类型组织代码
- 响应式系统:使用
ref和reactive等API创建响应式数据
在Nuxt.js中的使用方法
基本用法
在Nuxt.js 3中,组合式API是默认的API风格,你可以在组件中直接使用:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="increment">点击计数</button>
<p>计数:{{ count }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const title = ref('组合式API示例')
const count = ref(0)
// 计算属性
const message = computed(() => {
return `你已经点击了 ${count.value} 次`
})
// 方法
const increment = () => {
count.value++
}
</script>使用Nuxt.js特定的组合式API
Nuxt.js提供了一些特定的组合式API,用于访问Nuxt.js的功能:
<template>
<div>
<h1>{{ title }}</h1>
<div v-if="pending">加载中...</div>
<div v-else-if="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 { ref } from 'vue'
import { useAsyncData, useRoute, useRouter } from 'nuxt/app'
// 路由相关
const route = useRoute()
const router = useRouter()
// 响应式数据
const title = ref('博客文章')
// 数据获取
const { data: posts, pending, error } = useAsyncData('posts', () => {
return $fetch('/api/posts')
})
// 导航方法
const navigateToPost = (id) => {
router.push(`/posts/${id}`)
}
</script>与选项式API的对比
代码组织方式
选项式API:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="increment">点击计数</button>
<p>计数:{{ count }}</p>
</div>
</template>
<script>
export default {
data() {
return {
title: '选项式API示例',
count: 0
}
},
computed: {
message() {
return `你已经点击了 ${this.count} 次`
}
},
methods: {
increment() {
this.count++
}
}
}
</script>组合式API:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="increment">点击计数</button>
<p>计数:{{ count }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const title = ref('组合式API示例')
const count = ref(0)
// 计算属性
const message = computed(() => {
return `你已经点击了 ${count.value} 次`
})
// 方法
const increment = () => {
count.value++
}
</script>逻辑复用方式
选项式API:使用混入(mixin)
// mixins/counter.js
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}<template>
<div>
<button @click="increment">点击计数</button>
<p>计数:{{ count }}</p>
</div>
</template>
<script>
import counterMixin from '@/mixins/counter'
export default {
mixins: [counterMixin]
}
</script>组合式API:使用组合函数
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const reset = () => {
count.value = initialValue
}
return {
count,
increment,
reset
}
}<template>
<div>
<button @click="increment">点击计数</button>
<button @click="reset">重置</button>
<p>计数:{{ count }}</p>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, increment, reset } = useCounter(0)
</script>组合式API的优势
- 更好的逻辑复用:通过组合函数的方式复用逻辑,避免了混入的命名冲突和来源不明确的问题
- 更清晰的代码组织:按功能组织代码,提高代码的可读性和可维护性
- 更好的TypeScript支持:提供更准确的类型推断,减少类型错误
- 更小的打包体积:可以通过tree-shaking移除未使用的代码
- 更灵活的响应式系统:使用
ref和reactive等API创建响应式数据,提供更灵活的响应式系统
最佳实践
逻辑分离
将相关的逻辑分离到独立的组合函数中:
// composables/useAuth.js
import { ref, computed } from 'vue'
import { useRouter } from 'nuxt/app'
export function useAuth() {
const router = useRouter()
const user = ref(null)
const token = ref(localStorage.getItem('token'))
const isAuthenticated = computed(() => {
return !!token.value
})
const login = async (email, password) => {
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
token.value = response.token
user.value = response.user
localStorage.setItem('token', response.token)
router.push('/')
} catch (error) {
console.error('登录失败:', error)
}
}
const logout = () => {
token.value = null
user.value = null
localStorage.removeItem('token')
router.push('/login')
}
return {
user,
token,
isAuthenticated,
login,
logout
}
}响应式数据管理
使用ref和reactive创建响应式数据:
// 基本类型使用ref
const count = ref(0)
// 复杂类型使用reactive或ref
const user = ref({
name: 'John',
age: 30
})
// 或使用reactive
const user = reactive({
name: 'John',
age: 30
})生命周期钩子
使用组合式API的生命周期钩子:
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, onUpdated } from 'vue'
const title = ref('生命周期钩子示例')
const message = ref('')
const timer = ref(null)
// 组件挂载时
onMounted(() => {
console.log('组件挂载')
message.value = '组件已挂载'
// 设置定时器
timer.value = setInterval(() => {
console.log('定时器执行')
}, 1000)
})
// 组件更新时
onUpdated(() => {
console.log('组件更新')
})
// 组件卸载时
onUnmounted(() => {
console.log('组件卸载')
// 清除定时器
if (timer.value) {
clearInterval(timer.value)
}
})
</script>依赖注入
使用provide和inject进行依赖注入:
<!-- 父组件 -->
<template>
<div>
<h1>父组件</h1>
<ChildComponent />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const count = ref(0)
// 提供数据和方法
provide('count', count)
provide('increment', () => {
count.value++
})
</script><!-- 子组件 -->
<template>
<div>
<h2>子组件</h2>
<p>计数:{{ count }}</p>
<button @click="increment">点击计数</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据和方法
const count = inject('count')
const increment = inject('increment')
</script>实用案例分析
案例一:用户认证
功能需求
实现用户认证功能,包括登录、注册、登出和认证状态管理。
实现步骤
- 创建认证组合函数
// composables/useAuth.js
import { ref, computed } from 'vue'
import { useRouter } from 'nuxt/app'
export function useAuth() {
const router = useRouter()
const user = ref(null)
const token = ref(localStorage.getItem('token'))
const loading = ref(false)
const error = ref(null)
const isAuthenticated = computed(() => {
return !!token.value
})
const login = async (email, password) => {
loading.value = true
error.value = null
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
token.value = response.token
user.value = response.user
localStorage.setItem('token', response.token)
router.push('/')
} catch (err) {
error.value = err.response?.data?.message || '登录失败'
console.error('登录失败:', err)
} finally {
loading.value = false
}
}
const register = async (userData) => {
loading.value = true
error.value = null
try {
const response = await $fetch('/api/auth/register', {
method: 'POST',
body: userData
})
token.value = response.token
user.value = response.user
localStorage.setItem('token', response.token)
router.push('/')
} catch (err) {
error.value = err.response?.data?.message || '注册失败'
console.error('注册失败:', err)
} finally {
loading.value = false
}
}
const logout = () => {
token.value = null
user.value = null
localStorage.removeItem('token')
router.push('/login')
}
const fetchUser = async () => {
if (!token.value) return
loading.value = true
try {
const response = await $fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token.value}`
}
})
user.value = response
} catch (err) {
console.error('获取用户信息失败:', err)
// 如果获取用户信息失败,清除token
token.value = null
localStorage.removeItem('token')
} finally {
loading.value = false
}
}
return {
user,
token,
loading,
error,
isAuthenticated,
login,
register,
logout,
fetchUser
}
}- 创建登录页面
<!-- pages/login.vue -->
<template>
<div class="login">
<h1>登录</h1>
<div v-if="error" class="error">
{{ error }}
</div>
<form @submit.prevent="handleSubmit">
<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>
<button
type="submit"
class="btn"
:disabled="loading"
>
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
<p class="register-link">
还没有账号?<NuxtLink to="/register">立即注册</NuxtLink>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useAuth } from '@/composables/useAuth'
const { login, loading, error } = useAuth()
const form = ref({
email: '',
password: ''
})
const handleSubmit = async () => {
await login(form.value.email, form.value.password)
}
</script>- 创建注册页面
<!-- pages/register.vue -->
<template>
<div class="register">
<h1>注册</h1>
<div v-if="error" class="error">
{{ error }}
</div>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="name">姓名</label>
<input
type="text"
id="name"
v-model="form.name"
required
/>
</div>
<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>
<button
type="submit"
class="btn"
:disabled="loading"
>
{{ loading ? '注册中...' : '注册' }}
</button>
</form>
<p class="login-link">
已有账号?<NuxtLink to="/login">立即登录</NuxtLink>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useAuth } from '@/composables/useAuth'
const { register, loading, error } = useAuth()
const form = ref({
name: '',
email: '',
password: ''
})
const handleSubmit = async () => {
await register(form.value)
}
</script>- 创建导航栏组件
<!-- components/Navbar.vue -->
<template>
<nav class="navbar">
<div class="logo">
<NuxtLink to="/">Logo</NuxtLink>
</div>
<div class="nav-links">
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/about">关于</NuxtLink>
<template v-if="isAuthenticated">
<NuxtLink to="/profile">个人中心</NuxtLink>
<button @click="logout" class="btn">登出</button>
</template>
<template v-else>
<NuxtLink to="/login">登录</NuxtLink>
<NuxtLink to="/register">注册</NuxtLink>
</template>
</div>
</nav>
</template>
<script setup>
import { useAuth } from '@/composables/useAuth'
const { isAuthenticated, logout } = useAuth()
</script>案例二:商品列表
功能需求
实现商品列表功能,包括商品展示、分页、搜索和筛选。
实现步骤
- 创建商品组合函数
// composables/useProducts.js
import { ref, computed } from 'vue'
export function useProducts() {
const products = ref([])
const loading = ref(false)
const error = ref(null)
const currentPage = ref(1)
const pageSize = ref(10)
const searchQuery = ref('')
const category = ref('')
const totalPages = computed(() => {
return Math.ceil(products.value.length / pageSize.value)
})
const filteredProducts = computed(() => {
let result = [...products.value]
// 搜索筛选
if (searchQuery.value) {
result = result.filter(product =>
product.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
product.description.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
// 分类筛选
if (category.value) {
result = result.filter(product => product.category === category.value)
}
return result
})
const paginatedProducts = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredProducts.value.slice(start, end)
})
const fetchProducts = async () => {
loading.value = true
error.value = null
try {
const response = await $fetch('/api/products', {
params: {
page: currentPage.value,
pageSize: pageSize.value,
search: searchQuery.value,
category: category.value
}
})
products.value = response.data
} catch (err) {
error.value = '获取商品列表失败'
console.error('获取商品列表失败:', err)
} finally {
loading.value = false
}
}
const setPage = (page) => {
currentPage.value = page
fetchProducts()
}
const setSearchQuery = (query) => {
searchQuery.value = query
currentPage.value = 1 // 重置到第一页
fetchProducts()
}
const setCategory = (cat) => {
category.value = cat
currentPage.value = 1 // 重置到第一页
fetchProducts()
}
return {
products,
loading,
error,
currentPage,
pageSize,
searchQuery,
category,
totalPages,
filteredProducts,
paginatedProducts,
fetchProducts,
setPage,
setSearchQuery,
setCategory
}
}- 创建商品列表页面
<!-- pages/products/index.vue -->
<template>
<div class="products">
<h1>商品列表</h1>
<!-- 搜索和筛选 -->
<div class="filters">
<div class="search">
<input
type="text"
v-model="localSearchQuery"
@input="handleSearch"
placeholder="搜索商品..."
/>
</div>
<div class="category-filter">
<select v-model="localCategory" @change="handleCategoryChange">
<option value="">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="furniture">家具</option>
</select>
</div>
</div>
<!-- 商品列表 -->
<div v-if="loading" class="loading">
加载中...
</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else class="products-grid">
<div v-for="product in paginatedProducts" :key="product.id" class="product-card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<p class="price">{{ product.price }}</p>
<NuxtLink to="/products/${product.id}" class="btn">查看详情</NuxtLink>
</div>
</div>
<!-- 分页 -->
<div v-if="!loading && totalPages > 1" class="pagination">
<button
@click="setPage(currentPage - 1)"
:disabled="currentPage === 1"
>
上一页
</button>
<span v-for="page in totalPages" :key="page" class="page-number">
<button
@click="setPage(page)"
:class="{ active: currentPage === page }"
>
{{ page }}
</button>
</span>
<button
@click="setPage(currentPage + 1)"
:disabled="currentPage === totalPages"
>
下一页
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useProducts } from '@/composables/useProducts'
const {
loading,
error,
currentPage,
totalPages,
paginatedProducts,
fetchProducts,
setPage,
setSearchQuery,
setCategory
} = useProducts()
const localSearchQuery = ref('')
const localCategory = ref('')
// 处理搜索
const handleSearch = () => {
setSearchQuery(localSearchQuery.value)
}
// 处理分类变化
const handleCategoryChange = () => {
setCategory(localCategory.value)
}
// 初始加载
onMounted(() => {
fetchProducts()
})
</script>总结
本章节介绍了组合式API在Nuxt.js中的应用,包括:
- 组合式API的基本概念:了解了组合式API的设计理念和核心概念
- 在Nuxt.js中的使用方法:掌握了组合式API在Nuxt.js中的基本用法和特定API的使用
- 与选项式API的对比:理解了组合式API与选项式API在代码组织和逻辑复用方面的区别
- 组合式API的优势:了解了组合式API在逻辑复用、代码组织、TypeScript支持等方面的优势
- 最佳实践:掌握了组合式API的最佳实践,包括逻辑分离、响应式数据管理、生命周期钩子和依赖注入
通过本章节的学习,你应该能够熟练使用组合式API开发Nuxt.js应用,并能够充分利用组合式API的优势提高开发效率和代码质量。