Vue3$Data-Pinia

111 阅读2分钟

Vue3$Data-Pinia

0. What is Pinia

Pinia in Big Picture

组件的数据除了可以放在自身(ref 等),也可以从外部获取(props, provide-inject, v-model, fallthrough attributes等)。上述的外部获取,通常是指父子组件或者祖孙组件。兄弟组件或者不相关的组件之间处理起来就比较麻烦。

为了方便多个组件共享数据,人们就把数据及操作数据的函数放到了一个全局单例中。Vue 之前是通过 Vuex 实现的,后来使用了 Pinia。Pinia 删除了冗余的 mutations,更加扁平化,支持组合式写法。

VueStateManagement

Pinia Core Concepts

  1. store:存储数据和函数的对象
  2. state:数据
  3. getters:衍生的数据
  4. actions:操作数据的函数

Pinia 可以使用选项式 API,也可以使用组合式 API。这里使用组合式 API。

Basic Example

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

  return { count, name, doubleCount, increment }
})
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
</script>

<template>
  <!-- Access the state directly from the store -->
  <div>Current Count: {{ counter.count }}</div>
</template>

P. Install

npm install pinia之后,使用插件:

// @file main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

1. Defining a Store

import { defineStore } from 'pinia'

// the first argument is a **unique** id of the store across your application
export const useAlertsStore = defineStore('alerts', {
  // other options...
})

1.1 Setup Stores

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

  return { count, name, doubleCount, increment }
})
`ref()`s become `state` properties
`computed()`s become `getter`s
`function()`s become `action`s

Must return all state properties in setup stores: cannot have private state properties in stores.
- can use watchers
- globally provided properties (Router, Route)
- property provided at the App level
import { inject } from 'vue'
import { useRoute } from 'vue-router'

export const useSearchFilters = defineStore('search-filters', () => {
  const route = useRoute()
  // this assumes `app.provide('appProvided', 'value')` was called
  const appProvided = inject('appProvided')
  // ...
  return {
    // ...
  }
})

1.2 Using the store

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

// access the `store` variable anywhere in the component ✨
const store = useCounterStore()
</script>
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// ❌ This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = store
name // will always be "Eduardo"
doubleCount // will always be 0

setTimeout(() => {
  store.increment()
}, 1000)

// ✅ this one will be reactive
// 💡 but you could also just use `store.doubleCount` directly
const doubleValue = computed(() => store.doubleCount)
</script>

1.3 Destructuring from a Store

store 是一个 reactive 对象,不能直接解构。

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

const store = useCounterStore()
// `name` and `doubleCount` are reactive refs
// This will also extract refs for properties added by plugins
// but skip any action or non reactive (non ref/reactive) property
const { name, doubleCount } = storeToRefs(store)
// the increment action can just be destructured
const { increment } = store
</script>

2. State

2.1 Accessing the state

const store = useStore()

store.count++
// You cannot add a new state property.

2.2 Resetting the state

// in Option Stores
const store = useStore()

store.$reset()
// in Setup Stores: create your own `$reset()`
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

2.3 Mutating the state

// 1. direct
store.count++
// 2. patch object (partial state object)
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})
// 3. patch function
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

2.4 Replacing the state

// You cannot exactly replace the state of a store as that would break reactivity.
// You can patch it:
// this doesn't actually replace `$state`
store.$state = { count: 24 }
// it internally calls `$patch()`:
store.$patch({ count: 24 })
// set the initial state of your whole application: 只在 Options API 中好用
pinia.state.value = {}

2.5 Subscribing to the state

// The advantage of using $subscribe() over a regular watch() is that subscriptions will trigger only once after patches (e.g. when using the function version from above). WHAT??? watch will trigger multiple times?
cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // same as cartStore.$id
  mutation.storeId // 'cart'
  // only available with mutation.type === 'patch object'
  mutation.payload // patch object passed to cartStore.$patch()

  // persist the whole state to the local storage whenever it changes
  localStorage.setItem('cart', JSON.stringify(state))
})
<script setup>
const someStore = useSomeStore()

// this subscription will be kept even after the component is unmounted
someStore.$subscribe(callback, { detached: true })
</script>
watch(
  pinia.state,
  (state) => {
    // persist the whole state to the local storage whenever it changes
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

3. Actions

3.1 Subscribing to actions

It is possible to observe actions and their outcome with `store.$onAction()`. The callback passed to it is executed before the action itself.
- `after` handles promises and allows you to execute a function after the action resolves.
- In a similar way, onError allows you to execute a function if the action throws or rejects.
These are useful for tracking errors at runtime, similar to this tip in the Vue docs.
const unsubscribe = someStore.$onAction(
  ({
    name, // name of the action
    store, // store instance, same as `someStore`
    args, // array of parameters passed to the action
    after, // hook after the action returns or resolves
    onError, // hook if the action throws or rejects
  }) => {
    // a shared variable for this specific action call
    const startTime = Date.now()
    // this will trigger before an action on `store` is executed
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // this will trigger if the action succeeds and after it has fully run.
    // it waits for any returned promised
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // this will trigger if the action throws or returns a promise that rejects
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// manually remove the listener
unsubscribe()
<script setup>
const someStore = useSomeStore()

// this subscription will be kept even after the component is unmounted
someStore.$onAction(callback, true)
</script>

4. Plugins

PiniaPlugins

持久化 pinia-plugin-persistedstate

每个 store 单独开启

// store/index.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
import { defineStore } from 'pinia'

export const useStore = defineStore(
  'main',
  () => {
    const someState = ref('hello pinia')
    return { someState }
  },
  {
    persist: true,
  },
)

所有 store 默认开启

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

const pinia = createPinia()

pinia.use(createPersistedState({
  auto: true,
}))

Links

VueStateManagement

Pinia

pinia-plugin-persistedstate