Skip to content

19 Vue3 状态管理 Pinia

Pinia 简介

Pinia (发音为 /piːnjʌ/,类似英文中的“peenya”) 是最接近有效包名 piña (西班牙语中的 pineapple,即“菠萝”) 的词。菠萝花实际上是一组各自独立的花朵,它们结合在一起,由此形成一个多重的水果。与 Store 类似,每一个都是独立诞生的,但最终它们都是相互联系的。它 (菠萝) 也是一种原产于南美洲的美味热带水果。

什么是 Pinia

Pinia 是 Vue 的专属的最新状态管理库 ,是 Vuex 状态管理工具的替代品

  1. 提供更加简单的 APl(去掉了 mutation)
  2. 提供符合组合式风格的 APl(和 Vue3 新语法统一)
  3. 去掉了 modules 的概念,每一个 store 都是一个独立的模块
  4. 配合 TypeScript 更加友好,提供可靠的类型推断

为什么使用 Pinia

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。如果你熟悉组合式 API 的话,你可能会认为可以通过一行简单的 export const state = reactive({}) 来共享一个全局状态。对于单页应用来说确实可以,但如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。而如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:

  • Devtools 支持
    • 追踪 actions、mutations 的时间线
    • 在组件中展示它们所用到的 Store
    • 让调试更容易的 Time travel
  • 热更新
    • 不必重载页面即可修改 Store
    • 开发时可保持当前的 State
  • 插件:可通过插件扩展 Pinia 功能
  • 为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
  • 支持服务端渲染

对比 Vuex

Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的。

Pinia API 与 Vuex(<=4) 也有很多不同,即:

  • mutation 已被弃用。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。
  • 无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理。
  • 无过多的魔法字符串注入,只需要导入函数并调用它们,然后享受自动补全的乐趣就好。
  • 无需要动态添加 Store,它们默认都是动态的,甚至你可能都不会注意到这点。注意,你仍然可以在任何时候手动使用一个 Store 来注册它,但因为它是自动的,所以你不需要担心它。
  • 不再有嵌套结构的模块。你仍然可以通过导入和使用另一个 Store 来隐含地嵌套 stores 空间。虽然 Pinia 从设计上提供的是一个扁平的结构,但仍然能够在 Store 之间进行交叉组合。你甚至可以让 Stores 有循环依赖关系
  • 不再有可命名的模块。考虑到 Store 的扁平架构,Store 的命名取决于它们的定义方式,你甚至可以说所有 Store 都应该命名。

关于如何将现有 Vuex(<=4) 的项目转化为使用 Pinia 的更多详细说明,请参阅 Vuex 迁移指南

Pinia 使用

Pinia | Pinia

安装

bash
pnpm add pinia

以后项目中可以直接使用 pnpm create vue@latest 在项目初始化时自动添加。

创建 pinia 实例

js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

// 创建 Pinia 实例
const pinia = createPinia()
// 创建根实例
const app = createApp(App)
// 使用 Pinia 实例
app.use(pinia)
// 挂载 app 视图
app.mount('#app')

定义 Store

  • 创建一个 store 文件夹,在里面创建一个 xxx.js 文件。store 文件名一般和 id 相同。

  • Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的 ID。Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use... 是一个符合组合式函数风格的约定。

  • defineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象。

    js
    // store/alerts.js
    import { defineStore } from 'pinia'
    
    // 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
    // 第一个参数是你的应用中 Store 的唯一 ID。
    export const useAlertsStore = defineStore('alerts', {
      // 其他配置...
    })
  • 你可以定义任意多的 store,但为了让使用 pinia 的益处最大化 (比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store

Option Store

  • 与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象。

    js
    // store/counter.js
    export const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      getters: {
        double: (state) => state.count * 2,
      },
      actions: {
        increment() {
          this.count++
        },
      },
    })
  • 你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

  • 为方便上手使用,Option Store 应尽可能直观简单。

Setup Store

  • 与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

    js
    export const useCounterStore = defineStore('counter', () => {
      // 数据 state
      const count = ref(0)
      // 计算属性 getters
      const doubleCount = computed(() => count.value * 2)
      // 修改数据的方法 actions
      function increment() {
        count.value++
      }
    
      // 以对象形式返回
      return { count, doubleCount, increment }
    })
  • Setup Store 中:

    • ref() 就是 state 属性
    • computed() 就是 getters
    • function() 就是 actions

    Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

组件使用 Store

vue
<script setup>
  // 引入 useCounterStore 函数
  import { useCounterStore } from '@/stores/counter'
  // 通过 useCounterStore() 函数来获取 store 实例
  // 可以在组件中的任意位置访问 `store` 变量 ✨
  const store = useCounterStore()

  // `name` 和 `doubleCount` 是响应式的 ref
  // 同时通过插件添加的属性也会被提取为 ref
  // 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
  const { name, doubleCount } = storeToRefs(store)
  // 作为 action 的 increment 可以直接解构
  const { increment } = store
</script>
<template>
  <div>
    <h1>{{ name }}</h1>
    <p>Count: {{ store.count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>
  • 一旦 store 被实例化,你可以直接访问在 store 的 stategettersactions 中定义的任何属性。
  • 请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value,就像 setup 中的 props 一样,如果你写了,我们也不能解构它

案例 计数器

  1. 定义 Store(state + action)
  2. 组件使用 Store
js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 数据 state
  const count = ref(0)
  // 修改数据的方法 actions
  const autoIncrement = () => {
    const timer = setInterval(() => {
      count.value++
      if (count.value >= 20)
        clearInterval(timer)
    }, 300)
  }
  // 以对象形式返回
  return { count, autoIncrement }
})
vue
<script setup>
// 引入 useCounterStore 函数
import { useCounterStore } from '@/store/counter.js'

// 通过 useCounterStore() 函数来获取 store 实例
const counterStore = useCounterStore()
</script>

<template>
  <button @click="counterStore.autoIncrement">
    Auto Increment
  </button>
  <p>{{ counterStore.count }}</p>
</template>

State 状态

State | Pinia

  • 在大多数情况下,state 都是你的 store 的核心。人们通常会先定义能代表他们 APP 的 state。
  • 在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端和客户端。
js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Zhang san')
  const array = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  const hasMore = ref(true)
  const autoIncrement = () => {
    const timer = setInterval(() => {
      count.value++
      if (count.value >= 20)
        clearInterval(timer)
    }, 300)
  }
  const $reset = () => count.value = 0
  return { count, name, array, hasMore, autoIncrement, $reset }
})
vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/store/counter.js'

const counterStore = useCounterStore()
console.log(counterStore)
console.log('counterStore.count', counterStore.count)
console.log('counterStore.name', counterStore.name)
console.log('counterStore.array', counterStore.array)
console.log('counterStore.hasMore', counterStore.hasMore)

const { autoIncrement, $reset } = counterStore
const { count, name, array, hasMore } = storeToRefs(counterStore)
</script>

<template>
  <button @click="autoIncrement">
    Auto Increment
  </button>
  <button @click="$reset">
    Reset Count
  </button>
  <p> Count: {{ count }} </p>
  <p> Name: {{ name }} </p>
  <p> Array: {{ array }} </p>
  <p> Has More: {{ hasMore }} </p>
</template>

Getter 计算属性

Getter | Pinia

  • Pinia 中的 getters 直接使用 computed 函数 进行模拟,组件中需要使用需要把 getters return 出去
  • Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数。
  • 大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例但 (在 TypeScript 中) 必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter
  • Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数。
js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const array = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  const autoIncrement = () => {
    const timer = setInterval(() => {
      count.value++
      if (count.value === 20)
        clearInterval(timer)
    }, 300)
  }
  const evenOrOdd = computed(() => (count.value % 2 === 0 ? 'even' : 'odd'))
  const evenArray = computed(() => array.value.filter(n => n % 2 === 0))
  const autoAddToArray = () => {
    const timer = setInterval(() => {
      array.value.push(array.value.length + 1)
      if (array.value.length === 20)
        clearInterval(timer)
    }, 300)
  }
  const $reset = () => {
    count.value = 0
    array.value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  }
  return { count, array, autoIncrement, evenOrOdd, evenArray, autoAddToArray, $reset }
})
vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/store/counter.js'

const counterStore = useCounterStore()
console.log(counterStore)

const { autoAddToArray, $reset } = counterStore
const { count, array, evenOrOdd, evenArray } = storeToRefs(counterStore)
</script>

<template>
  <p>count: {{ count }} | {{ evenOrOdd }}</p>
  <p>array: {{ array }}</p>
  <p>evenArray: {{ evenArray }}</p>
  <button @click="counterStore.autoIncrement">
    Auto Increment
  </button> &nbsp;&nbsp;
  <button @click="autoAddToArray">
    autoAddToArray
  </button> &nbsp;&nbsp;
  <button @click="$reset">
    reset
  </button>
</template>

Action 异步操作

  • 异步 action 函数的写法和组件中获取异步数据的写法完全一致
  • Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
  • 类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注 (以及自动补全 ✨)不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!

  • 案例需求:在 Pinia 中获取频道列表数据并把数据渲染 App 组件的模板中
  • 接口地址:http://geek.itheima.net/v1_0/channels
js
import axios from 'axios'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useChannelStore = defineStore('channel', () => {
  const channels = ref([])
  const fetchChannels = async () => {
    const { data } = await axios.get('http://geek.itheima.net/v1_0/channels')
    channels.value = data.data.channels
  }
  return { channels, fetchChannels }
})
vue
<script setup>
import { useChannelStore } from '@/store/channel.js'

const channelStore = useChannelStore()
console.log(channelStore)
</script>

<template>
  <el-card class="box-card">
    <template #header>
      <div class="card-header">
        <h3>频道列表</h3>
        <el-button round size="large" type="primary" @click="channelStore.fetchChannels">
          获取频道列表数据
        </el-button>
      </div>
    </template>
    <div v-if="channelStore.channels.length">
      <div v-for="item in channelStore.channels" :key="item.id" class="text item">
        {{ `${item.id} - ${item.name}` }}
      </div>
    </div>
    <div v-else class="text item">
      0 - xxx
    </div>
  </el-card>
</template>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.text {
  font-size: 14px;
}

.item {
  margin-bottom: 18px;
}

.box-card {
  width: 480px;
}
</style>

storeToRefs 工具函数

storeToRefs | Pinia

  • 创建一个引用对象,包含 store 的所有 state、getter 和 plugin 添加的 state 属性。
  • 类似于 toRefs(),但专门为 Pinia store 设计,所以 method 和非响应式属性会被完全忽略。
  • storeToRefs 会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性。非响应式数据和 action 可以直接解构。
js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 响应式数据
  const count = ref(0)
  // 普通数据
  const name = 'Zhang san'
  // 方法 action
  const increment = () => count.value++

  const autoIncrement = () => {
    const timer = setInterval(() => {
      count.value++
      if (count.value >= 20)
        clearInterval(timer)
    }, 300)
  }
  return { count, name, increment, autoIncrement }
})
vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/store/counter.js'

const counterStore = useCounterStore()
console.log(counterStore)

// 响应式数据通过 storeToRefs 函数转换后可以结构出来,同时保持数据的响应式
const { count } = storeToRefs(counterStore)
// 非响应式数据和 Action(方法)可以直接结构出来
const { name, increment, autoIncrement } = counterStore
</script>

<template>
  <p> Counter: {{ count }} </p>
  <p> Name: {{ name }} </p>
  <button @click="increment">
    Increment
  </button> &nbsp;
  <button @click="autoIncrement">
    Auto Increment
  </button>
</template>

Pinia 持久化插件

Home | pinia-plugin-persistedstate

GitHub - prazdevs/pinia-plugin-persistedstate: 🍍 Configurable persistence and rehydration of Pinia stores.

pinia-plugin-persistedstate 介绍

persistedstate 丰富的功能可以使 Pinia Store 的持久化更易配置:

  • vuex-persistedstate 相似的 API
  • 所有 Store 均可单独配置
  • 自定义 storage 和数据序列化
  • 恢复持久化数据前后的 hook
  • 每个 Store 具有丰富的配置
  • 兼容 Vue 2 和 3
  • 无任何外部依赖

插件使用

  1. 安装插件 pinia-plugin-persistedstate。

    bash
    pnpm add pinia-plugin-persistedstate
  2. 将插件添加到 pinia 实例上。

    js
    // main.js
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    // 导入持久化的插件
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    
    import App from './App.vue'
    // 创建 Pinia 实例
    const pinia = createPinia()
    // 创建根实例
    const app = createApp(App)
    // pinia 插件的安装配置
    app.use(pinia.use(piniaPluginPersistedstate))
    // 视图的挂载
    app.mount('#app')
  3. Store 持久化配置。

    js
    import { defineStore } from 'pinia'
    
    export const useStore = defineStore(
      'main',
      () => {
        const someState = ref('你好 pinia')
        return { someState }
      },
      {
        persist: true,
      },
    )

默认配置

常用自定义配置

js
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => ({
    someState: '你好 pinia',
  }),
  persist: {
    // 在这里进行自定义配置
  },
})
  • key:自定义存储的 key。默认为 store.$id。Key 用于引用 storage 中的数据。

    js
    import { defineStore } from 'pinia'
    
    export const useStore = defineStore('store', {
      state: () => ({
        someState: '你好 pinia',
      }),
      persist: {
        key: 'my-custom-key',
      },
    })

    这个 Store 将被持久化存储在 localStorage 中的 my-custom-key key 中。

  • storage:自定义存储方式。默认为 localStorage

    js
    import { defineStore } from 'pinia'
    
    export const useStore = defineStore('store', {
      state: () => ({
        someState: '你好 pinia',
      }),
      persist: {
        storage: sessionStorage,
      },
    })

    这个 store 将被持久化存储在 sessionStorage中。

  • path:自定义存储路径。默认为整个 state。

    js
    import { defineStore } from 'pinia'
    
    export const useStore = defineStore('store', {
      state: () => ({
        save: {
          me: 'saved',
          notMe: 'not-saved',
        },
        saveMeToo: 'saved',
      }),
      persist: {
        paths: ['save.me', 'saveMeToo'],
      },
    })

    这个 store 中只有 save.mesaveMeToo 被持久化,而 save.notMe 不会被持久化。

全局持久化配置

在安装插件之后,你可以使用 createPersistedState 来初始化插件。这些配置将会成为项目内所有 Store 的默认选项。

js
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'

const pinia = createPinia()

pinia.use(
  createPersistedState({
    storage: sessionStorage,
  }),
)

每个声明 persist: true 的 Store 都会默认将数据持久化到 sessionStorage 中。

全局 key 配置

全局 key 配置接受传入 Store key 的函数,并返回一个新的 storage 密钥。

js
// main.js
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'

const pinia = createPinia()

pinia.use(
  createPersistedState({
    key: (id) => `__persisted__${id}`,
  }),
)
js
// store.js
import { defineStore } from 'pinia'

defineStore('store', {
  state: () => ({ saved: '' }),
  persist: true,
})
  • store 将保存在 __persisted__store key 下,而不是 store 下。
  • 当你需要在全局范围内对所有 Store key 添加前缀/后缀时,应考虑此选项。

示例代码

js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { ElButton, ElCard, ElRadio, ElRadioButton, ElRadioGroup, ElTable, ElTableColumn, ElText } from 'element-plus'

// 导入持久化的插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

// 创建 Pinia 实例
const pinia = createPinia()
// 创建根实例
const app = createApp(App)
// 使用 Pinia 实例
app.use(pinia)
app.use(ElCard)
app.use(ElText)
app.use(ElButton)
app.use(ElTable)
app.use(ElTableColumn)
app.use(ElRadioGroup)
app.use(ElRadio)
app.use(ElRadioButton)

// pinia 插件的安装配置
app.use(pinia.use(piniaPluginPersistedstate))

// 挂载 app 视图
app.mount('#app')
js
import axios from 'axios'
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useChannelStore = defineStore(
  'channel',
  () => {
    const channels = ref([])
    const fetchChannels = async () => {
      const { data } = await axios.get('http://geek.itheima.net/v1_0/channels')
      channels.value = data.data.channels
    }
    return { channels, fetchChannels }
  },
  {
    // 使用默认配置
    persist: true,
  },
)
js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore(
  'counter',
  () => {
    const count = ref(0)
    const name = ref('Zhang san')
    // 方法 action
    const increment = () => count.value++
    const changeName = newName => name.value = newName

    const autoIncrement = () => {
      const timer = setInterval(() => {
        count.value++
        if (count.value >= 20)
          clearInterval(timer)
      }, 300)
    }
    return { count, name, increment, changeName, autoIncrement }
  },
  {
    // pinia-plugin-persistedstate 插件配置
    persist: {
      // key: 自定义存储的 key
      key: 'pinia-persistedstate-counter',
      // storage: 存储方式,默认 localStorage
      storage: localStorage,
      // paths: 需要持久化的数据
      paths: ['count'],
    },
  },
)
js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useStudentStore = defineStore(
  'student',
  () => {
    const studentList = [
      { id: 1, name: '赵博', age: 16, sex: '男', class: '高三二班' },
      { id: 2, name: '李丽丽', age: 14, sex: '女', class: '高三一班' },
      { id: 3, name: '武婷', age: 17, sex: '女', class: '高一二班' },
      { id: 4, name: '高强', age: 18, sex: '女', class: '高三一班' },
      { id: 5, name: '王桂珍', age: 14, sex: '男', class: '高三一班' },
      { id: 6, name: '叶娜', age: 17, sex: '女', class: '高二一班' },
      { id: 7, name: '陈海燕', age: 17, sex: '男', class: '高三一班' },
      { id: 8, name: '张凤兰', age: 14, sex: '女', class: '高三二班' },
      { id: 9, name: '蔡成', age: 15, sex: '女', class: '高三一班' },
      { id: 10, name: '敖萍', age: 18, sex: '女', class: '高三二班' },
    ]
    const addStudent = ([
      { id: 11, name: '许鹏', age: 14, sex: '男', class: '高三一班' },
      { id: 12, name: '李杨', age: 14, sex: '男', class: '高三二班' },
      { id: 13, name: '华莹', age: 18, sex: '女', class: '高三一班' },
      { id: 14, name: '何兰英', age: 17, sex: '女', class: '高三二班' },
      { id: 15, name: '乔亮', age: 15, sex: '男', class: '高二一班' },
      { id: 16, name: '傅帆', age: 16, sex: '女', class: '高三一班' },
      { id: 17, name: '李杨', age: 16, sex: '女', class: '高三一班' },
      { id: 18, name: '陈霞', age: 18, sex: '女', class: '高一一班' },
      { id: 19, name: '柳琴', age: 16, sex: '女', class: '高三一班' },
      { id: 20, name: '王明', age: 14, sex: '男', class: '高三一班' },
      { id: 21, name: '冯龙', age: 15, sex: '女', class: '高一一班' },
      { id: 22, name: '郭勇', age: 15, sex: '男', class: '高一一班' },
      { id: 23, name: '余燕', age: 17, sex: '女', class: '高三一班' },
      { id: 24, name: '范琳', age: 15, sex: '男', class: '高三一班' },
      { id: 25, name: '吕慧', age: 14, sex: '女', class: '高一一班' },
      { id: 26, name: '赵桂珍', age: 15, sex: '男', class: '高三一班' },
    ])

    const students = ref(studentList)
    const addStudentAction = () => students.value.push(...addStudent)
    const resetStudentAction = () => students.value = studentList
    const logStudentList = () => console.log(students.value)
    const DeleteStudentAction = student => students.value = students.value.filter(item => item.id !== student.id)

    return { students, addStudentAction, resetStudentAction, logStudentList, DeleteStudentAction }
  },
  {
    persist: {
      key: 'pinia-student',
      paths: ['students'],
    },
  },
)
vue
<script setup>
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useChannelStore } from '@/store/channel.js'
import { useCounterStore } from '@/store/counter.js'
import { useStudentStore } from '@/store/student.js'

const channelStore = useChannelStore()
const { channels } = storeToRefs(channelStore)
const { fetchChannels } = channelStore

const counterStore = useCounterStore()
const { count, name } = storeToRefs(counterStore)
const { increment, changeName, autoIncrement } = counterStore

const studentStore = useStudentStore()
const { students } = storeToRefs(studentStore)
const { addStudentAction, resetStudentAction, logStudentList, DeleteStudentAction } = studentStore

const tableLayout = ref('fixed')
</script>

<template>
  <p> Counter: {{ count }} </p>
  <p> Name: {{ name }} </p>
  <el-button type="primary" @click="increment">
    Increment
  </el-button> &nbsp;
  <el-button type="info" @click="autoIncrement">
    Auto Increment
  </el-button> &nbsp;
  <el-button plain type="info" @click="changeName('Li si')">
    Change Name
  </el-button>
  <p>
    <label> Input Name: <input v-model="name" @change="changeName"> </label>
  </p>
  <hr>

  <el-card class="box-card">
    <template #header>
      <div class="card-header">
        <span>频道列表</span>
        <el-button round size="large" type="primary" @click="fetchChannels">
          获取频道列表数据
        </el-button>
      </div>
    </template>
    <div v-if="channels.length > 0">
      <el-text v-for="item in channels" :key="item.id" class="mx-1" size="large">
        {{ `${item.name}` }} &nbsp;
      </el-text>
    </div>
    <div v-else class="text item">
      频道列表数据
    </div>
  </el-card>
  <hr>
  <el-radio-group v-model="tableLayout">
    <el-radio-button label="fixed" />
    <el-radio-button label="auto" />
  </el-radio-group>
  <el-table
    :data="students" :default-sort="{ prop: 'id', order: 'descending' }" :table-layout="tableLayout" border
    height="250" stripe style="width: 100%"
  >
    <el-table-column label="Id" prop="id" sortable />
    <el-table-column label="Name" prop="name" sortable />
    <el-table-column label="Age" prop="age" sortable />
    <el-table-column label="Sex" prop="sex" sortable />
    <el-table-column label="Class" prop="class" />
    <el-table-column label="Operations">
      <template #default="scope">
        <el-button type="danger" @click="DeleteStudentAction(scope.row)">
          Delete
        </el-button>
      </template>
    </el-table-column>
  </el-table>
  <div style="margin-top: 20px">
    <el-button plain type="primary" @click.once="addStudentAction">
      添加学生
    </el-button>
    <el-button type="success" @click="resetStudentAction">
      重置学生数据
    </el-button>
    <el-button type="danger" @click="logStudentList">
      打印学生数据
    </el-button>
  </div>
</template>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.text {
  font-size: 14px;
}

.item {
  margin-bottom: 18px;
}

.box-card {
  width: 480px;
}
</style>

总结

  1. Pinia 是用来做什么的?

    新一代的状态管理工具,替代 vuex

  2. Pinia 中还需要 mutation 吗?

    不需要,action 既支持同步也支持异步

  3. Pinia 如何实现 getter?

    computed 计算属性函数

  4. Pinia 产生的 Store 如何解构赋值数据保持响应式?

    storeToRefs 工具函数

  5. Pinia 如何快速实现持久化?

    pinia-plugin-persistedstate 插件