组合式API在Nuxt.js中的应用

学习目标

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

  • 了解组合式API的基本概念和设计理念
  • 掌握组合式API在Nuxt.js中的使用方法
  • 理解组合式API与选项式API的区别
  • 了解组合式API的优势和适用场景
  • 掌握组合式API的最佳实践

核心知识点

组合式API的基本概念

组合式API(Composition API)是Vue 3中引入的一种新的API风格,它允许开发者使用函数式的方式组织组件逻辑,而不是通过选项式API(Options API)的方式。

设计理念

组合式API的设计理念主要包括:

  1. 逻辑复用:通过组合函数的方式复用逻辑,而不是通过混入(mixin)或高阶组件
  2. 类型推断:更好的TypeScript支持,提供更准确的类型推断
  3. 代码组织:按功能组织代码,而不是按选项类型组织代码
  4. 响应式系统:使用refreactive等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的优势

  1. 更好的逻辑复用:通过组合函数的方式复用逻辑,避免了混入的命名冲突和来源不明确的问题
  2. 更清晰的代码组织:按功能组织代码,提高代码的可读性和可维护性
  3. 更好的TypeScript支持:提供更准确的类型推断,减少类型错误
  4. 更小的打包体积:可以通过tree-shaking移除未使用的代码
  5. 更灵活的响应式系统:使用refreactive等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
  }
}

响应式数据管理

使用refreactive创建响应式数据:

// 基本类型使用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>

依赖注入

使用provideinject进行依赖注入:

<!-- 父组件 -->
<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>

实用案例分析

案例一:用户认证

功能需求

实现用户认证功能,包括登录、注册、登出和认证状态管理。

实现步骤

  1. 创建认证组合函数
// 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
  }
}
  1. 创建登录页面
<!-- 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>
  1. 创建注册页面
<!-- 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>
  1. 创建导航栏组件
<!-- 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>

案例二:商品列表

功能需求

实现商品列表功能,包括商品展示、分页、搜索和筛选。

实现步骤

  1. 创建商品组合函数
// 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
  }
}
  1. 创建商品列表页面
<!-- 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中的应用,包括:

  1. 组合式API的基本概念:了解了组合式API的设计理念和核心概念
  2. 在Nuxt.js中的使用方法:掌握了组合式API在Nuxt.js中的基本用法和特定API的使用
  3. 与选项式API的对比:理解了组合式API与选项式API在代码组织和逻辑复用方面的区别
  4. 组合式API的优势:了解了组合式API在逻辑复用、代码组织、TypeScript支持等方面的优势
  5. 最佳实践:掌握了组合式API的最佳实践,包括逻辑分离、响应式数据管理、生命周期钩子和依赖注入

通过本章节的学习,你应该能够熟练使用组合式API开发Nuxt.js应用,并能够充分利用组合式API的优势提高开发效率和代码质量。

« 上一篇 Nuxt.js核心功能项目实战 下一篇 » Nuxt.js自定义服务器中间件