Appearance
第6章:数据获取与状态管理
1. 三种数据获取方式(新手循序渐进)
1.1 方式1:useAsyncData(核心,用于异步获取数据,支持 SSR)
基本用法:
vue
<template>
<div>
<h1>Posts</h1>
<div v-if="pending">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 { data: posts, pending, error } = useAsyncData('posts', () => {
return $fetch('/api/posts')
})
</script>带参数的使用:
vue
<template>
<div>
<h1>Post Details</h1>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { data: post, pending, error } = useAsyncData(`post-${route.params.id}`, () => {
return $fetch(`/api/posts/${route.params.id}`)
})
</script>带选项的使用:
vue
<template>
<div>
<h1>Posts</h1>
<!-- 内容 -->
</div>
</template>
<script setup>
const { data: posts, pending, error, refresh } = useAsyncData('posts', () => {
return $fetch('/api/posts')
}, {
server: true, // 在服务端获取数据
lazy: false, // 非懒加载
refreshInterval: 10000, // 每10秒刷新一次
initialCache: false, // 不使用初始缓存
watch: [/* 依赖项 */] // 依赖项变化时重新获取数据
})
// 手动刷新数据
function handleRefresh() {
refresh()
}
</script>1.2 方式2:useFetch(简化版 useAsyncData,直接请求接口)
基本用法:
vue
<template>
<div>
<h1>Posts</h1>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<ul>
<li v-for="post in data" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const { data, pending, error } = useFetch('/api/posts')
</script>带参数的使用:
vue
<template>
<div>
<h1>Post Details</h1>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<h2>{{ data.title }}</h2>
<p>{{ data.body }}</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { data, pending, error } = useFetch(`/api/posts/${route.params.id}`)
</script>带选项的使用:
vue
<template>
<div>
<h1>Posts</h1>
<!-- 内容 -->
</div>
</template>
<script setup>
const { data, pending, error, refresh } = useFetch('/api/posts', {
method: 'get',
params: { page: 1, limit: 10 },
server: true,
lazy: false
})
</script>1.3 方式3:useLazyFetch / useLazyAsyncData(懒加载数据)
基本用法:
vue
<template>
<div>
<h1>Posts</h1>
<div v-if="!data">Loading...</div>
<div v-else>
<ul>
<li v-for="post in data" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const { data } = useLazyFetch('/api/posts')
</script>useLazyAsyncData:
vue
<template>
<div>
<h1>Posts</h1>
<div v-if="!posts">Loading...</div>
<div v-else>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const { data: posts } = useLazyAsyncData('posts', () => {
return $fetch('/api/posts')
})
</script>2. 数据请求封装(统一请求实例、请求拦截器、响应拦截器)
2.1 创建请求实例
创建 composables/useApi.ts:
typescript
// composables/useApi.ts
import { $fetch, type FetchOptions } from 'ofetch'
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
const api = $fetch.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
export function useApi() {
return {
get: <T>(url: string, options?: FetchOptions) => {
return api<T>(url, { ...options, method: 'get' })
},
post: <T>(url: string, body?: any, options?: FetchOptions) => {
return api<T>(url, { ...options, method: 'post', body })
},
put: <T>(url: string, body?: any, options?: FetchOptions) => {
return api<T>(url, { ...options, method: 'put', body })
},
delete: <T>(url: string, options?: FetchOptions) => {
return api<T>(url, { ...options, method: 'delete' })
}
}
}2.2 请求拦截器
修改 composables/useApi.ts:
typescript
// composables/useApi.ts
import { $fetch, type FetchOptions } from 'ofetch'
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
const api = $fetch.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
onRequest({ options }) {
// 添加 token
const token = useCookie('token').value
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`
}
}
}
})
// 其余代码不变2.3 响应拦截器
修改 composables/useApi.ts:
typescript
// composables/useApi.ts
import { $fetch, type FetchOptions } from 'ofetch'
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
const api = $fetch.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
onRequest({ options }) {
// 添加 token
const token = useCookie('token').value
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`
}
}
},
onResponse({ response }) {
// 统一处理响应
return response._data
},
onResponseError({ response }) {
// 统一处理错误
if (response.status === 401) {
// 未授权,跳转到登录页
navigateTo('/login')
}
throw response._data
}
})
// 其余代码不变2.4 使用封装的请求
示例:
vue
<template>
<div>
<h1>Posts</h1>
<div v-if="pending">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 { get } = useApi()
const { data: posts, pending, error } = useAsyncData('posts', () => {
return get('/posts')
})
</script>3. 状态管理(Nuxt 内置状态管理)
3.1 简单状态共享(useState 核心用法,响应式、服务端兼容)
基本用法:
vue
<template>
<div>
<h1>Counter: {{ count }}</h1>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup>
const count = useState('count', () => 0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
</script>在多个组件中共享:
vue
<!-- ComponentA.vue -->
<template>
<div>
<h1>Component A</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
const count = useState('count', () => 0)
function increment() {
count.value++
}
</script>
<!-- ComponentB.vue -->
<template>
<div>
<h1>Component B</h1>
<p>Count: {{ count }}</p>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup>
const count = useState('count', () => 0)
function decrement() {
count.value--
}
</script>3.2 复杂状态管理(Pinia 集成 Nuxt,实战场景)
3.2.1 安装 Pinia
bash
# 使用 npm
npm install pinia @pinia/nuxt
# 使用 pnpm
pnpm add pinia @pinia/nuxt
# 使用 yarn
yarn add pinia @pinia/nuxt3.2.2 配置 Pinia
在 nuxt.config.ts 中添加模块:
typescript
export default defineNuxtConfig({
modules: [
'@pinia/nuxt'
]
})3.2.3 创建 Store
创建 stores/counter.ts:
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Nuxt'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
reset() {
this.count = 0
}
}
})3.2.4 使用 Store
示例:
vue
<template>
<div>
<h1>Counter: {{ counter.count }}</h1>
<p>Double count: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
<button @click="counter.reset">Reset</button>
</div>
</template>
<script setup>
const counter = useCounterStore()
</script>3.2.5 异步 Action
示例:
typescript
// stores/post.ts
import { defineStore } from 'pinia'
export const usePostStore = defineStore('post', {
state: () => ({
posts: [],
loading: false,
error: null
}),
actions: {
async fetchPosts() {
this.loading = true
this.error = null
try {
const { get } = useApi()
this.posts = await get('/posts')
} catch (error) {
this.error = error
} finally {
this.loading = false
}
}
}
})使用:
vue
<template>
<div>
<h1>Posts</h1>
<button @click="fetchPosts">Fetch Posts</button>
<div v-if="postStore.loading">Loading...</div>
<div v-else-if="postStore.error">Error: {{ postStore.error.message }}</div>
<div v-else>
<ul>
<li v-for="post in postStore.posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const postStore = usePostStore()
function fetchPosts() {
postStore.fetchPosts()
}
</script>4. 本地存储持久化(useCookie、localStorage 封装)
4.1 使用 useCookie
基本用法:
vue
<template>
<div>
<h1>Welcome {{ username }}</h1>
<input v-model="username" placeholder="Enter your name" />
</div>
</template>
<script setup>
const username = useCookie('username', {
default: 'Guest',
maxAge: 60 * 60 * 24 * 7 // 7天
})
</script>4.2 使用 localStorage
创建 composables/useLocalStorage.ts:
typescript
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = localStorage.getItem(key)
const value = ref<T>(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}使用:
vue
<template>
<div>
<h1>Counter: {{ count }}</h1>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup>
const count = useLocalStorage('count', 0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
</script>4.3 结合状态管理使用
示例:
typescript
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: useCookie('user').value || null
}),
actions: {
login(userData) {
this.user = userData
useCookie('user', { maxAge: 60 * 60 * 24 * 7 }).value = userData
},
logout() {
this.user = null
useCookie('user').value = null
}
}
})小结
本章介绍了 Nuxt.js 的数据获取与状态管理,包括三种数据获取方式(useAsyncData、useFetch、useLazyFetch/useLazyAsyncData)、数据请求封装、状态管理(useState 和 Pinia)以及本地存储持久化。通过本章的学习,你应该已经掌握了如何在 Nuxt.js 中获取数据、管理状态,以及如何实现数据的持久化存储。
在接下来的章节中,我们将学习 Nuxt.js 的布局与组件化开发、Composables 与组合式逻辑复用等核心特性,帮助你更深入地理解和使用 Nuxt.js。
