欢迎光临
我们一直在努力

跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现

文章目录

    • 一、Pinia 相关介绍
      • 1.1 什么是 Pinia?
      • 1.2 为什么要用 Pinia?
      • 1.3 Pinia 存什么?
    • 二、Pinia 基础使用
      • 2.1 项目引入:四步初始化
      • 2.2 定义仓库:两种写法
        • 写法一:Option 对象式(类 Vuex 风格)
        • 写法二:Setup 函数式(组合式 API 风格)
      • 2.3 组件内使用仓库
    • 三、待办任务列表实战案例
      • 3.1 页面搭建(组件化思路)
      • 3.2 Pinia 落地流程
      • 3.3 功能分步实现
        • 1)新增待办 —— TodoHeader.vue
        • 2)删除待办 —— TodoMain.vue
        • 3)单选与全选双向联动
        • 4)底部统计与清除已完成 —— TodoFooter.vue
        • 5)筛选 + 列表数据切换
    • 全文总结
    • 核心知识点复盘

一、Pinia 相关介绍

1.1 什么是 Pinia?

Pinia 是专为 Vue 生态打造的状态管理工具,用于实现跨组件通信和多组件共享数据。它是 Vuex 的继任者,由 Vue 核心团队维护,现已成为 Vue3 官方推荐的状态管理方案。

在理解 Pinia 之前,先回顾 Vue 的两大基础特性:

  • 数据驱动视图:数据变了,页面自动更新
  • 组件化开发:页面拆成一个个独立组件,每个组件各司其职

问题来了:当组件 A、B、C、D 都需要读写同一份公共数据时,靠父子组件传值(props / emit)会非常繁琐且难以维护。Pinia 就是用来解决这个问题的 —— 把公共数据抽到一个独立的"仓库"里,所有组件都能直接访问和修改,并且数据变更全程可追踪。

组件A ←→ Pinia仓库(公共数据) ←→ 组件B
组件C ←→ ←→ 组件D

1.2 为什么要用 Pinia?

使用 Pinia 有两个核心目的:

目的说明
多组件数据同步 任意组件修改仓库数据后,所有引用该数据的组件都会自动更新
数据变更可追踪 配合 Vue DevTools 可回溯每一次数据变化,方便调试

1.3 Pinia 存什么?

一句话:存储多个组件共用的公共数据。比如:

  • 用户登录状态(token、用户信息)
  • 购物车商品列表
  • 页面主题配置
  • 多步骤表单的中间数据

组件私有的、不共享的数据,依然放在组件自身的 ref / reactive 里,不需要放进 Pinia。


二、Pinia 基础使用

2.1 项目引入:四步初始化

任何项目使用 Pinia,都需要走完这固定四步:

// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 第1步:导入 createPinia 方法
import { createPinia } from 'pinia'

// 第2步:创建 Pinia 实例
const pinia = createPinia()

const app = createApp(App)

// 第3步:全局挂载
app.use(pinia)

// 第4步:挂载应用
app.mount('#app')

注意:app.use(pinia) 必须在 app.mount('#app') 之前执行,否则组件内调用仓库时会报错。

2.2 定义仓库:两种写法

引入仓库定义函数,然后创建并导出仓库:

import { defineStore } from 'pinia'

export const useXxxStore = defineStore('仓库唯一id', Setup函数 或 Option对象)

defineStore 有三个注意要点:

  • 仓库 id 必须全局唯一,不能与其他仓库重名
  • 导出变量命名遵循 useXxxStore 规范,这是 Pinia 的约定,也方便识别
  • 第二个参数支持两种写法:Setup 函数式 和 Option 对象式

  • 写法一:Option 对象式(类 Vuex 风格)

    这种方式通过 state、getters、actions 三大核心配置来组织代码,和 Vue2 Options API 风格一致:

    // store/stock.js
    import { defineStore } from 'pinia'

    export const useStockStore = defineStore('stock', {
    // state:存放共享原始数据(必须是函数,返回一个对象)
    state: () => ({
    stock: 100
    }),

    // getters:基于 state 计算衍生数据(等效组件内的 computed)
    getters: {
    doubleStock: (state) => state.stock * 2
    },

    // actions:修改共享数据的函数,支持同步和异步操作
    actions: {
    addStock() {
    // 通过 this 访问 state 和 getters
    this.stock++
    },
    subStock() {
    this.stock—
    }
    }
    })

    关键点解析:

    • state 必须写成箭头函数返回对象的形式 state: () => ({ … }),确保每个组件调用时拿到独立的数据副本
    • getters 接收 state 作为第一个参数,可直接通过 state.xxx 访问数据
    • actions 内部通过 this 访问 state 和 getters,支持同步和异步逻辑

    下表对比三个配置项的功能和对应关系:

    配置项作用对应 Vue 组件内
    state 存放原始共享数据 data()
    getters 基于 state 计算衍生值 computed
    actions 定义修改数据的函数 methods

    写法二:Setup 函数式(组合式 API 风格)

    直接复用 Vue3 组合式 API 的写法,更灵活、也更受 Vue3 开发者青睐:

    // store/stock.js
    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'

    export const useStockStore = defineStore('stock', () => {
    // 用 ref/reactive 声明响应式数据(等同于 state)
    const stock = ref(20)

    // 用 computed 计算衍生值(等同于 getters)
    const doubleStock = computed(() => {
    return stock.value * 2
    })

    // 普通函数修改数据(等同于 actions)
    function addStock() {
    stock.value++
    }

    function subStock() {
    stock.value—
    }

    // 最后 return 暴露出去,组件才能访问
    return {
    stock,
    doubleStock,
    addStock,
    subStock
    }
    })

    两种写法如何选择? 如果你熟悉 Vue3 组合式 API,推荐 Setup 函数式;如果你从 Vuex 迁移过来或偏好 Options API,选 Option 对象式。两者功能完全等价。

    2.3 组件内使用仓库

    组件内使用仓库统一分三步:

    <!– components/Add.vue –>
    <script setup>
    // 第1步:导入仓库
    import { useStockStore } from '@/store/stock'

    // 第2步:调用函数,获取仓库实例
    const stockStore = useStockStore()
    </script>

    <template>
    <div class="add">
    <h3>Add 组件</h3>
    <!– 第3步:通过实例读取数据和调用方法 –>
    <p>已知库存数: {{ stockStore.stock }}</p>
    <button @click="stockStore.addStock()">库存 +1</button>
    </div>
    </template>

    再来一个减法组件,验证跨组件共享:

    <!– components/Sub.vue –>
    <script setup>
    import { useStockStore } from '@/store/stock'
    const stockStore = useStockStore()
    </script>

    <template>
    <div class="sub">
    <h3>Sub 组件</h3>
    <p>已知库存数: {{ stockStore.stock }}</p>
    <button @click="stockStore.subStock()">库存 -1</button>
    </div>
    </template>

    在 App.vue 中汇总:

    <script setup>
    import Add from './components/Add.vue'
    import Sub from './components/Sub.vue'
    import { useStockStore } from '@/store/stock'

    const stockStore = useStockStore()
    </script>

    <template>
    <h1>根组件</h1>
    库存总数:
    <input type="number" v-model.number="stockStore.stock" />
    <p>stock 的翻倍值 = {{ stockStore.doubleStock }}</p>
    <hr />
    <Add />
    <hr />
    <Sub />
    </template>

    效果验证:在 Add 组件中点击 “+1”,Sub 组件显示的数字会同步更新;在输入框中直接修改库存数,所有组件的显示也会同步。这就是 Pinia 的核心能力 —— 一处修改,处处响应。


    三、待办任务列表实战案例

    以上是基础语法,接下来通过一个完整的 Todo List(待办事项) 项目,把 Pinia 真正用起来。本章节同时给出 Option 和 Setup 两种写法的代码,方便对比学习。

    3.1 页面搭建(组件化思路)

    先将页面拆分为 3 个子组件:

    组件文件名职责
    TodoHeader TodoHeader.vue 输入框,新增待办事项
    TodoMain TodoMain.vue 展示待办列表,单选/全选操作
    TodoFooter TodoFooter.vue 统计未完成数量,筛选切换,清除已完成

    在 App.vue 中引入组装:

    <script setup>
    import './assets/style.css'
    import TodoHeader from './componnets/TodoHeader.vue'
    import TodoMain from './componnets/TodoMain.vue'
    import TodoFooter from './componnets/TodoFooter.vue'
    </script>

    <template>
    <div class="todoapp">
    <TodoHeader />
    <TodoMain />
    <TodoFooter />
    </div>
    </template>

    3.2 Pinia 落地流程

    按照前面的四步法初始化 Pinia(安装 → 引入 → 创建 → 挂载),然后创建 todo 业务仓库。

    Option 对象式仓库

    // store/todo.js
    import { defineStore } from 'pinia'

    // 定义筛选类型常量,避免组件内硬编码字符串
    export const TODO_TYPE = {
    all: 'all', // 全部
    unfinished: 'unfinished', // 未完成
    finished: 'finished' // 已完成
    }

    export const useTodoStore = defineStore('todo', {
    state: () => ({
    // 当前筛选类型,默认显示全部
    type: TODO_TYPE.all,
    // 共享的待办列表
    todos: [
    { id: 1, text: 'Buy milk', finished: false },
    { id: 2, text: 'Buy eggs', finished: true },
    { id: 3, text: 'Buy bread', finished: false }
    ]
    }),

    getters: {
    // 判断是否全部选中(用于全选复选框)
    isAll: (state) => state.todos.every((item) => item.finished),

    // 未完成任务列表(用于底部统计)
    unfinishedTodos: (state) => state.todos.filter((item) => !item.finished),

    // 根据筛选类型,计算当前需要展示的列表
    showTodos: (state) => {
    switch (state.type) {
    case TODO_TYPE.all:
    return state.todos
    case TODO_TYPE.unfinished:
    return state.todos.filter((item) => !item.finished)
    case TODO_TYPE.finished:
    return state.todos.filter((item) => item.finished)
    }
    }
    },

    actions: {
    // 新增待办
    addTodo(text) {
    this.todos.push({
    text,
    id: this.todos.length + 1,
    finished: false
    })
    },
    // 删除待办
    delTodo(i) {
    if (window.confirm('确认删除么')) {
    this.todos.splice(i, 1)
    }
    },
    // 批量切换完成状态(全选/取消全选)
    toggleTodo(flag) {
    this.todos.forEach((item) => (item.finished = flag))
    },
    // 清除所有已完成任务
    clearTodo() {
    this.todos = this.todos.filter((item) => !item.finished)
    },
    // 设置筛选类型
    setType(type) {
    this.type = type
    }
    }
    })

    Setup 函数式仓库(对比写法)

    // store/todo.js
    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'

    export const TODO_TYPE = {
    all: 'all',
    unfinished: 'unfinished',
    finished: 'finished'
    }

    export const useTodoStore = defineStore('todo', () => {
    const todos = ref([
    { id: 1, text: 'Buy milk', finished: false },
    { id: 2, text: 'Buy eggs', finished: true },
    { id: 3, text: 'Buy bread', finished: false }
    ])
    const type = ref(TODO_TYPE.all)

    const isAll = computed(() => todos.value.every((item) => item.finished))
    const unfinishedTodos = computed(() => todos.value.filter((item) => !item.finished))
    const showTodos = computed(() => {
    switch (type.value) {
    case TODO_TYPE.all:
    return todos.value
    case TODO_TYPE.unfinished:
    return todos.value.filter((item) => !item.finished)
    case TODO_TYPE.finished:
    return todos.value.filter((item) => item.finished)
    }
    })

    const addTodo = (text) => {
    todos.value.push({ text, id: todos.value.length + 1, finished: false })
    }
    const delTodo = (i) => {
    if (window.confirm('确认删除么')) {
    todos.value.splice(i, 1)
    }
    }
    const toggleTodo = (flag) => {
    todos.value.forEach((item) => (item.finished = flag))
    }
    const clearTodo = () => {
    todos.value = todos.value.filter((item) => !item.finished)
    }
    const setType = (selectType) => {
    type.value = selectType
    }

    // Setup 写法的关键:最后必须 return 所有需要暴露的变量和方法
    return {
    todos, type, isAll, unfinishedTodos, showTodos,
    addTodo, delTodo, toggleTodo, clearTodo, setType
    }
    })

    两种写法的核心区别:

    • Option 写法:state 中数据通过 this.xxx 访问,结构清晰分层
    • Setup 写法:ref 数据需要通过 .value 访问,最后 return 暴露;优点是可以用 JS 原生语法组织逻辑(比如把相关功能抽成独立函数)

    3.3 功能分步实现

    下面以 Option 写法为例,逐一实现各项功能。


    1)新增待办 —— TodoHeader.vue

    <script setup>
    import { useTodoStore } from '@/store/todo'
    const todoStore = useTodoStore()

    // 监听回车键,触发新增
    const onEnter = (e) => {
    const text = e.target.value.trim() // 去除首尾空格
    if (!text) return alert('任务名称不能为空') // 非空校验
    todoStore.addTodo(text) // 调用仓库新增方法
    e.target.value = '' // 清空输入框
    }
    </script>

    <template>
    <header class="header">
    <h1>todos</h1>
    <input
    @keydown.enter="onEnter"
    class="new-todo"
    placeholder="What needs to be finished?"
    autofocus
    />

    </header>
    </template>

    代码思路:用户在输入框按回车 → 触发 onEnter → 校验输入内容 → 调用仓库的 addTodo 方法 → 仓库往 todos 数组 push 一条新记录 → 所有引用该数组的组件自动更新。


    2)删除待办 —— TodoMain.vue

    删除逻辑在仓库 actions 中已定义好了,组件只需调用即可:

    <button class="destroy" @click="todoStore.delTodo(index)"></button>

    这里传入的 index 来自 v-for 循环的索引,仓库中通过 splice(i, 1) 删除对应项。删除前弹出确认框,防止误操作。


    3)单选与全选双向联动

    这是本案例最核心、最容易出错的功能点,涉及两条数据流:

    流程图解:

    单选(勾选某一条)
    ↓ getters.isAll 自动计算
    → 所有项都 finished=true 时 isAll=true
    → 全选复选框自动勾上(get)

    全选(点击全选框)
    ↓ v-model 触发 set
    → 调用 actions.toggleTodo(flag)
    → 遍历所有项,批量赋 finished=flag(set)

    单选 ↔ 全选 形成闭环联动

    TodoMain.vue 关键代码:

    <script setup>
    import { computed } from 'vue'
    import { useTodoStore } from '@/store/todo'
    const todoStore = useTodoStore()

    // 带 get/set 的完整计算属性,是实现双向联动的关键
    const isAllSelected = computed({
    // get:读取时自动触发 → 返回仓库 isAll 的值 → 控制全选框状态
    get() {
    return todoStore.isAll
    },
    // set:全选框被点击时触发 → 接收新的勾选状态 → 批量更新所有项
    set(flag) {
    todoStore.toggleTodo(flag)
    }
    })
    </script>

    <template>
    <section class="main">
    <!– 全选框通过 v-model 绑定带 get/set 的计算属性 –>
    <input
    v-model="isAllSelected"
    id="toggle-all"
    class="toggle-all"
    type="checkbox"
    />

    <label for="toggle-all">Mark all as complete</label>

    <ul class="todo-list">
    <li
    v-for="(item, index) in todoStore.showTodos"
    :key="item.id"
    :class="{ completed: item.finished }"
    >

    <div class="view">
    <!– 单选框直接 v-model 绑定 finished 字段 –>
    <input v-model="item.finished" class="toggle" type="checkbox" />
    <label>{{ item.text }}</label>
    <button class="destroy" @click="todoStore.delTodo(index)"></button>
    </div>
    </li>
    </ul>
    </section>
    </template>

    为什么用带 get/set 的计算属性? 因为全选框需要"读取"仓库的计算结果,又需要"写入"触发批量更新。普通 computed 只有 get,无法响应 v-model 的双向绑定;写成 computed({ get, set }) 才能让全选框和仓库数据形成闭环。

    仓库中对应的 getter 和 action:

    // getters:判断是否所有项都已勾选
    isAll: (state) => state.todos.every((item) => item.finished),

    // actions:遍历所有项,统一设置 finished 状态
    toggleTodo(flag) {
    this.todos.forEach((item) => (item.finished = flag))
    }


    4)底部统计与清除已完成 —— TodoFooter.vue

    <script setup>
    import { useTodoStore, TODO_TYPE } from '@/store/todo'
    const todoStore = useTodoStore()
    </script>

    <template>
    <footer class="footer">
    <!– 统计未完成任务数:直接用 getter 返回的数组取 length –>
    <span class="todo-count">
    <strong>{{ todoStore.unfinishedTodos.length }}</strong> item left
    </span>

    <!– 筛选标签:点击切换 type,动态绑定 selected 类名实现高亮 –>
    <ul class="filters">
    <li>
    <a
    @click="todoStore.setType(TODO_TYPE.all)"
    href="#/"
    :class="{ selected: todoStore.type === TODO_TYPE.all }"
    >
    All</a>
    </li>
    <li>
    <a
    @click="todoStore.setType(TODO_TYPE.unfinished)"
    href="#/active"
    :class="{ selected: todoStore.type === TODO_TYPE.unfinished }"
    >
    Active</a>
    </li>
    <li>
    <a
    @click="todoStore.setType(TODO_TYPE.finished)"
    href="#/completed"
    :class="{ selected: todoStore.type === TODO_TYPE.finished }"
    >
    Completed</a>
    </li>
    </ul>

    <!– 一键清除已完成任务 –>
    <button class="clear-completed" @click="todoStore.clearTodo()">
    Clear completed
    </button>
    </footer>
    </template>

    功能解析:

    • 统计:unfinishedTodos 是 getters 中 filter 的结果,取 .length 即可得到未完成任务数量。因为是计算属性,数据变化时自动更新。
    • 清除已完成:调用 clearTodo(),内部用 filter 过滤掉 finished: true 的项并重新赋值,触发响应式更新。
    • 筛选高亮:仓库 state.type 记录当前筛选类型,点击 a 标签时调用 setType() 切换;模板中通过 :class="{ selected: todoStore.type === TODO_TYPE.xxx }" 动态添加高亮样式。

    5)筛选 + 列表数据切换

    这个功能的核心在于 getters 中的 showTodos:

    showTodos: (state) => {
    switch (state.type) {
    case TODO_TYPE.all:
    return state.todos // 全量
    case TODO_TYPE.unfinished:
    return state.todos.filter((item) => !item.finished) // 未完成
    case TODO_TYPE.finished:
    return state.todos.filter((item) => item.finished) // 已完成
    }
    }

    工作流程:用户点击筛选标签 → setType() 修改 state.type → showTodos 计算属性自动重新计算 → TodoMain 中 v-for="item in todoStore.showTodos" 拿到新数据 → 列表重渲染。

    要点:我们没有真的删除数据或创建新数组,只是基于 type + 原始 todos 计算出一个派生视图。原始数据始终完整保留,筛选只是"换个角度看数据"。这种模式在 Pinia 中非常常见。


    全文总结

    本文从零开始,系统梳理了 Pinia 的核心知识体系:

  • 概念层:Pinia 是什么、解决什么问题、适用于什么场景
  • 语法层:四步项目引入、仓库定义的两种写法(Option 对象式 vs Setup 函数式)、组件内三步入库
  • 实战层:以 Todo List 为例,完整演示了组件化拆分 → 仓库设计 → 新增/删除/单选全选联动/统计筛选等功能的实现,以及两种仓库写法的对比

  • 核心知识点复盘

    知识点关键内容
    Pinia 定位 Vue3 官方状态管理工具,替代 Vuex
    四步引入 import → createPinia() → app.use(pinia) → mount
    defineStore 参数 仓库唯一 id + Setup 函数 或 Option 对象
    Option 三大件 state(数据)→ getters(计算)→ actions(修改)
    Setup 写法 ref/reactive → computed → 普通函数 → 最后 return 暴露
    组件使用三步骤 导入仓库 → 调用获取实例 → 读取数据/调用方法
    全选联动 带 get/set 的 computed + getters 计算全选状态 + actions 批量更新
    筛选模式 state 记录筛选类型,getters 根据类型动态过滤,不修改原始数据

    本文基于 Vue3 + Pinia 实战项目整理,代码完整可运行,适合作为学习复盘和技术分享的参考资料。

    赞(0)
    未经允许不得转载:171主机测评 » 跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址