Skip to content

pinia与安装(Lesson25 3/3) ​

本页对应 Lesson25,共 6 段视频,约 1 小时 3 分。

学习目标 ​

  • 能够完成 Pinia 的安装、创建大仓库并在应用入口注册,通过控制台或开发者工具验证 store 已成功挂载。
  • 能够使用选项式 API 的 defineStore 定义模块的 state,并在组件中读取、渲染和修改状态,验证视图随数据变化更新。
  • 能够使用 $patch 和 actions 修改 store 状态,并验证直接修改、对象补丁和 action 调用三种方式的效果。
  • 能够使用选项式 getters 基于 state 派生计算结果,并验证只有依赖数据变化时计算逻辑才重新执行。
  • 能够使用组合式 API 的 defineStore,通过 ref/reactive 定义状态、函数定义 actions、computed 定义 getters,并正确 return 后在组件中调用。
  • 能够在组件外部使用 store,引入 Pinia 实例并作为参数传给 useXxxStore,解决无活动 Pinia 实例导致的报错。

知识点 ​

本节笔记:Pinia符合直觉的 Vue.js 状态管理库

先装好 Pinia:为什么需要、怎么想、怎么做、易错点 ​

为什么需要:组件之间共享数据时,如果靠 props/emit 一层层传,会很绕;Pinia 把共享状态放进“仓库”,组件直接取用,状态变化后视图自动更新。转写稿里安装后能在 Vue Devtools 看到“菠萝”图标和仓库面板,就是它在开发工具里的体现。

怎么想:先有 Pinia 实例,再让 Vue 应用 use 它;之后每个模块用 defineStore 定义。模块标识要唯一,返回值一般以 use 开头。

怎么做:

ts
// main.ts
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

易错点:只在组件里写 useXxxStore(),但入口没有 app.use(pinia),就会报“没有 active Pinia”一类错误。转写稿里组件外使用 store 时也复现了这个错误。

用 state 定义和读取状态 ​

为什么需要:state 是仓库里的数据源。转写稿先建 counter 模块,定义 name: 100 和数组 arr: [1,2,3,4,5],再在 App.vue 里读取。

怎么想:选项式写法用 state: () => ({...});组合式写法用 ref/reactive,并且必须 return,否则外部拿不到。

怎么做:

ts
// store/modules/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    name: 100,
    arr: [1, 2, 3, 4, 5]
  })
})
vue
<script setup lang="ts">
import { useCounterStore } from '@/store/modules/counter'
const counter = useCounterStore()
console.log(counter.name) // 100
</script>

<template>
  <p>{{ counter.name }}</p>
  <p>{{ counter.arr }}</p>
</template>

易错点:转写稿里反复确认“模板里不用 .value”,但脚本里如果拿到的是 ref,就要 .value;组合式 store 不 return 状态,仓库里就是空的。

actions:修改状态与异步操作 ​

为什么需要:状态不能到处随便改,最好把修改逻辑收进 actions,便于复用和调试。

怎么想:选项式 actions 里 this 指向当前 store;组合式 actions 就是普通函数。同步、异步都可以。转写稿还试了 $patch,它接收对象,对象的属性名是要改的状态名,属性值是要改成的值。

怎么做:

ts
export const useCounterStore = defineStore('counter', {
  state: () => ({ name: 100, arr: [1, 2, 3, 4, 5] }),
  actions: {
    add() {
      this.name += 1
    },
    addAsync() {
      setTimeout(() => {
        this.name = 900
      }, 2000)
    },
    patchName() {
      this.$patch({ name: this.name + 3 })
    }
  }
})

易错点:直接 counter.name++ 能改,但转写稿明确说“不建议”;$patch 不要写成 $patch(this.name + 3),要传对象。异步 action 调试时先确认方法是否执行,再看延时是否生效。

getters:派生状态要像计算属性一样理解 ​

为什么需要:数组求和、过滤等派生数据不适合每次在模板里重算。getters 就是仓库里的计算属性。

怎么想:选项式 getters 可接收 state;组合式用 computed。转写稿用 arr.reduce 求和,并验证:只改 name 不会让求和 getter 重新执行,因为影响结果的数据没变。

怎么做:

ts
getters: {
  sum(state) {
    return state.arr.reduce((s, item) => s + item, 0)
  }
}

易错点:转写稿里一开始写 reduce 时漏类型/漏返回,调试后补上初始值和类型推断。组合式里 computed 也要 return,否则组件拿不到。

组合式 API 写 store:ref/reactive + 返回 ​

为什么需要:转写稿说选项式支持,但更建议组合式;一个模块内只能选一种写法,项目里不同模块可共存。

怎么想:defineStore('todo', () => { ... return { ... } })。状态用 ref/reactive,方法就是 actions,computed 就是 getters,watch 用来监听变化。

怎么做:

ts
import { defineStore } from 'pinia'
import { ref, reactive, computed, watch } from 'vue'

export const useToDoStore = defineStore('todo', () => {
  const list = ref([1, 2, 3, 4])
  const obj = reactive({ username: '张三', age: 12 })

  function add(n: number) {
    list.value.push(n)
  }

  const sum = computed(() =>
    list.value.reduce((s, item) => s + item, 0)
  )

  watch(list, () => {
    console.log('list改变了')
  })

  return { list, obj, add, sum }
})

易错点:转写稿里点击按钮没反应,最后发现是 add 没 return;watch 一开始没触发,改成监听 list.value 或 getter 后才观察到变化。脚本里 list.value.push 不能漏 .value。

组件外部使用 store:要传入 Pinia 实例 ​

为什么需要:在 request.ts 等非组件文件里也想用仓库数据。转写稿新建 test.ts 引入 useToDoStore() 后报错,因为组件外没有当前 Pinia 上下文。

怎么想:组件内由 app.use(pinia) 提供上下文;组件外要手动把“大仓库”即 pinia 实例传给 useToDoStore(pinia)。

怎么做:

ts
import { useToDoStore } from '@/store/modules/todo'
import { pinia } from '@/main'

const todo = useToDoStore(pinia)
console.log(todo.list)

易错点:只引入模块、不传 pinia,就会复现转写稿里的报错;传参后错误消失。这个做法在项目里封装 request、做非组件逻辑时很实用。

安装 Pinia:先让项目具备“仓库能力” ​

为什么需要:Vue 组件之间共享状态时,如果只靠父子传参,层级一深就会很绕;Pinia 用来做全局状态管理。安装是第一步,只有依赖装好,后面才能创建仓库、定义模块、在组件里读写状态。

怎么想:先装包,再创建 Pinia 实例,最后在入口文件 main.ts 里通过 app.use(store) 注册。注册完成后,Vue Devtools 里能看到 Pinia 的“菠萝”图标,说明仓库已经接入应用。

怎么做:在项目根目录执行:

shell
yarn add pinia

然后创建 src/store/index.ts,这里导出的是“大仓库”实例:

ts
// 创建大仓库。
import {createPinia} from "pinia";
export default createPinia();

接着在 src/main.ts 中注册:

ts
import {createApp} from "vue";
import App from "@/App.vue";
import store from "./store";
createApp(App)
    .use(store)
    .mount("#app");

易错点:createPinia() 是函数调用,不能写成 createPinia;main.ts 里必须 .use(store),否则组件里使用仓库时找不到 Pinia 实例。转写稿里安装后刷新页面,在开发工具中看到“菠萝”,并提示仓库已安装,这就是验证点。

验证安装:刷新页面看 Devtools ​

为什么需要:代码写完不代表接入成功,要确认 Pinia 真的挂到了 Vue 应用上。

怎么想:入口文件执行 createApp(App).use(store).mount("#app") 后,Pinia 会随应用初始化;打开 Vue Devtools 能看到 Pinia 面板/菠萝图标。转写稿中讲师安装后刷新页面,观察到“菠萝”和仓库安装提示,再继续定义模块。

怎么做:保存 main.ts,刷新浏览器,打开 Vue Devtools 查看 Pinia 是否出现;若没有,先检查 import store from "./store" 路径和 .use(store) 是否遗漏。

易错点:不要只装包不注册;不要漏掉 store/index.ts 的默认导出;路径 ./store 默认会找 store/index.ts,文件位置不要放错。

安装后的边界:组件外部使用 store 要传 Pinia 实例 ​

为什么需要:转写稿后面专门补充了“组件外部使用 store”的问题:在 .ts 文件里直接 useTodoStore() 可能报错,因为此时没有组件上下文,Pinia 不知道该用哪个大仓库。

怎么想:组件内部能直接用,是因为 app.use(store) 已经把 Pinia 注入应用;组件外部使用时,需要手动把大仓库实例传进去。转写稿的解决思路是:先引入大仓库,再把它作为参数传给 useTodoStore(pinia)。

怎么做:在非组件文件中先引入 src/store/index.ts 导出的 Pinia 实例,再调用模块 store:

ts
import pinia from "@/store";
import {useTodoStore} from "@/store/modules/todo";

const todoStore = useTodoStore(pinia);

易错点:不要以为 app.use(store) 后所有 .ts 文件都能无条件使用模块 store;组件外部要显式传入 Pinia 实例。转写稿中反复调试“引入大仓库—作为参数传入”的过程,就是在解决这个问题。安装节先把大仓库建好、注册好,后面定义 defineStore 模块时才不会卡在“找不到 Pinia”上。

(本节笔记未附图片,下面按转写稿还原关键代码与调试过程。)

先理解选项式 API 的定位 ​

为什么需要:Pinia 同时支持选项式和组合式。转写稿里明确说,一个模块内部只能选其中一种写法,但不同模块可以共存。先学选项式,是因为它和 Vue2 的 data/methods/computed 思路接近,便于把 state/actions/getters 对应起来。
怎么想:把“仓库模块”看成一个对象:state 放数据,actions 放修改数据的方法,getters 放派生计算。
怎么做:在 src/store/modules 下建模块文件,用 defineStore('标识', 配置对象) 定义。
易错点:第一个参数是模块标识,必须唯一;第二个参数是配置对象;state 必须是函数返回对象。

ts
// src/store/modules/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    name: 200,
    arr: [1, 2, 3, 4, 5]
  })
})

在组件中使用 state ​

为什么需要:定义完模块后,组件要能拿到状态并让视图响应。
怎么想:defineStore 的返回值通常以 use 开头,例如 useCounterStore;组件里先调用它,得到 store 实例。
怎么做:在 App.vue 中导入并执行,模板里通过 counter.name 访问。转写稿还验证了 v-model 可用,并可用 .number 转数字。
易错点:不要以为定义了模块就自动有数据,必须执行 useCounterStore();调试时先 console.log(counter),看根仓库下是否出现对应模块。

vue
<script setup lang="ts">
import { useCounterStore } from '@/store/modules/counter'
const counter = useCounterStore()
console.log(counter.name)
</script>

<template>
  <p>{{ counter.name }}</p>
  <input v-model.number="counter.name" />
</template>

用 actions 修改状态 ​

为什么需要:直接改 counter.name 虽然能生效,但转写稿明确说“不建议”,因为修改逻辑会散落在组件里。
怎么想:把修改动作收进 actions,组件只负责触发。
怎么做:在配置对象中写 actions,内部用 this 访问当前 store。转写稿还演示了 $patch 和异步延迟修改。
易错点:actions 里不要用箭头函数,否则 this 不是 store;$patch 接收对象时,属性名是状态名,值是“要改成多少”。调试“点击没反应”时,先确认方法是否执行,再看 this 和传参。

ts
actions: {
  addName(n: number) {
    this.name += n
  },
  async delaySetName() {
    setTimeout(() => {
      this.name = 900
    }, 2000)
  }
}
ts
const patchAdd = () => {
  counter.$patch({ name: counter.name + 3 })
}

用 getters 做派生计算 ​

为什么需要:像“数组求和”这种值不该反复写在模板里,应由 getters 统一计算。
怎么想:getters 类似计算属性,依赖不变就不重复执行。转写稿先返回固定 100 验证渲染,再换成 reduce,这是很好的调试顺序。
怎么做:在 getters 中通过 state 取数据并返回结果。
易错点:reduce 要写初值 0;TypeScript 下可标返回类型;getters 不返回,组件拿不到。转写稿还通过 action 修改 arr,观察到求和结果更新。

ts
getters: {
  total: (state): number =>
    state.arr.reduce((sum, item) => sum + item, 0)
}
vue
<p>{{ counter.total }}</p>

组件外部使用 store ​

为什么需要:转写稿提到,将来在 request 等非组件文件里也可能要用 store 数据。
怎么想:组件内能用,是因为 app.use(pinia) 提供了上下文;组件外没有这个上下文,就要手动传入 pinia 实例。
怎么做:先导出大仓库 pinia,再 useTodoStore(pinia)。转写稿的调试现象是:不传参报 getActivePinia 一类错误,传入后正常。
易错点:不是所有地方都能直接 useXxxStore();组件外要传 pinia。

ts
// src/store/index.ts
import { createPinia } from 'pinia'
export const pinia = createPinia()

// src/test.ts
import { pinia } from '@/store'
import { useTodoStore } from '@/store/modules/todo'
const todo = useTodoStore(pinia)
console.log(todo.todoList)

转写稿补充:组合式 API 的对应写法 ​

为什么需要:转写稿后段用 todo 模块演示了组合式写法,便于对比。
怎么想:ref/reactive 相当于 state,函数相当于 actions,computed 相当于 getters;最后必须 return 暴露。
怎么做:defineStore('todo', () => { ... return { ... } })。
易错点:不 return,组件里拿不到;ref 在 script 中要 .value;watch 监听数组内部变化时要注意监听源和 deep,转写稿中就先出现“点击后没打印”的调试过程。

ts
import { defineStore } from 'pinia'
import { ref, reactive, computed, watch } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  const todoList = ref<number[]>([1, 2, 3, 4])
  const obj = reactive({ username: '张三', age: 12 })

  const addTodo = (m: number) => {
    todoList.value.push(m)
  }

  const total = computed(() =>
    todoList.value.reduce((sum, item) => sum + item, 0)
  )

  watch(todoList, () => {
    console.log('todoList改变了')
  }, { deep: true })

  return { todoList, obj, addTodo, total }
})

本节最核心的是:选项式模块里掌握 state → actions → getters 的闭环;组件外使用记得传 pinia;组合式则记住“定义响应式数据并 return”。

先明确本节目标:用组合式 API 写 Pinia 模块 ​

选项式 API 也能写 Pinia,但组合式 API 更贴近 <script setup> 的写法:状态、方法、计算属性、监听逻辑可以放在一起。转写稿里反复提到的“deef范/地婚”其实是 defineStore,“优思”是 use 开头的仓库函数,“该特”是 getters,“埃克什”是 actions。本节要建立一个 todos 模块,并把 state、actions、getters、watch 都写进去。

为什么需要组合式写法?因为当模块逻辑变多时,选项式要分别拆到 state、actions、getters 中;组合式则像写组件一样集中组织。怎么想?记住映射关系:ref/reactive → state,普通函数 → actions,computed → getters,watch 照常在 setup 函数里写。怎么做?看本节笔记代码:

ts
import {defineStore} from "pinia";
import {computed, reactive, ref, watch} from "vue";
const useTodosStore = defineStore("todos",()=>{
    // 响应式的ref,reactive----->state
    let taskList = ref([1,2,3,4]);
    let obj = reactive({
        userName:"zhangsan",
        age:12
    })
    // 定义的方法相当于------------>actions
    const addTaskList = function(num:number){
        taskList.value.push(num);
    }
    // 计算属性------------------->getters
    const sum = computed(()=>taskList.value.reduce((v:number,item:number)=>v=v+item,0));
    // watch
    // watch(taskList,()=>{
    //     console.log("taskList改变了")
    // },{
    //     immediate:true,
    //     deep:true
    // })
  
    watch(()=>taskList.value,()=>{
        console.log("taskList改变了")
    },{
        immediate:true,
        deep:true
    })
    // 切记一定一定一定要返回!
    return {
        taskList,
        obj,
        addTaskList,
        sum
    }
})
export default useTodosStore;

易错点很关键:组合式写法必须 return。转写稿中调试时出现过“仓库里没有状态”“按钮调用不到方法”,原因就是定义完没有返回;把 taskList、addTaskList、sum 返回后,组件里才能拿到。另一个易错点是 taskList.value.push,ref 在脚本里要加 .value。

状态:ref/reactive 就是 state ​

为什么需要状态?页面要显示任务列表和用户信息,并且数据变化后视图要更新。怎么想?简单值或数组用 ref,对象用 reactive 都可以;在 Pinia 的组合式模块里,它们就承担 state 的角色。怎么做?本节用 ref([1,2,3,4]) 定义 taskList,用 reactive 定义 obj。

易错点:ref 在 JS/TS 中访问要 .value,模板中通过 store 访问时 Pinia 会做解包,所以 {{todos.taskList}} 能直接显示数组。不要把 reactive 对象整个重新赋值,否则容易丢失响应式;修改对象属性更稳。

方法:普通函数就是 actions ​

为什么需要 actions?状态不能只在仓库里躺着,按钮点击要追加任务。怎么想?在 setup 函数里定义普通函数,内部修改 state,然后 return 出去。怎么做?addTaskList(num) 中执行 taskList.value.push(num)。

易错点:第一,push 前别漏 .value;第二,方法名要 return,否则模板里 todos.addTaskList 是 undefined;第三,传参类型要写清楚,本节写 num:number,调用时传 todos.taskList.length+1 是数字。

计算属性:computed 就是 getters ​

为什么需要 getters?sum 需要根据 taskList 自动求和,而不是手动维护一个总数。怎么想?用 computed 包住 reduce,依赖 taskList.value,数组变化时自动重算。怎么做?本节代码:

ts
const sum = computed(()=>taskList.value.reduce((v:number,item:number)=>v=v+item,0));

转写稿调试时提到 reduce 的类型问题,给 v 和 item 标 number 后正常。易错点:computed 返回的是计算属性,模板里通过 todos.sum 使用;如果依赖的数据没变,它不会重复执行,这和选项式 getters 的“缓存”思路一致。

监听:watch 的调试与最终写法 ​

为什么需要 watch?任务列表变化时可能要打日志、同步缓存或做副作用。怎么想?组合式模块里可以直接写 watch。转写稿中先尝试监听 taskList,后来按本节最终代码改成监听 ()=>taskList.value,并加上 immediate:true 和 deep:true,这样首次会执行,后续 push 也能打印“taskList改变了”。

易错点:监听数组内部变化时,deep 要开;需要首次执行时加 immediate;监听源写成 getter 函数时,函数体里访问 taskList.value。这些都不是“额外魔法”,就是 Vue 的 watch 用法。

在 App.vue 中消费 store ​

为什么还要看组件?仓库定义好只是第一步,组件要执行 useTodosStore() 才能拿到实例。怎么做?本节代码:

vue
<template>
    <h3>组合式API</h3>
    <p>{{todos}}</p>
    <p>taskList:{{todos.taskList}}</p>
    <p>sum:{{todos.sum}}</p>
    <button @click="todos.addTaskList(todos.taskList.length+1)">增加元素</button>
</template>

<script lang="ts" setup>
import useTodosStore from "@/store/modules/todos";
const todos = useTodosStore();
</script>

<style scoped>
</style>

易错点:先 const todos = useTodosStore(),再在模板里用 todos.xxx;按钮里调用方法要带括号和参数。转写稿里还验证了:如果模块没 return,模板拿不到 taskList 和 addTaskList。

补充:组件外部使用 store 要传入 Pinia 实例 ​

转写稿最后补充了一个做项目常见问题:在 .ts 文件里直接 useTodosStore() 可能报 getActivePinia 一类错误。为什么?因为组件外没有组件上下文,Pinia 找不到当前大仓库。怎么想?先把 main.ts 中创建的 Pinia 实例引入,再把它作为参数传给 useTodosStore(pinia);具体实例名以你项目 main.ts 为准。易错点:不要在组件外直接裸调 useTodosStore();先安装大仓库,再告知模块属于哪个 Pinia。

一、先明确:什么是“组件外部使用 store” ​

为什么需要:在 .vue 组件里,我们通常直接调用 useTodosStore() 就能拿到模块数据;但实际项目里,普通 .ts 文件也可能要用 store,例如请求封装、路由守卫、测试文件等。转写稿里就提到“将来做项目时,request 封装里要使用 store 里面的数据”。这类文件不在组件 setup 上下文里,写法需要调整。

怎么想:组件内能直接调用,是因为入口里已经通过 app.use(store) 安装了大仓库,Pinia 能找到当前活动实例;普通 TS 文件不在这个上下文里,直接调用就可能报“没有活动 Pinia/大仓库”的错误。

怎么做:先在 src/test.ts 中按笔记写测试代码:

ts
// 1- 引入大仓库
import store from "@/store";
import useTodosStore from "@/store/modules/todos";
// 2- 传入大仓库:告知模块todos是属于哪一个大仓库下的。
const todos = useTodosStore(store);
console.log(todos.taskList);

易错点:不要只引入 useTodosStore 就调用;组件外部使用时要显式传入大仓库实例。这里的“大仓库”指 @/store 导出的 Pinia 实例,不是某个具体业务模块。

二、复现问题:为什么直接调用会报错 ​

为什么需要:先复现错误,才能理解“传入大仓库”不是多余参数,而是解决上下文缺失。

怎么想:转写稿的调试过程是:先在 test.ts 里像组件内一样直接 useTodosStore() 并输出 todos.taskList,然后在 main.ts 引入 @/test,刷新页面看控制台。结果报错;把 import "@/test" 注释掉后不报错;组件内部使用又正常。说明问题不在模块本身,而在“组件外部没有可用的大仓库上下文”。

怎么做:按笔记在入口中引入测试文件:

ts
import "@/test";

这样 test.ts 会被执行,控制台能看到测试输出或报错。验证时先看报错是否与 Pinia 活动实例有关,再改为传入 store。

易错点:只在 test.ts 里写代码但不在 main.ts 引入,文件不会执行,容易误以为“没问题”。另外不要把“组件内正常”直接套到普通 TS 文件。

三、正确做法:把大仓库作为参数传给模块 store ​

为什么需要:useTodosStore(store) 的作用是告诉 todos 模块:你要从哪个大仓库里取状态。转写稿说得很直白:“告知你的模块 todos 是属于哪一个仓库”。

怎么想:组件内部由 Pinia 自动找到活动实例;组件外部则手动把实例交给 useTodosStore。所以步骤是:先引入大仓库 store,再引入模块 useTodosStore,最后调用时传参。

怎么做:核心就是笔记中的两行:

ts
import store from "@/store";
import useTodosStore from "@/store/modules/todos";
const todos = useTodosStore(store);
console.log(todos.taskList);

易错点:传入的必须是同一个 Pinia 实例;不要误传成 useTodosStore() 的返回值;也不要漏掉 import store from "@/store"。如果项目里 main.ts 已经 app.use(store),组件内可省略参数,但组件外部仍建议显式传入。

四、验证与总结:入口引入、控制台检查 ​

为什么需要:改完后要确认测试文件真的执行、模块真的取到。

怎么想:入口 main.ts 引入 @/test 后,test.ts 会执行;控制台应能打印 todos.taskList。如果仍报错,先检查是否引入大仓库、是否传参、main.ts 是否引入测试文件。

怎么做:保留笔记代码,刷新页面看控制台;确认 taskList 有输出后,再把这个写法迁移到请求封装等普通 TS 文件。

易错点:import "@/test" 不要忘记;测试代码不要污染正式逻辑,验证完可移除或改成正式封装。记住本节结论:组件外部使用 store,两步——引入大仓库,把大仓库传给模块 useStore。

先明确:这节用 Pinia 小仓库做组件通讯 ​

这节笔记标题是“组件通讯”,但转写稿实际练的是用 Pinia 的 store 模块共享状态:在 src/store/modules 下定义小仓库,在组件里读取和修改,也可以在组件外部使用。为什么需要:父子组件用 props/emit 可以,但跨层级、多个组件共享同一份数据时,层层传递很麻烦。怎么想:把共享数据放进 store,组件只负责“拿 store、用 store”。怎么做:先定义模块,再在组件中 useXxxStore()。易错点:定义完不等于自动可用,组件里要执行 useXxxStore();组件外使用还要处理 Pinia 实例。

选项式 store:用 state 定义数据状态 ​

为什么需要:先要有“数据源”,组件才有东西可通讯。怎么想:一个模块文件就是一个“小仓库”,用 defineStore 定义,第一个参数是模块标识,第二个参数写配置对象。怎么做:按转写稿整理如下。

ts
// src/store/modules/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    name: 100,
    arr: [1, 2, 3, 4, 5]
  })
})

组件里使用:

vue
<script setup lang="ts">
import { useCounterStore } from './store/modules/counter'
const counter = useCounterStore()
console.log(counter.name)
</script>

<template>
  <p>{{ counter.name }}</p>
  <p>{{ counter.arr }}</p>
</template>

易错点:转写稿里定义完模块后页面没内容,是因为还没在组件里执行 useCounterStore();执行后 counter 是响应式对象,模板里直接写 counter.name,不要习惯性写成 counter.name.value。选项式 store 的 state 通过 counter.name 访问,数据变了视图会刷新。

修改 state:直接改、$patch、action ​

为什么需要:通讯不只是读,还要改。怎么想:能改,但不建议到处直接改,最好收口到 action。怎么做:转写稿演示了三类写法。

vue
<template>
  <input v-model.number="counter.name" />
  <button @click="counter.name += 2">直接改</button>

  <button @click="counter.$patch({ name: counter.name + 3 })">
    $patch 对象
  </button>

  <button @click="counter.$patch((state) => { state.name += 3 })">
    $patch 函数
  </button>

  <button @click="counter.addOne">action</button>
  <button @click="counter.add(2)">带参 action</button>
</template>

对应 action:

ts
actions: {
  addOne() {
    this.name++
  },
  add(n: number) {
    this.name += n
  }
}

易错点:v-model.number 可以绑定并转数字,转写稿现场验证过;$patch 接收对象时,对象属性名就是要修改的状态名,属性值就是要改成/加成的值;直接改和 $patch 本质都能改,但项目里更推荐 action,便于维护。

getters:像计算属性一样派生数据 ​

为什么需要:有些数据要基于 state 算出来,比如数组求和。怎么想:不要在每个组件里重复算,放进 getters。怎么做:

ts
getters: {
  sum: (state) => state.arr.reduce((s, item) => s + item, 0)
}

组件里:

vue
<p>{{ counter.sum }}</p>

转写稿现场调试过:数组 [1,2,3,4,5] 求和为 15;后来通过 action 改 arr,sum 会跟着变。易错点:getters 只依赖 state 计算,不要在里面写异步请求;如果结果没变,它不会重复执行,这正是计算属性式缓存。

组合式 API 的 store:ref/reactive 定义状态,记得 return ​

为什么需要:转写稿明确说组合式 API 使用频率较高,选项式和组合式可以共存,但每个模块里选一种。怎么想:defineStore 第二个参数写成函数,里面用 ref、reactive、computed、watch。怎么做:

ts
// src/store/modules/todo.ts
import { defineStore } from 'pinia'
import { ref, reactive, computed, watch } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  const todoList = ref<number[]>([1, 2, 3, 4])
  const obj = reactive({ username: '张三', age: 12 })

  const sum = computed(() =>
    todoList.value.reduce((v, item) => v + item, 0)
  )

  const add = (n: number) => {
    todoList.value.push(n)
  }

  watch(todoList, () => {
    console.log('todoList改变了')
  }, { deep: true })

  return { todoList, obj, sum, add }
})

易错点:转写稿里多次出现“没返回就拿不到”的调试:ref/reactive 定义的状态、方法、computed 都必须 return,否则组件里拿不到;watch 监听数组 push 时要加 deep: true,否则现场点了没反应。

组件外部使用 store:必须传入 pinia 实例 ​

为什么需要:转写稿提到在 request 等非组件文件里也可能要用 store 数据。怎么想:组件内 useStore() 能自动找到当前 Pinia,组件外没有激活上下文。怎么做:先导出大仓库实例,再传参。

ts
// src/store/index.ts
import { createPinia } from 'pinia'
export const pinia = createPinia()
ts
// src/test.ts
import { useTodoStore } from './store/modules/todo'
import { pinia } from './store'

const todoStore = useTodoStore(pinia)
console.log(todoStore.todoList)

易错点:转写稿现场在 test.ts 里直接 useTodoStore() 报错,提示与 getActivePinia 相关;解决两步:引入大仓库 pinia,把 pinia 作为参数传给 useTodoStore(pinia),告诉模块去哪个大仓库里找自己。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:创建大仓库并在 main.ts 中挂载 ​

ts
// src/store/index.ts —— 创建大仓库
import { createPinia } from "pinia";
export default createPinia();
ts
// src/main.ts —— 挂载大仓库
import { createApp } from "vue";
import App from "@/App.vue";
import store from "./store";

createApp(App)
    .use(store)
    .mount("#app");

关键行与易错点

  • createPinia() 只负责“建仓库”,返回的实例必须 export default 出去,main.ts 才能拿到。
  • createApp(App).use(store) 是“安装”,没有 .use(store) 这一步,任何组件里调用 useXxxStore() 都会报 getActivePinia was called with no active Pinia。
  • import store from "./store" 走的是目录默认入口 store/index.ts,别写成 "./store/index" 之外的乱路径。
  • yarn add pinia 装完之后不用额外注册插件,Vue 3 下直接 .use() 即可。

示例二:组合式 API 定义 todos 模块,并在 App.vue 中使用 ​

ts
// src/store/modules/todos.ts
import { defineStore } from "pinia";
import { computed, reactive, ref, watch } from "vue";

const useTodosStore = defineStore("todos", () => {
    // 响应式的 ref / reactive ——> state
    let taskList = ref([1, 2, 3, 4]);
    let obj = reactive({
        userName: "zhangsan",
        age: 12
    });

    // 定义的方法 ——> actions
    const addTaskList = function (num: number) {
        taskList.value.push(num);
    };

    // 计算属性 ——> getters
    const sum = computed(() =>
        taskList.value.reduce((v: number, item: number) => (v = v + item), 0)
    );

    // watch
    watch(
        () => taskList.value,
        () => {
            console.log("taskList改变了");
        },
        {
            immediate: true,
            deep: true
        }
    );

    // 切记一定一定一定要返回!
    return {
        taskList,
        obj,
        addTaskList,
        sum
    };
});

export default useTodosStore;
vue
<!-- src/App.vue -->
<template>
    <h3>组合式API</h3>
    <p>{{ todos }}</p>
    <p>taskList:{{ todos.taskList }}</p>
    <p>sum:{{ todos.sum }}</p>
    <button @click="todos.addTaskList(todos.taskList.length + 1)">增加元素</button>
</template>

<script lang="ts" setup>
import useTodosStore from "@/store/modules/todos";
const todos = useTodosStore();
</script>

<style scoped>
</style>

关键行与易错点

  • defineStore("todos", () => { ... }):第二个参数写成函数就是组合式写法,写成对象就是选项式写法,同一个模块只能选一种。
  • 组合式写法里,ref / reactive 声明的就是 state,普通函数就是 actions,computed 就是 getters,一一对应,不需要额外语法糖。
  • 最易错的一行:return { taskList, obj, addTaskList, sum }。漏掉任何一个,模板里拿到的就是 undefined,且不报错,非常难排查。
  • watch 直接监听 taskList 只能拿到新值、拿不到“值变化”的细粒度;用 () => taskList.value 才是笔记里写的写法,配合 deep: true 才能监听数组内部 push。
  • reduce 必须给初始值 0 并标注 (v: number, item: number),否则 TS 会推断成 never 或报类型错误。
  • 模板里 {{ todos }} 输出的是整个 store(一个 Proxy),{{ todos.taskList }} 才是具体状态;Pinia 的 state 在组件里不需要写 .value,这点和直接写 ref 不同。

示例三:在组件外部(非 .vue 文件)使用 store ​

ts
// src/test.ts
// 1- 引入大仓库
import store from "@/store";
import useTodosStore from "@/store/modules/todos";

// 2- 传入大仓库:告知模块 todos 是属于哪一个大仓库下的
const todos = useTodosStore(store);

console.log(todos.taskList);
ts
// src/main.ts —— 引入即执行
import "@/test";

关键行与易错点

  • 在 .vue 组件里能直接 useTodosStore(),是因为 app.use(store) 之后 Pinia 会被“激活”;而在 .ts 文件里没有这个激活上下文,所以必须把大仓库实例当参数传进去:useTodosStore(store)。
  • 漏传参数时的报错是 getActivePinia was called with no active Pinia,不要误以为是没装 pinia,实际是没传大仓库。
  • import "@/test"; 是副作用引入,只为让文件执行一次;如果只写 import "@/test" 却放在 .use(store) 之前,同样可能拿不到 store,顺序上建议放最后。
  • 这种写法常用于封装好的 request.ts、路由守卫、工具函数里读 store 数据,属于实战场景的标配。

常见错误 ​

  1. 现象:组件里调用 useTodosStore() 报 getActivePinia was called with no active Pinia。
    原因:createPinia() 已创建,但未在 main.ts 中通过 .use(store) 安装。
    修法:确认入口执行 createApp(App).use(store).mount("#app"),再在组件中使用 store。

  2. 现象:组合式 defineStore("todos", () => { ... }) 中定义了 taskList、addTaskList、sum,组件里却取不到或调用报错。
    原因:组合式写法末尾忘记 return,未返回的内容不会暴露为 store 的 state/actions/getters。
    修法:把需要给组件使用的 ref/reactive/computed/方法 全部放入 return { ... }。

  3. 现象:在 src/test.ts 等组件外使用 useTodosStore() 报错或拿不到模块。
    原因:组件外没有当前 Pinia 上下文,未告知该模块属于哪个大仓库。
    修法:先 import store from "@/store",再调用 useTodosStore(store),并在入口引入该测试文件验证。

  4. 现象:const { taskList } = useTodosStore() 后修改 store 数据,页面不更新。
    原因:直接解构响应式 store 会丢失响应性。
    修法:保留 todos.taskList 的访问方式;需要解构时使用 storeToRefs 解构 state/getters,方法仍从 store 实例取。

  5. 现象:选项式 store 的 action 中 this.taskList 为 undefined。
    原因:action 写成箭头函数,this 不指向 store 实例。
    修法:选项式 actions 使用普通函数写法,通过 this 访问 state/getters/其他 action。

  6. 现象:组合式 store 中 watch(taskList, ...) 监听不到数组变化,或首次不执行。
    原因:监听目标、immediate/deep 配置与 ref 的 .value 使用不一致。
    修法:按实际需求使用 watch(() => taskList.value, ...),并明确配置 immediate、deep。

练习 ​

  1. 打开 src/store/modules/todos.ts,故意删除组合式 store 的 return 中 addTaskList,在 App.vue 点击按钮观察报错;再恢复并说明修复点。

  2. 在 src/test.ts 中只写 const todos = useTodosStore() 并在 main.ts 引入,复现组件外使用报错;改为传入大仓库实例后打印 todos.taskList。

  3. 在 App.vue 中用解构方式取 taskList,修改 store 后观察视图是否更新;改用 storeToRefs 或保留 todos.taskList 访问方式修复。

  4. 在选项式 store 中新增一个使用箭头函数的 action,调用后观察 this 异常;改成普通函数并验证能修改 state。

  5. 给组合式 todos store 增加 sum getter 与一个 watch,要求:新增任务后 sum 更新;watch 能监听到 taskList 变化,并说明 return 中需要暴露哪些内容。

原视频对照 ​

#视频时长
1使用state13:42
2补充01:51
3使用actions13:22
4使用getters08:48
5采用组合式API的方案19:28
6组件外部使用store06:13

小结 ​

  1. Pinia 大仓库必须先在入口安装,组件内才能正常调用模块 store。
  2. 组合式 store 的核心检查点不是“写没写”,而是“该返回的有没有返回”。
  3. 组件内使用 useXxxStore();组件外使用 useXxxStore(store) 并传入大仓库实例。
  4. 不要直接解构 store 后长期使用,响应性丢失是高频问题。
  5. 选项式 store 中 action 依赖 this 时,避免箭头函数。
  6. 组合式 store 中 ref/reactive 对应状态,方法对应 actions,computed 对应 getters。
  7. 排查顺序建议:大仓库是否安装 → 模块是否导出 → 组合式是否 return → 调用处是否传对实例 → 是否破坏响应性。
  8. 练习时优先用控制台和 Vue DevTools 观察 store 状态,再定位模板或调用链问题。