主题
Vue3 + TypeScript 与 Pinia(三)
本页对应 Vue3 + TypeScript 与 Pinia,共 6 段视频,约 1 小时 2 分。
学习目标
- 能够完成频道与文章列表联动:为文章列表组件定义 props 类型,接收频道 ID,并在 Tab 切换时传递最新 ID 触发对应列表请求。
- 能够抽离组件与接口相关类型到独立 TS 文件,并使用泛型抽象接口响应中重复的
data、message结构,消除重复类型定义。 - 能够按照官方文档在 Vue3 + TS 项目中安装并注册 Pinia,使用
defineStore定义包含 state、action 的 store,并在组件中导入和使用。 - 能够使用
computed实现 Pinia getters,编写异步 action 发起请求并更新 state,在组件中触发 action 并渲染返回数据。 - 能够使用
storeToRefs对 state/getters 解构并保持响应式,区分数据与方法的不同解构方式,并使用 Vue Devtools 查看和调试 Pinia 状态。 - 能够运用“慢就是快”的方法拆解面试题并编写最小示例,结合简历项目准备功能亮点、技术难点及实现细节说明。
知识点
本节说明与学习路径
本节笔记标题是“Vue3 + TS开发环境创建”,但转写稿实际覆盖了综合案例联动、类型优化、Pinia环境搭建与基础使用、getters、异步action、storeToRefs和调试等内容。原文未附图片与完整源码,下面不臆造图片,只按转写稿明确描述整理代码与推导过程。学习路径可以理解为:先用综合案例巩固父子通信与TS类型,再进入Pinia状态管理,最后掌握调试与解构响应式。
频道与文章列表联动:父传子
为什么需要:点击频道后,文章列表要按最新频道ID重新请求,否则列表不会随频道变化。
怎么想:App 中使用 list 组件,二者是父子关系;频道ID在父组件获得,要传给子组件替换原来的 0,所以是父传子。
怎么做:子组件用 defineProps 定义 channelId: number,请求时使用 props.channelId;父组件用 ref(0) 保存频道ID,在 tab 的 change 事件中拿到当前项ID并赋值,再通过属性传给 list 组件。
易错点:只改子组件不传值会报类型错误;channelId 必须是 number;change 回调参数要标注 number;请求参数要确认已使用最新ID。
ts
// List.vue:按转写稿整理
interface Props {
channelId: number
}
const props = defineProps<Props>()
// 请求文章列表时使用 props.channelIdts
// App.vue:按转写稿整理
const channelId = ref(0)
function tabChange(id: number) {
channelId.value = id
}类型抽离与泛型优化
为什么需要:类型都写在组件里不利于复用;多个接口返回都有 data、message,重复定义会难维护。
怎么想:与后端数据结构相关的类型抽到 src/types/data.d.ts,组件通过 import type 导入;把固定字段保留,把可变的 data 抽象成泛型参数 T。
怎么做:定义 ApiRes<T> = { data: T; message: string },频道接口写成 ApiRes<{ channels: ChannelItem[] }>,文章接口同理。
易错点:导入类型要用 type 关键词区分;props 类型通常跟随组件更聚合,不必强行抽离;泛型要找准“可变部分”。
ts
export interface ChannelItem {
id: number
name: string
}
export type ApiRes<T> = {
data: T
message: string
}
export type ChannelRes = ApiRes<{ channels: ChannelItem[] }>Pinia环境搭建
为什么需要:Vue3 项目需要新的状态管理方案,Pinia 去掉了 mutation,action 同时支持同步和异步,组合式写法与组件一致。
怎么想:先创建 Vite + TS + Vue3 空项目,再按官方文档安装 Pinia,创建实例并通过 app.use(pinia) 注册。
怎么做:安装后检查 package.json 是否出现依赖;入口文件中 createPinia() 创建实例,app.use(pinia) 安装。
易错点:装包后不看 package.json;忘记 app.use;把 Pinia 当组件局部状态使用。
ts
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)定义与使用Store
为什么需要:把共享状态和修改方法集中管理。
怎么想:用 defineStore,第一个参数是模块名,第二个参数是组合式回调;ref 定义 state,函数定义 action,最后必须 return 数据和方。
怎么做:组件中导入 useXxxStore,执行得到 store 对象,模板通过点语法使用。
易错点:忘记 return;方法名与数据名混用;以为 store 里不能写组合式 API。
ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})getters与异步action
为什么需要:getters 用于派生状态;异步 action 用于发请求并把结果存入 state。
怎么想:getters 用 computed 模拟;异步 action 就是普通 async 函数,写法与组件内请求一致。
怎么做:定义 list = ref<ChannelItem[]>([]),在 getList 中按接口文档拼接 URL,request.get<ChannelRes> 后把 res.data.data.channels 赋给 list.value,并 return { list, getList }。组件在 onMounted 中调用 action。
易错点:请求返回层级写错;忘记 .value;忘记 return;URL 未按接口文档拼接。
ts
const list = ref<ChannelItem[]>([])
async function getList() {
const res = await request.get<ChannelRes>('/按接口文档拼接')
list.value = res.data.data.channels
}
return { list, getList }storeToRefs与调试
为什么需要:直接解构 store 会丢失响应式;调试时需要观察 state/getters 变化。
怎么想:数据用 storeToRefs 包裹后再解构,方法直接从原 store 解构。
怎么做:const { count, doubleCount } = storeToRefs(counterStore),const { increment } = counterStore。Vue Devtools 的 Pinia 面板可查看模块、state、getters,并支持修改调试。
易错点:方法也用 storeToRefs;解构后忘记保留原 store;调试面板选错模块。
ts
const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore学习建议与易错点收束
转写稿最后还强调:看面试题不要只背答案,建议围绕一个知识点写最小示例,例如父子组件传值、双向绑定,把“曾经写过”转化为可举出的项目经验;简历项目要熟悉功能、亮点和难点。技术上要牢记:凡是存储数据的地方都可能需要类型标注;接口文档是类型定义的依据;Pinia 中数据和方法都要 return;解构数据用 storeToRefs,解构方法直接用原 store。
为什么需要 Vue + TS 模板
为什么需要:基础阶段的纯 TS 环境只能练习 TypeScript 语法,不能直接写 Vue 单文件组件。进入 Vue + TS 阶段后,需要 .vue 文件、Vue 类型推导、Vite 构建等能力,所以要用 Vite 的 Vue + TS 模板快速起项目。
怎么想:不要手写一堆配置文件。Vite 官方脚手架 create-vite 已经内置模板,创建时选 vue-ts 即可。转写稿里也反复体现一个思路:项目创建已经做过很多次,重点是按命令创建、进入目录、装依赖、跑起来验证,而不是重复造配置。
怎么做:在准备放项目的父目录执行:
bash
$ npm create vite@latest vue-ts-pro -- --template vue-ts说明:
npm create vite@latest:基于最新版本的 Vite 创建项目。vue-ts-pro:项目名称,可换成自己的项目名。-- --template vue-ts:选择 Vue + TS 开发模板。
易错点:不要漏掉中间的 --;模板名是 vue-ts,不是 vue 或 ts;项目名不要随意使用大写和特殊字符;命令要在父目录执行,不要已经在同名目录里再创建同名项目。
命令拆解与创建思路
为什么需要:看懂命令才能知道每一步在做什么,出错时能定位是包管理器、项目名还是模板参数的问题。
怎么想:npm create vite@latest 是“用 npm 执行 create-vite 脚手架”,后面的 vue-ts-pro 是传给脚手架的项目名,-- --template vue-ts 是把 --template vue-ts 继续传给 create-vite。转写稿里讲师也强调按官方文档操作,包管理器不限制,npm、yarn 等都可以,但命令写法要对应。
怎么做:执行后如果交互式询问项目名或模板,按提示选择;如果命令参数完整,脚手架会直接生成目录。生成后进入项目:
bash
cd vue-ts-pro
npm install
npm run dev易错点:npm create vite@latest 中的 @latest 不要写成笔记里带链接的格式;如果网络慢导致创建失败,先检查 npm 源和网络;安装依赖前确认已经进入项目目录;不要在没有 package.json 的父目录直接 npm run dev。
创建后的验证流程
为什么需要:项目创建成功不等于能运行。必须安装依赖并启动开发服务器,看到页面和本地地址,才算环境可用。转写稿里创建项目后也是先进入项目、安装依赖、运行 dev,打开本地地址确认页面正常;并强调装包后要看 package.json,确认依赖确实写进去了。
怎么想:验证分三层:目录结构是否存在、依赖是否安装、开发服务器是否启动。先看项目根目录有没有 package.json、src、vite.config.ts 等;再执行安装;最后启动并打开终端给出的本地地址。
怎么做:
bash
cd vue-ts-pro
npm install
npm run dev启动后终端会给出本地访问地址,浏览器打开能看到 Vite + Vue 页面即可。随后可打开 package.json 检查 vue、typescript、vite 等依赖是否在依赖列表中。转写稿中“每装一个新包就看一下 package.json 是否正儿八经装过来”的习惯,在这里同样适用。
易错点:只创建不安装依赖就启动,会报找不到模块;端口被占用时按终端提示换端口;不要把终端里的本地地址当成固定不变;如果页面空白,先看终端和浏览器控制台报错,不要盲目改配置。
易错点与调试建议
为什么需要:环境创建阶段的问题大多来自命令、目录、依赖和 Node 版本,先按顺序排查比乱改配置高效。
怎么想:按“命令是否完整 → 当前目录是否正确 → 依赖是否安装 → Node/npm 版本是否满足 → 网络是否正常”的顺序排查。转写稿里讲师处理项目时也是先创建、再安装、再运行、再看结果,强调按步骤落地和验证。
怎么做:命令报错时先复制完整报错;确认 node -v、npm -v 可用;确认在项目根目录执行 npm install;确认 package.json 中存在依赖;必要时删除 node_modules 后重新安装。创建成功后不要急着写业务代码,先保证默认页面能跑起来。
易错点:把 npm create 和 npm install 混为一谈;在错误目录执行安装;看到报错只改一半;忽略终端提示的本地地址;项目名与已有目录冲突。完成本节后,应得到一个可启动的 vue-ts-pro 项目,为后续 Vue + TS 开发打好环境基础。
为什么要把类型检查拆成两段
.vue 文件不是普通 .ts 文件:它同时包含模板、脚本和样式,类型检查既涉及 script setup 里的变量、props、接口,也涉及模板表达式。若所有类型检查都压到 Vite 启动/打包链路里,开发服务器会变慢;若完全不做,又容易把类型错误带到线上。因此本节的核心思路是:开发阶段把实时类型提示交给 IDE,打包阶段再用 vue-tsc 做兜底校验。这样既保证开发体验,又保证最终质量。

开发阶段:Volar 负责实时类型错误反馈
为什么需要:写 .vue 时,模板里绑定的变量、props 类型、接口字段一旦写错,最好在编辑器里立刻看到,而不是等打包才暴露。
怎么想:把 Volar 看成 Vue 单文件组件的“语言服务”,它负责让 IDE 理解 .vue 的结构并给出类型提示和错误反馈。
怎么做:在编辑器中安装并启用 Volar,确保当前工作区使用项目对应的 TypeScript 版本;打开 .vue 文件后观察模板和脚本中的类型提示是否正常。
易错点:不要把 Volar 当成打包工具;不要同时启用冲突的旧 Vue 插件;编辑器缓存或 TS 版本不一致时,可能出现“明明改了还报错”,要重启语言服务或检查工作区 TS 版本。
开发阶段:TypeScript Vue Plugin 支持在 TS 中导入 .vue
为什么需要:TypeScript 默认不认识 import X from './X.vue',会提示找不到模块。只有让 TS 语言服务知道 .vue 也是一个有类型的模块,导入才能获得类型。
怎么想:Volar 解决 .vue 内部理解问题,TypeScript Vue Plugin 解决“从 .ts 里导入 .vue”的模块识别问题,两者配合。
怎么做:在 IDE/TS 插件环境启用 TypeScript Vue Plugin,与 Volar 配套使用;随后在 .ts 中导入 .vue 组件时,应能获得组件类型而不是模块报错。
易错点:只装 Volar 不启用该插件,可能出现 *.vue 模块找不到;用 declare module '*.vue' { const c: any } 这类 shim 虽然能消错,但会把组件类型变成 any,丢失检查价值;插件版本与 Volar 不匹配也会异常。
打包阶段:vue-tsc 做最终类型检查
为什么需要:开发时 IDE 可能没打开、类型错误可能被漏看;而 Vite 的构建转译并不等同于完整类型检查。发布前必须有统一兜底。
怎么想:开发阶段追求快,打包/CI 阶段追求稳。vue-tsc 负责对 .vue 和 TS 代码做最终类型校验,失败就阻断构建。
怎么做:在项目打包脚本中配置并执行 vue-tsc 做类型检查(具体参数以项目脚本为准),把它作为构建前或构建中的质量门禁。
易错点:误以为 vite build 一定会做完整类型检查;把 vue-tsc 和普通 tsc 混用;只在本地偶尔跑、CI 不跑;类型报错被忽略或跳过。
从转写稿看类型检查的调试顺序
转写稿没有直接讲 Volar 和 vue-tsc,但综合案例展示了类型落地后的调试顺序:先把频道、文章列表的接口类型抽到独立的 .d.ts 文件,组件里用 import type 引入;再把重复的 data、message 结构抽成泛型,把可变的数据类型作为 T 传入。改完后先检查 App.vue 和列表组件是否还有类型错误,再刷新浏览器确认运行无报错。这里要纠正几个同音字:props、defineProps、泛型、.d.ts、import type。这套顺序正好说明:编辑器先暴露类型问题,改完再由项目运行和打包检查兜底。
课件图示








代码示例
示例 1:频道与文章列表联动(父传子 + defineProps)
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ArticleList from './components/ArticleList.vue'
import type { ChannelItem } from './types/data'
// 频道列表(实际项目由接口返回,这里先写死方便运行)
const channelList = ref<ChannelItem[]>([
{ id: 0, name: '推荐' },
{ id: 1, name: 'HTML' },
{ id: 2, name: 'JS' }
])
// 父组件维护"当前选中频道ID",默认 0
const channelId = ref(0)
// 对应 van-tabs 的 @change 回调:点击哪一项就把哪一项的 id 拿到
const onTabChange = (id: number) => {
channelId.value = id
}
</script>
<template>
<!-- 原案例用 van-tabs 的 @change,这里用原生按钮还原同样的逻辑 -->
<div class="tabs">
<button
v-for="item in channelList"
:key="item.id"
:class="{ active: item.id === channelId }"
@click="onTabChange(item.id)"
>
{{ item.name }}
</button>
</div>
<!-- 父传子:把当前频道ID传给子组件 -->
<ArticleList :channel-id="channelId" />
</template>vue
<!-- src/components/ArticleList.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue'
import request from '@/utils/request' // axios 封装
import type { ArticleItem, ArticleRes } from '@/types/data'
// 第一步:定义 props 类型
interface Props {
channelId: number
}
// 第二步:通过 defineProps 的泛型参数把类型注解过来
const props = defineProps<Props>()
const list = ref<ArticleItem[]>([])
const getList = async () => {
const res = await request.get<ArticleRes>('/v1_0/articles', {
// 用 props 里的 ID 替换掉之前写死的 0
params: { channel_id: props.channelId }
})
list.value = res.data.data.results
}
// 频道ID变化就重新请求
watch(() => props.channelId, getList, { immediate: true })
</script>
<template>
<ul>
<li v-for="item in list" :key="item.art_id">{{ item.title }}</li>
</ul>
</template>要点与易错点
defineProps<Props>()是"宏函数 + 泛型参数"两步走:先写interface Props,再把Props作为泛型传进去,不需要再写props: { channelId: Number }。- 类型必须跟后端接口一致:
channelId后端给的是 number,就写number;写成 string 后父组件传值会直接飘红。 - 父组件必须真的把值传下来(
:channel-id="channelId"),只改子组件不传,props.channelId就是undefined,请求参数会丢。 - 千万不要
const { channelId } = defineProps<Props>()解构,解构后 props 失去响应式,频道切换了列表不会跟着变;用props.channelId或toRefs。 - 子组件里只负责"拿 ID 发请求",切 ID 的动作由父组件的
@change完成,这就是最典型的父传子通信。
示例 2:类型抽离到 .d.ts + 用泛型消除重复
ts
// src/types/data.d.ts
// 频道相关
export interface ChannelItem {
id: number
name: string
}
// 文章相关
export interface ArticleItem {
art_id: string
title: string
}
// 把两个接口重复的 { message, data } 抽出来,data 用泛型 T 表示可变部分
export interface ApiRes<T> {
message: string
data: T
}
// 频道列表接口:可变部分是 { channels: ChannelItem[] }
export type ChannelRes = ApiRes<{ channels: ChannelItem[] }>
// 文章列表接口:可变部分是 { results: ArticleItem[]; total_count: number }
export type ArticleRes = ApiRes<{
results: ArticleItem[]
total_count: number
}>ts
// 组件中导入类型使用(注意 type 关键字,区分普通模块导入)
import type { ChannelItem, ChannelRes } from '@/types/data'
import type { ArticleItem, ArticleRes } from '@/types/data'ts
// 组件内部:props 类型跟着组件走,更聚合,不抽到 data.d.ts
interface Props {
channelId: number
}
const props = defineProps<Props>()要点与易错点
message是固定的,data是每个接口各不相同的,所以只把data抽成泛型参数T;泛型优化的思路就是"找到可变的那部分,其余固定"。- 抽出去的类型要
export,组件里用import type { ... }导入,type关键字用于区分类型导入和值导入。 - 命名建议
xxx.d.ts放在src/types下,纯类型文件不会被打包进产物。 - 易错点:把
interface Props也一起搬到data.d.ts里——props 类型属于当前组件,留在组件内部更聚合;只有跟后端接口相关的类型才抽离。 - 易错点:泛型写成
ApiRes<T>却在使用的接口里把message也塞进 T 里,那就白抽了。
示例 3:Pinia 安装、getters、异步 action 与 storeToRefs
ts
// src/main.ts —— 创建 pinia 实例并作为插件注册到应用
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia) // 本质是插件安装
app.mount('#app')ts
// src/store/counter.ts —— 组合式风格定义 store
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0) // state
const doubleCount = computed(() => count.value * 2) // getters:用 computed 模拟
const increment = () => { count.value++ } // action:同步
// 必须 return,组件里才能拿到
return { count, doubleCount, increment }
})ts
// src/store/channel.ts —— 异步 action 获取频道列表
import { defineStore } from 'pinia'
import { ref } from 'vue'
import request from '@/utils/request'
import type { ChannelItem, ChannelRes } from '@/types/data'
export const useChannelStore = defineStore('channel', () => {
const list = ref<ChannelItem[]>([]) // state,泛型标注依据接口文档
const getList = async () => { // 异步 action,写法和组件里完全一致
const res = await request.get<ChannelRes>('/v1_0/channels')
list.value = res.data.data.channels
}
return { list, getList }
})vue
<!-- src/App.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useCounterStore } from './store/counter'
import { useChannelStore } from './store/channel'
const counterStore = useCounterStore()
// 数据(state / getters)用 storeToRefs 包裹后再解构,保持响应式
const { count, doubleCount } = storeToRefs(counterStore)
// 方法直接从 store 上解构即可
const { increment } = counterStore
const channelStore = useChannelStore()
const { list } = storeToRefs(channelStore)
const { getList } = channelStore
// 组件挂载时触发异步 action
onMounted(() => {
getList()
})
</script>
<template>
<button @click="increment">{{ count }}</button>
<span>getter:{{ doubleCount }}</span>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</template>要点与易错点
defineStore('模块名', 回调)两个参数缺一不可;回调里用ref定义 state、用computed定义 getters、用普通函数定义 action,最后必须return,不 return 组件里拿不到。- Pinia 没有 mutation,action 同时支持同步和异步;异步 action 就是"组件里那个发请求的普通函数"搬进了 store,写法一模一样。
- 数据用
storeToRefs(store)解构才能保持响应式;直接const { count } = counterStore会丢失响应式,点击加号视图不动。 storeToRefs只对数据有效(state 和 getters),方法不要用 storeToRefs,直接从原 store 上解构。request.get<ChannelRes>的泛型依据接口文档写,写对后res.data.data.channels才有联想和类型校验。- 浏览器装好 Vue Devtools 后,面板里会多出一个小菠萝(Pinia)标签,可以看到
counter、channel两个模块的 state / getters,并支持直接改数据调试。
常见错误
1. 父组件没传 channelId,子组件类型检查报错
- 现象:子组件模板里
channelId出现红色波浪线,运行时拿到undefined,请求参数变成undefined。 - 原因:只完成了
defineProps的类型声明,父组件忘了绑定;或绑定时写成channelId="1"(字符串)。 - 修法:父组件用
:channel-id="channelId"绑定,保持 number 类型;给 props 一个默认值兜底,先渲染再请求。
2. tab 切换后请求参数还是旧值
- 现象:点击频道后列表没变,Network 里参数一直是
0。 - 原因:只在
change回调里打了 log,没把事件参数写回响应式变量,或写回的不是ref.value。 - 修法:
change回调显式标注(id: number),回调里channelId.value = id;确认请求函数依赖的是这个响应式变量而非写死的常量。
3. 直接解构 store,数据变了页面不动
- 现象:点按钮后 devtools 里
count在变,视图始终停在旧值。 - 原因:
const { count } = counterStore拿到的是普通值,脱离响应式追踪。 - 修法:用
storeToRefs(counterStore)包裹后再解构。
4. 对方法用 storeToRefs 导致调用失效
- 现象:解构出来的
increment是undefined或调用无反应。 - 原因:
storeToRefs只处理 state 与 getters 这类响应式数据,函数不在其列。 - 修法:数据走
storeToRefs,方法仍从原 store 解构:const { increment } = counterStore。
5. store 里写了数据和方法,组件里却取不到
- 现象:
store.list为undefined,控制台打印 store 对象里没有该字段。 - 原因:setup 风格的 store 必须显式
return,漏 return 等于没暴露。 - 修法:把 state、getters、action 统一以对象形式 return;新增字段后回组件里打一次 log 自查。
6. 异步 action 里赋值忘了 .value
- 现象:接口有数据返回,模板渲染空白,
list打印出来是RefImpl对象。 - 原因:
ref在 script 中必须通过.value赋值,写成list = res.data.data.channels是重新赋值局部变量。 - 修法:
list.value = res.data.data.channels;赋值前先访问res.data.data验证泛型是否生效。
7. 接口返回类型在两个组件里各写一份
- 现象:后端加字段时改了一处漏了另一处,类型检查通过但运行时字段缺失。
- 原因:没有抽离公共类型,也没用泛型抽象
{ data, message }这层重复结构。 - 修法:类型集中到
src/types/data.ts并export,用ApiRes<T>泛型承接变化部分;组件里用import type { ... }引入。
8. 本地跑得好好的,build 时类型报错
- 现象:
npm run dev一切正常,打包阶段vue-tsc抛出一堆类型错误。 - 原因:Vite 开发期不做类型检查,只靠 Volar 在 IDE 里提示,关掉编辑器提示就"看不见"错误。
- 修法:把
vue-tsc --noEmit接进构建脚本,提交前跑一次;保证 Volar 与 TypeScript Vue Plugin 均已启用。
练习
练习 1|联动补全 在频道与文章列表联动的基础上,增加"切换频道时先把列表清空并显示加载中"的效果。要求:props 用 defineProps 泛型声明;change 事件参数标注为 number;加载态由子组件内部维护,不通过 props 传递。
练习 2|类型抽离与泛型改造 把 ChannelItem、ChannelRes、ArticleItem、ArticleRes 全部迁到 src/types/data.ts,并用泛型把重复的 { data, message } 结构抽象掉。要求:组件内只保留 props 类型,接口返回类型全部外部导入;改造完成后确认编辑器无红色提示。
练习 3|新建一个 store 创建 articleStore:包含 list 响应式数据、getList(channelId: number) 异步 action、total 计算属性(返回列表长度)。在文章列表组件中 onMounted 触发请求,用 storeToRefs 解构 list 与 total 渲染,方法从原 store 解构。完成后打开 devtools 的 Pinia 面板,观察点击频道时 state 的实时变化。
练习 4|排查改错 有如下代码,点击按钮数据会变但页面不更新,且点击时报 increment is not a function:
ts
const { count, increment } = useCounterStore()找出两处问题并给出修正写法;再说明如果 count 换成 getter doubleCount,结论是否变化。
练习 5|自测清单 不看笔记,口头回答:Pinia 里 state / getters / action 分别对应组合式 API 的哪个函数?为什么 Pinia 不需要 mutation?回答完毕后回代码里各找出一个对应实例验证。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 综合案例-频道和文章列表联动实现 | 05:43 |
| 2 | 综合案例-综合和类型优化 | 10:11 |
| 3 | Pinia-环境搭建和基础使用 | 16:59 |
| 4 | Pinia-getters和异步action | 13:32 |
| 5 | Pinia-storeToRefs和调试 | 07:19 |
| 6 | day03-项目-13番外_如何看面试题和面试_个人建议 | 09:02 |
小结
- 类型定义的唯一依据是接口文档,不是"看起来像什么"——文档改,类型跟着改。
- 父子联动的本质只有两步:父传子给 ID,子用 ID 发请求;复杂交互都是这两步的叠加。
- 判断类型要不要抽离,看"是否被两处以上引用",而不是看代码行数多少。
- 泛型消除的是结构上的重复,不是逻辑上的重复;找不到"可变的那部分"就说明还不该用泛型。
- Pinia 把同步与异步统一收敛到 action,概念数量减少才是它替代 Vuex 的核心收益。
storeToRefs只解决数据解构的响应式丢失,方法解构从来不需要它——这条区分错了,排查会绕很远。- 开发期靠 IDE 提示、构建期靠
vue-tsc兜底,是两条独立的防线,缺一条都会在上线前集中爆雷。 - 调试顺序固定为:先看 devtools 里的 state 是否符合预期,再看 Network 的请求参数与返回值——顺序反了会白改很多代码。