Pinia

简介

  • pinia是一个状态管理工具
  • pinia是 Vuex4 的升级版,也就是 Vuex5

官网:https://pinia.vuejs.org/

相比于vuex4,pinia有很大改进,即:

  • mutation 已被弃用,Actions同步异步都支持
  • 无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理。
  • 无需要动态添加 Store,它们默认都是动态的
  • 不再有嵌套结构的模块
  • 不再有可命名的模块

文件结构区别

# Vuex 示例(假设是命名模块)。
src
└── store
    ├── index.js           # 初始化 Vuex,导入模块
    └── modules
        ├── module1.js     # 命名模块 'module1'
        └── nested
            ├── index.js   # 命名模块 'nested',导入 module2 与 module3
            ├── module2.js # 命名模块 'nested/module2'
            └── module3.js # 命名模块 'nested/module3'

# Pinia 示例,注意 ID 与之前的命名模块相匹配
src
└── stores
    ├── index.js          # (可选) 初始化 Pinia,不必导入 store
    ├── module1.js        # 'module1' id
    ├── nested-module2.js # 'nested/module2' id
    ├── nested-module3.js # 'nested/module3' id
    └── nested.js         # 'nested' id

安装

yarn add pinia
# 或者使用 npm
npm install pinia

封装

/src/stores/index.js

import { createPinia } from "pinia" //引入pinia
 
const pinia = createPinia() //创建pinia实例
 
export default pinia //导出pinia用于main.js注册

/main.js

import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'  //引入
 
 const app = createApp(App)
 //使用
app.use(pinia)
app.mount('#app')

Store

Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字

import { defineStore } from 'pinia'

// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
  // 其他配置...
})

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

Option Store

类似于vue2的选项式api,可以传入一个带有 stateactionsgetters 属性的 Option 对象

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'Eduardo' }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

可以对照着来看,state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

Setup Store

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

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

使用Store

<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>

store 被实例化后,就可以直接访问在 store 的 stategettersactions 中定义的任何属性

注意store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。就像 setup 中的 props 一样,我们不能对它进行解构

<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
  store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>

解构访问

官方提供了一个方法storeToRefs()可以进行解构访问,原理就是恢复响应式

import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
// const { name, doubleCount} = userStore //直接结构赋值  不是响应式
// const { name, doubleCount} = toRefs(userStore) // 响应式
const { name, doubleCount } = storeToRefs(store) // 响应式
// 作为 action 的 increment 可以直接解构
const { increment } = store

State

import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // 为了完整类型推理,推荐使用箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断出它们的类型
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
}

访问state

const store = useStore()

store.count++

注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。例如:如果 secondCount 没有在 state() 中定义,我们无法执行 store.secondCount = 2

//也可以在组件中通过computed获得state
const count = computed(()=>store.count)

这样做有很多好处,例如:

  • 响应式更新:
    computed属性是响应式的。这意味着当menuStore.routerList发生变化时,任何使用这个computed属性的组件都会自动更新。这确保了UI的实时性和准确性。
  • 性能优化:
    使用computed可以避免不必要的计算和渲染。Vue会缓存computed属性的结果,只要其依赖的响应式数据没有变化,Vue就不会重新计算这个computed属性的值。这减少了不必要的计算开销,提高了应用的性能。
  • 代码可读性:
    通过computed属性,你可以将复杂的逻辑从模板或方法中抽象出来,使得模板更加简洁和易于理解。同时,这也使得状态的计算逻辑更加集中和明确。
  • 逻辑复用:
    computed属性可以被多个组件复用。如果你有多个组件需要访问menuStore.routerList的转换或计算结果,你可以在一个地方定义这个computed属性,然后在多个组件中引用它,避免了代码的重复。
  • 简化状态管理:
    使用computed属性可以帮助你更好地组织和管理应用中的状态。你可以将状态的计算逻辑与组件的逻辑分开,使得状态管理更加清晰和模块化。
  • 避免直接修改store中的state:
    使用computed属性获取state,而不是直接引用或修改store中的state,可以帮助你遵循Vuex或Pinia的单一状态树原则,减少状态管理的复杂性,并避免潜在的bug。

重置state

使用选项式 API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。

const store = useStore()
store.$reset()

$reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。

Setup Stores 中,您需要创建自己的 $reset() 方法:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function $reset() {
    count.value = 0
  }
  return { count, $reset }
})

使用选项式api

// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
})

如果你想修改这些 state 属性 (例如,如果你有一个表单),你可以使用 mapWritableState() 作为代替。但注意你不能像 mapState() 那样传递一个函数:

import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
  computed: {
    // 可以访问组件中的 this.count,并允许设置它。
    // this.count++
    // 与从 store.count 中读取的数据相同
    ...mapWritableState(useCounterStore, ['count'])
    // 与上述相同,但将其注册为 this.myOwnName
    ...mapWritableState(useCounterStore, {
      myOwnName: 'count',
    }),
  },
}

变更state

除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。它允许在同一时间更改多个属性:

一般用法

store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

函数用法

用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。

替换state

store.$state = { count: 24 }

这种方法是错误的,因为这样会破坏其响应性
所以只能用patch方法进行修改

// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })

订阅state

类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。比起普通的 watch(),使用 $subscribe() 的好处是 subscriptionspatch 后只触发一次 (例如,当使用上面的函数版本时)。

cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // 和 cartStore.$id 一样
  mutation.storeId // 'cart'
  // 只有 mutation.type === 'patch object'的情况下才可用
  mutation.payload // 传递给 cartStore.$patch() 的补丁对象。

  // 每当状态发生变化时,将整个 state 持久化到本地存储。
  localStorage.setItem('cart', JSON.stringify(state))
})

默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离

<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$subscribe(callback, { detached: true })
</script>

Getter

Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数:

如何定义

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

在组件中使用

<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

向getter传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:

export const useUserListStore = defineStore('userList', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

在组件中使用:

<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>

这样用的话,getter不再缓存,只是一个被调用的函数。但是,我们也可以在getter中缓存一些结果,可以提高性能

export const useUserListStore = defineStore('userList', {
  getters: {
    getActiveUserById(state) {
    //先过滤出active
      const activeUsers = state.users.filter((user) => user.active)
      //再找id
      return (userId) => activeUsers.find((user) => user.id === userId)
    },
  },
})

访问其他store中getter

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

使用`setup() 时的用法

<script setup>
const store = useCounterStore()
store.count = 3
//直接访问getter
store.doubleCount // 6
</script>

使用选项式 API 的用法

// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
})

使用 setup()

setup() 钩子可以使 Pinia 在选项式 API 中更易用。并且不需要额外的映射辅助函数!

<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
  setup() {
    const counterStore = useCounterStore()
    return { counterStore } }},
  computed: {
    quadrupleCounter() {
      return this.counterStore.doubleCount * 2
    },
  },
})

不使用setup()

你可以使用前一节的 state 中的 mapState() 函数来将其映射为 getters:

import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 允许在组件中访问 this.doubleCount
    // 与从 store.doubleCount 中读取的相同
    ...mapState(useCounterStore, ['doubleCount']),
    // 与上述相同,但将其注册为 this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'doubleCount',
      // 你也可以写一个函数来获得对 store 的访问权
      double: (store) => store.doubleCount,
    }),
  },
}

Action

Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

export const useCounterStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 的action。

在页面使用
Action 可以像函数或者通常意义上的方法一样被调用:

<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
  <!-- 即使在模板中也可以 -->
  <button @click="store.randomizeCounter()">Randomize</button>
</template>

访问其他 store 的 action

访问其他store中的action,直接调用即可

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

使用 setup()

<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
  setup() {
    const counterStore = useCounterStore()
    return { counterStore }
  },
  methods: {
    incrementAndPrint() {
      this.counterStore.increment()
      console.log('New Count:', this.counterStore.count)
    },
  },
})
</script>

不使用 setup()

import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  methods: {
    // 访问组件内的 this.increment()
    // 与从 store.increment() 调用相同
    ...mapActions(useCounterStore, ['increment'])
    // 与上述相同,但将其注册为this.myOwnName()
    ...mapActions(useCounterStore, { myOwnName: 'increment' }),
  },
}

插件

介绍: 插件通过 pinia.use() 添加到 pinia 实例。
例子:将一个静态属性添加到所有 store。

import { createPinia } from 'pinia'

// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia() 
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin)

// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'

可用于添加全局属性

持久化存储插件

安装

npm i pinia-plugin-persistedstate

添加到pinia实例上

在/src/stores/index.js中

import { createPinia } from "pinia" //引入pinia
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' //引入持久化插件
 
const pinia = createPinia() //创建pinia实例
pinia.use(piniaPluginPersistedstate) //将插件添加到 pinia 实例上
 
export default pinia //导出pinia用于main.js注册

使用

基本使用
创建 Store 时,将 persist 选项设置为 true,整个 Store 将使用默认持久化配置保存。

import { defineStore } from "pinia"
 
const useUserStore = defineStore('userInfo', {
  state: () => ({
  age:18,
  sex:1
  }),
  getters: {
        ...........
  },
  action:{
    .........
  },
  persist: true,
})
 
export default useUserStore

高级使用
有三个常用属性

key:存储名称。

storage:存储方式。

path:用于指定 state 中哪些数据需要被持久化。[] 表示不持久化任何状态,undefined 或 null 表示持久化整个state。

 
import { defineStore } from "pinia"
 
const useUserStore = defineStore('userInfo', {
  state: () => ({
    age: 23,
    sex:1
  }),
  getters: {
        ...........
  },
  action:{
    .........
  },
  persist: {
      key: 'userInfo', //存储名称
      storage: sessionStorage, // 存储方式
      paths: ['age', 'sex'], //指定 state 中哪些数据需要被持久化。[] 表示不持久化任何状态,undefined 或 null 表示持久化整个 state
  },
})
 
export default useUserStore

在组件外使用

在setup中开箱即用,但是如果在组件外使用,要等pinia挂载完才能使用
例如在main.js中

import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'

// ❌  失败,因为它是在创建 pinia 之前被调用的
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ 成功,因为 pinia 实例现在激活了
const userStore = useUserStore()

创建 const userStore = useUserStore()实例一定要在 app.use(pinia)之后

在路由配置中

import { createRouter } from 'vue-router'
const router = createRouter({
  // ...
})

// ❌ 由于引入顺序的问题,这将失败
const store = useStore()

router.beforeEach((to, from, next) => {
  // 我们想要在这里使用 store
  if (store.isLoggedIn) next()
  else next('/login')
})

router.beforeEach((to) => {
  // ✅ 这样做是可行的,因为路由器是在其被安装之后开始导航的,
  // 而此时 Pinia 也已经被安装。
  const store = useStore()

  if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值