为什么需要 Store
将请求直接写在页面组件中存在以下问题:
- 数据处理分散:不同页面可能对同一数据有不同的处理逻辑
- 数据无法共享:多个页面可能需要访问同一份接口数据
- 维护困难:请求逻辑散落在各组件中,不易管理
因此,将接口响应数据统一放在 Pinia Store 中管理,页面只负责消费数据。
创建 useHomeStore
Store 数据结构
Store 中的 State 结构与接口文档中定义的响应数据结构保持一致:
// src/store/useHomeStore.ts
import { defineStore } from 'pinia'
import { getHomeData } from '~/api/home'
interface ProjectType {
icon: string
title: string
id: number
}
interface CourseType {
image: string
title: string
subtitle: string
url: string
}
interface SwiperType {
image: string
url: string
}
export const useHomeStore = defineStore('home', {
state: () => ({
swipes: [] as SwiperType[],
projects: [] as ProjectType[],
courses: [] as CourseType[],
swipeProjects: [] as SwiperType[],
}),
actions: {
async fetchData() {
try {
const res = await getHomeData()
if (res.status === 200) {
const { data } = res.data // 注意:需要 res.data.data 两层
this.swipes = data.swipes
this.projects = data.projects
this.courses = data.courses
this.swipeProjects = data.swipeProjects
}
} catch (error) {
console.error('获取首页接口失败', error)
}
},
},
})
ts
注意:Axios 响应结构中,
res.data是 Axios 封装的响应体,res.data.data才是接口实际返回的 data 字段。
Actions 中的数据处理
async fetchData() {
try {
const res = await getHomeData()
if (res.status === 200) {
const { data } = res.data // 取出接口返回的数据
this.swipes = data.swipes
this.projects = data.projects
this.courses = data.courses
this.swipeProjects = data.swipeProjects
}
} catch (error) {
console.error('获取首页接口失败', error)
}
}
ts
Axios 拦截器增强
利用 AI 工具(如 Cursor/Codeium)快速生成拦截器代码,然后根据项目需求调整:
// src/utils/axios.ts
class AxiosService {
private instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
this.setupInterceptors()
}
private setupInterceptors() {
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
console.error('请求错误:', error)
// 此处可接入全局 Toast 提示
// 例如通过 Store 控制全局 Toast 显示
return Promise.reject(error)
}
)
}
}
ts
全局错误提示方案
推荐通过 Store + Provide/Inject 实现全局 Toast:
请求失败 → 拦截器捕获错误 → 更新 Toast Store(show: true, message: 'xxx')
↓
App.vue 监听 Store 变化 → 显示 TinyToast 组件
text
页面组件数据对接
改造前(硬编码数据)
// 直接在组件中定义 mock 数据
const items = ref([...])
const projects = ref([...])
ts
改造后(从 Store 获取)
<script setup lang="ts">
import { useHomeStore } from '~/store/useHomeStore'
const homeStore = useHomeStore()
// 调用 action 获取数据
onBeforeMount(async () => {
await homeStore.fetchData()
})
// 从 Store 中读取数据
const swiperItems = computed(() => homeStore.swipes)
const projectList = computed(() => homeStore.projects)
const courseList = computed(() => homeStore.courses)
const swipeProjectList = computed(() => homeStore.swipeProjects)
</script>
vue
防御性渲染
异步数据加载期间,模板中访问数据属性可能报错。使用 v-if 做防御性判断:
<div v-if="selectedItem">
<img :src="selectedItem.url" />
</div>
html
调试技巧
Vue DevTools 检查 Store 状态
- 打开浏览器开发者工具
- 切换到 Vue 面板
- 在组件树中选择 Store -> home
- 查看 State 是否正确更新
断点调试
在 fetchData 中设置断点,检查响应数据结构:
async fetchData() {
const res = await getHomeData()
debugger // 在此暂停,检查 res.data 的完整结构
// ...
}
ts
常见问题:数据未更新
| 现象 | 原因 | 解决方案 |
|---|---|---|
| Store state 始终为空 | 未调用 fetchData() | 在 onBeforeMount 中调用 |
res.data.swipes 为 undefined | 少取一层 data | 使用 res.data.data.swipes |
模板报错 Cannot read property | 异步数据未加载时访问属性 | 使用 v-if 判断 |
| 数据只有一条 | Mock 数据量不足 | 在 mock 文件中增加数据条目 |
TypeScript 类型定义
为 Store 中的数据定义接口类型,确保模板中的属性访问有类型提示:
interface ProjectType {
icon: string
title: string
id: number
}
interface CourseType {
image: string
title: string
subtitle: string
url: string
}
ts
在 Store 的 state 中通过类型断言确保数据安全:
state: () => ({
projects: [] as ProjectType[],
courses: [] as CourseType[],
})
ts
数据流向图
Mock 接口 (mock/home.ts)
|
v
Axios 请求 (api/home.ts → utils/axios.ts)
|
v
Pinia Store (store/useHomeStore.ts)
- state: 存储接口数据
- actions: fetchData() 发起请求
|
v
页面组件 (pages/index.vue)
- computed: 从 Store 读取数据
- onBeforeMount: 触发数据加载
text
完成标志
- Mock 接口返回正确的数据结构
- Store 成功接收并存储数据
- 页面从 Store 读取数据完成渲染
- 刷新页面时数据随机更新(
sort + Math.random) - 无控制台错误
↑