Skip to content

Pinia

安装

bash
npm install pinia

引入

创建一个 pinia 实例 (根 store) 并将其传递给应用:

js
import { createApp } from 'vue'
import { createPinia } from 'pinia' //[!code hl]
import App from './App.vue'

const pinia = createPinia() //[!code hl]
const app = createApp(App)

app.use(pinia) //[!code hl]
app.mount('#app')

定义 Store

defineStore()

defineStore() 的返回值可以任意命名,但最好使用 store 的名字,同时以 use 开头且以 Store 结尾,如 useUserStoreuseCartStoreuseProductStore

js
import { defineStore } from 'pinia'

// 第一个参数是你的应用中 Store 的唯一 ID
export const useAlertsStore = defineStore('alerts', {
  // 第二个参数可接受两类值:Setup 函数或 Option 对象
})

API 风格

Option Store
js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },

  getters: {
    double: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    }
  }
})
Setup Store
js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  //  state
  const count = ref(0)

  //  getters
  const double = computed(() => {
    count * 2
  })

  //  actions
  function increment() {
    count.value++
  }

  return { count, double, increment }
})

TIP

state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

三个概念

State

在 Pinia 中,state 被定义为一个返回初始状态的函数。

  • 访问 state
js
const store = useStore()

store.count++
  • 重置 state
js
const store = useStore()

store.$reset()

组合式 API 下重置 state

解决直接调用 store.$reset 时报错: Store "xxx" is built using the setup syntax and does not implement $reset()

js
const pinia createPinia();

pinia.use(({store}) => {
  const initialState = JSON.parse(JSON.stringify(store.$state));

  store.$reset = () => {
    store.$state = JSON.parse(JSON.stringify(initialState));
  }
})

app.use(pinia)
  • 变更 state
js
//  对象
store.$patch({
  count: store.count + 1
})

// 函数
store.$patch((state) => {
  state.count += 1
})
  • 替换 state
js
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })

替换 store 中的整个 state 对象

js
pinia.state.value = {}
  • 订阅 state
js
store.$subscribe((mutation, state) => {
  console.log(mutation, state)
})

TIP

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

Getter

推荐使用箭头函数,并且它将接收 state 作为第一个参数:

js
export const useStore = defineStore('main', () => {
  const count = ref(0)

  const doubleCount = computed(() => count * 2)

  return { count, doubleCount }
})
  • 访问其他 getter
js
export const useStore = defineStore('main', () => {
  const count = ref(0)

  const doubleCount = computed(() => count.value * 2)

  const doubleCountPlusOne = computed(() => doubleCount.value + 1)

  return { count, doubleCount, doubleCountPlusOne }
})
  • 访问其他 store 的 getter
js
import { useOtherStore } from './other-store'

export const useStore = defineStore('main', () => {
  const localData = ref(0)

  const otherGetter = computed(() => {
    const otherStore = useOtherStore()
    return localData.value + otherStore.data
  })

  return { otherGetter }
})
Action
js
export const useCounterStore = defineStore('main', () => {
  const count = ref(0)

  const increment = () => count.value++

  return { count, increment }
})
  • 访问其他的 action
js
import { defineStore } from 'pinia'

export const useStore = defineStore('main', () => {
  const count = ref(0)

  const increment = () => doubleIncrement()
  const doubleIncrement = () => (count.value += 2)

  return { count, increment, doubleIncrement }
})
  • 访问其他 store 的 action
js
import { useOtherStore } from './other-store'

export const useStore = defineStore('main', () => ({
  const count = ref(0);

  const increment = () => count.value++;
  const otherAction = () => {
    const otherStore = useOtherStore();
    return increment() + otherStore.data
  }

  return { count, increment, otherAction}
}));
  • async & await
js
import { defineStore } from 'pinia'

export const useStore = defineStore('main', () => {
  const count = ref(0)

  const fetchData = async (api) => {
    const response = await fetch(api)
    const data = await response.json()
    console.log(data)
  }

  return { count, fetchData }
})
  • 订阅 action
js
store.$onAction((context, payload) => {
  // 在 action 执行前执行的逻辑
  console.log('Action 执行前')

  // 可以对 payload 进行操作或根据需求执行其他逻辑

  // 在 action 执行后执行的逻辑
  context.$nextTick(() => {
    console.log('Action 执行后')
  })
}, true)

使用 Store

store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value

vue
<script setup>
  import { useCounterStore } from '@/stores/counter'

  // 可以在组件中的任意位置访问 `store` 变量
  const store = useCounterStore()

  console.log(store.count, store.double, store.increment)
</script>

响应式解构

为了从 store 中提取 state 时保持其响应性,你需要使用 storeToRefs()。它将为每一个响应式数据创建引用。当你只使用 state 而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:

vue
<script setup>
  import { storeToRefs } from 'pinia'
  import { useCounterStore } from '@/stores/counter'

  const store = useCounterStore()
  /**
   * `count` 和 `doubleCount` 是响应式的 ref
   * 同时通过插件添加的属性也会被提取为 ref
   * 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
   */
  const { count, doubleCount } = storeToRefs(store)
  // 作为 action 的 increment 可以直接解构
  const { increment } = store
</script>

组合式 Store

如果两个或更多的 store 相互使用,它们不可以通过 getters 或 actions 创建一个无限循环。它们也不可以同时在它们的 setup 函数中直接互相读取对方的 state。

Pinia 插件

Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性且具体响应式。它接收一个可选参数,即 context。通常用于添加全局对象很有用,如路由器、modal 或 toast 管理器。

js
function myPiniaPlugin(context) {
  context.pinia // 用 `createPinia()` 创建的 pinia。
  context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
  context.store // 该插件想扩展的 store
  context.options // 定义传给 `defineStore()` 的 store 的可选对象。
}

扩展 Store

js
//方法一
const myPiniaPlugin = () => {
  return {
    secret: 'the cake is a lie',
    handler: () => 'handler'
  }
}

//方法二: 解构赋值 context.store
const myPiniaPlugin = ({ store }) => {
  ;(store.secret = 'the cake is a lie'), (store.handler = () => 'handler')
}

安装插件

js
import { createPinia } from 'pinia'

const pinia = createPinia()

pinia.use(myPiniaPlugin)

使用插件

js
import useStore from './store'

const store = useStore()

console.log(store.secret, store.handler)

第三方插件

pinia-plugin-persistedstate (持久化存储)
  • 安装
bash
npm i pinia-plugin-persistedstate
  • 引入
js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
  • 定义
js
import { defineStore } from 'pinia'

export const useStore = defineStore('store', {
  const someState = ref('hello pinia')

  return { someState }
},{
  persist: true,
})

配置 SessionStorage

js
import { defineStore } from 'pinia'

export const useStore = defineStore('store', {
  const someState = ref('hello pinia')

  return { someState }
},{
  persist: {
    storage: sessionStorage,
    paths: ['someState'],
  }
})

Pinia 与 Vue Router 导航守卫

解决错误:"getActivePinia()" was called but there was no active Pinia. Did you forget to install pinia?

js
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'
})

读书、摄影、画画、弹琴、编程