# Pinia使用

vue3官方推出新的状态管理器pinia来代替vuex,相比较vuex具有很多优点。

  • 无需嵌套使用,更扁平化,再创建好pinia实例挂载到vue上后,就可以同时定义多个store,然后在各个组件中引入store就可以使用,各个store之间也可以相互引入使用。不像vuex那样还要通过模块引入,层层获取。

  • 弃用mutation,直接使用state就可以读写数据

  • 不在需要使用map辅助函数,直接导入函数就可以调用

  • 更适用于组合式API方式使用

# 创建实例

安装:

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

在src目录创建 stores 文件来创建pinia实例,来区分vuex的store,也更容易形容pinia可以同时创建多个store。

├── src
│   ├── stores
│   │   ├── index.ts             # pinia实例
│   │   ├── modules              # 模块
│   │   │   ├── app              # app store
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   │
│   │   │   ├── public           # public store
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts

创建实例:

// stores/index.ts
import { createPinia } from 'pinia'

const store = createPinia()

export default store

引入:

// main.ts
import { createApp } from 'vue'

import App from './App.vue'
import router from './router'
import store from './stores'

const app = createApp(App)

app.use(store)
app.use(router)

app.mount('#app')

# 定义 Store

pinia定义多个store,这个多个store是什么意思呢,我们先来看看之前的vuex,使用vuex时我们只能创建一个store实例,如果要分模块,只能通过modules来创建不同的模块或者通过命名空间来区分,引入和使用都是通过这个store实例,层层获取和调用,所以才会有computed和map辅助函数来帮助区分使用,显然这在多模块下使用很麻烦。

而在pinia中,把创建好的pinia实例挂载到vue实例上后我们就不需要关注这个实例了,我们只需要关心如何去创建多个store,pinia实例会自动帮我们把创建的store注册好,一个store就是一个vuex,不用在区分模块。各个store之间互不关联,使用哪个引入哪个,其数据是保持全局状态的。

定义store我们使用 defineStore() 来定义,其接收两个参数,第一参数是名字,第二个参数是定义该store的值,有两种定义方式setup函数式和选项式。

defineStore() 定义好的返回值名称最好使用store的名称,并且以 use 开头,以 Store 结尾,比如 useAppStoreuseUserStore

import { defineStore } from 'pinia'

// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAppStore = defineStore('app', {
  state: () => {
    return {
      theme: '',
      userList: []
    }
  },

  getters: {
    userLength(state) {
      return state.userList.length
    }
  },

  actions: {
    switchTheme(color) {
      this.theme = color
    }
  }
})

# 两种定义方式

pinia的store有选项式和setup函数两种定义方式,选项式语义更像vuex好理解,setup函数式更像是组合式函数。使用哪种方式都行,前期可以使用选项式,倾向vuex写法,更容易理解。

选项式第二个参数为一个对象:

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

函数式第二个参数为一个函数,里面定义数据与方法,并在最后暴露出来:

import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

# 使用Store

哪个组件使用就直接在哪个组件引入该store,然后通过返回值直接访问在 store 的 state、getters 和 actions 中定义的任何属性,不需像vuex那样麻烦。

store的返回值是一个reactive对象,不能直接进行解构state、getters,需要使用storeToRefs()为其创建响应性,而action却可以随便解构。

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

// 读写state
store.theme
store.theme = '#fff'

// 获取getters
store.userLength

// 调用actions
const switchDarkTheme = () => {
  store.switchTheme('#000')
}

// 解构store
// 解构state、getters
const { theme, userLength } = storeToRefs(store)
theme.value
userLength.value
// 解构action
const { switchTheme } = store
switchTheme('#000')
</script>
<template>
  <h1>{{store.theme}}</h1>
  <a-button type="primary" @click="switchDarkTheme">深色主题</a-button>
</template>

# state

# 类型标注

创建store时我们最好创建一个types文件为其进行TS类型标注。

// app/index.ts
import { defineStore } from 'pinia'
import type { AppState } from './types'

export const useAppStore = defineStore('app', {

  state: (): AppState => ({
    theme: '',
    userList: []
  }),

  getters: {
    userLength(state): number {
      return state.userList.length
    }
  },

  actions: {
    switchTheme(color: string) {
      this.theme = color
    }
  }
})

types类型标注文件:

// app/types.ts
// app store 接口
interface UserInfo {
  id: number,
  name: string
}

export interface AppState {
  theme: string,
  userList: UserInfo[]
}

# 访问和修改

pinia去除mutations,获取和修改state的值就直接用state就行了。

<script setup>
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

// 获取整个state
store.$state

// 获取某个值
store.theme

// 修改某个值
store.theme = '#fff'
</script>
<template>
  <h1>{{store.theme}}</h1>
</template>

# 重置、批量修改state

将 state 所有的值重置为初始值可以使用$reset() 方法

<script setup>
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

store.$reset()
</script>

同时修改 state 中的多个值或者添加值使用 $patch 方法

<script setup>
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

// 批量修改
store.$patch({
  // 修改state值
  theme: '#fff',
  // 新增state
  age: 120,
  name: 'DIO',
})

// 通过函数修改复杂类型数据
store.$patch((state) => {
  state.userList.push({ name: 'shoes', id: 1 })
  state.theme = 'blue'
})
</script>

# 订阅state

订阅 state 通过 $subscribe() 方法监听state的变化。使用watch也能进行监听state,但$subscribe() 方法只触发一次

<script setup>
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

// 任何修改和变更都会被监听到
store.$subscribe((mutation, state) => {
  // mutation变更事件
  console.log(mutation.type) // 变更类型
  console.log(mutation.storeId ) // store名称
  console.log(mutation.payload) // 传递给 cartStore.$patch() 的补丁对象

  // 变更后的state
  console.log(state)
})
</script>

监听整个pinia实例变化

<script setup>
import { watch } from 'vue'
import pinia from '@/stores'

watch(
  pinia.state,
  (state) => {
    console.log(state)
  },
  { deep: true }
)
</script>

# getters

getters 与 vuex 的 Getter 一样是state的计算属性。在getters中既可以其他getters也可以访问其他store的getters。

export const useCountStore = defineStore('count', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 普通写法
    floorCount(state) {
      return state.count--
    },
    // 箭头函数写法
    doubleCount: (state) => state.count * 2
  }
})

获取getters

<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount
</script>

# 向getters传参

通过返回一个函数来向getters传递参数

export const useAppStore = defineStore('app', {
  state: () => ({
    userList: [],
  }),
  getters: {
    getUserById: (state) => {
      return (userId) => state.userList.find((item) => item.id === userId)
    }
  }
})

在组件中使用:

<script setup>
import { storeToRefs } from 'pinia'
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

// 解构store
const { getUserById } = storeToRefs(store)

// 需要getUserById.value(2)
const { getUserById } = storeToRefs(store)
const getUser = getUserById.value(2)
console.log(getUser)
</script>

# 访问其他getter

在getters中可以使用 this 访问其他getters

export const useCountStore = defineStore('count', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 类型是自动推断出来的,因为我们没有使用 `this`
    doubleCount: (state) => state.count * 2,

    // 需要手动推断返回类型
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    }
  }
})

# 访问其他store的getters

访问其他store的getters直接在getter中引入就行

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

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

# actions

actions类似组件中的method方法,可以是异步的。action中可以通过 this 访问整个store实例。

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

调用actions

<script setup>
const store = useCounterStore()
store.increment()
store.setCount(100)
store.setDoubleCount(101)

// 或解构调用
const { increment, setCount, setDoubleCount } = store
increment()
setCount(100)
setDoubleCount(101)
</script>

# 访问其他action

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: ''
  }),
  actions: {
    setName() {
      this.name = '标题'
    }increment() {
      this.setName()
      this.count++
    }
  }
})

# 访问其他store的actions

访问其他store的actions直接在action中引入就行

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

export const useCountStore = defineStore('count', {
  state: () => ({
    // ...
  }),
  actions: {
    setName() {
      this.name = '标题'
    }increment() {
      const otherStore = useOtherStore()
      if (otherStore.getAuthor()) {
        this.count++
      }
    }
  }
})

# 订阅action

订阅 action 通过 $onAction() 方法监听action的变化和结果。

<script setup>
import { useAppStore } from '@/stores/modules/app'
const store = useAppStore()

const unsubscribe = store.$onAction(
  ({
    name, // action 名称
    store, // store 实例,类似 `someStore`
    args, // 传递给 action 的参数数组
    after, // 在 action 返回或解决后的钩子
    onError, // action 抛出或拒绝的钩子
  }) => {
    // 为这个特定的 action 调用提供一个共享变量
    const startTime = Date.now()
    // 这将在执行 "store "的 action 之前触发。
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 这将在 action 成功并完全运行后触发。
    // 它等待着任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 抛出或返回一个拒绝的 promise,这将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动删除监听器
unsubscribe()
</script>

# 在组件外使用 store

在组件外使用 store 必须先确保pinia实例挂载到vue实例上之后才行,比如使用router导航守卫监听路由,就先要确定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'
})