Pinia学习(个人笔记)

2022-08-26  本文已影响0人  kevision
image.png

Pinia官方文档:https://pinia.web3doc.top/introduction.html

为什么叫Pinia


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

安装


npm install pinia

什么是 Store ?


一个 Store (如 Pinia)是一个实体,它持有未绑定到您的组件树的状态和业务逻辑。换句话说,它托管全局状态。它有点像一个始终存在并且每个人都可以读取和写入的组件。它有三个概念stategettersactions 并且可以安全地假设这些概念等同于组件中的“数据”“计算”“方法”

提前预览


import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
    // pinia中没有mutations
    state: () => { // state相当于vue中的data
        return {
            count: 0
        }
    },
    getters: { // getters相当于vue中的computed
        doubleCount(state) {
            // return this.count * 2 也可以直接通过this访问state
            return state.count * 2
        }
    },
    actions: { // actions相当于vue中的methods
        increment() {
            this.count++
        }
    }
})

正式开始


定义一个 Store

在深入了解核心概念之前,我们需要知道 Store 是使用 defineStore() 定义的,并且它需要一个唯一名称,作为第一个参数传递:

import { defineStore } from 'pinia'

// useStore 可以是 useUser、useCart 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useStore = defineStore('main', {
  state: () => {
      return {
        count: 0
      }
  }
  // other options...
})

这个 name,也称为 id,是必要的,Pinia 使用它来将 store 连接到 devtools。 将返回的函数命名为 use... 是跨可组合项的约定,以使其符合你的使用习惯。

使用 store

我们正在 定义 一个 store,因为在 setup() 中调用 useStore() 之前不会创建 store:

一旦 store 被实例化,你就可以直接在 store 上访问 state、getters 和 actions 中定义的任何属性。

import { useStore } from '@/stores/counter'
<script setup>
const store = useStore()
console.log(store.count) // store中定义的state, getters和actions的属性和方法,都可以直接通过store实例访问
</script>

注意:store 是一个用reactive 包裹的对象,这意味着不需要在getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构

import { useStore } from '@/stores/counter'
<script setup>
const store = useStore()
// ❌ 这不起作用,因为它会破坏响应式
// 这和从 props 解构是一样的
const { name, doubleCount } = store
</script>

想要对其进行解构同时保持其响应式,解决办法如下:使用storeToRefs()。它将为任何响应式属性创建 refs。

import { useStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
<script setup>
const store = useStore()
// `name` 和 `doubleCount` 是响应式引用
// 这也会为插件添加的属性创建引用
// 但跳过任何 action 或 非响应式(不是 ref/reactive)的属性
const { name, doubleCount } = storeToRefs(store)
</script>

State


state相当于vue组件中的data

大多数时候,state 是 store 的核心部分。 我们通常从定义应用程序的状态开始。 在 Pinia 中,状态被定义为返回初始状态的函数。 Pinia 在服务器端和客户端都可以工作。

import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // 推荐使用 完整类型推断的箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断其类型
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
})

访问state

默认情况下,您可以通过 store 实例访问状态来直接读取和写入状态:

<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
store.counter++
</script>

重置状态

您可以通过调用 store 上的 $reset() 方法将状态 重置 到其初始值:

<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
store.$reset()
</script>

$patch改变状态

除了直接用 store.counter++ 修改 store,你还可以调用 $patch 方法。 它允许您使用部分“state”对象同时应用多个更改:

store.$patch({
  counter: store.counter + 1,
  name: 'Abalam',
})

但是,使用这种语法应用某些突变非常困难或代价高昂:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。 正因为如此,$patch 方法也接受一个函数来批量修改集合内部分对象的情况:

cartStore.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

替换state

您可以通过将其 $state 属性设置为新对象来替换 Store 的整个状态:

store.$state = { counter: 666, name: 'Paimon' }

Getters


getters相当于vue组件中的computed

Getter 完全等同于 Store 状态的 计算值。 它们可以用 defineStore() 中的 getters 属性定义。 他们接收“状态”作为第一个参数以鼓励箭头函数的使用:

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

大多数时候,getter 只会依赖状态,但是,他们可能需要使用其他 getter。 正因为如此,我们可以在定义常规函数时通过 this 访问到 整个 store 的实例, 但是需要定义返回类型(在 TypeScript 中)。

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 自动将返回类型推断为数字
    doubleCount(state) {
      return state.counter * 2
    },
    // 返回类型必须明确设置
    doublePlusOne(): number {
      return this.counter * 2 + 1
    },
  },
})

然后你可以直接在 store 实例上访问 getter

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>
<script setup>
import { useStore } from '@/stores/counter'
const store = useStore()
</script>

访问其他 getter

与计算属性一样,您可以组合多个 getter。 通过 this 访问任何其他 getter

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 类型是自动推断的,因为我们没有使用 `this`
    doubleCount: (state) => state.counter * 2,
    doubleCountPlusOne() {
      // 自动完成 ✨
      return this.doubleCount + 1
    },
  },
})

将参数传递给 getter

Getters 只是幕后的 computed 属性,因此无法向它们传递任何参数。 但是,您可以从 getter 返回一个函数以接受任何参数

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      // 接收userId参数
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

并在组件中使用:

<template>
  <p>User 2: {{ store.getUserById(2) }}</p>
</template>
<script setup>
    const store = useStore()
</script>

访问其他 Store 的getter

要使用其他存储 getter,您可以直接在 getter 内部使用它:

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

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

Actions


actions 相当于vue组件中的 methods

Actions 相当于组件中的 methods。 它们可以使用 defineStore() 中的 actions 属性定义,并且它们非常适合定义业务逻辑

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      // 可以通过this访问整个store实例
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})

Actions 像 methods 一样被调用:

<script setup>
    import { useStore } from '@/stores/counter'
    const main = useStore()
    // Actions 像 methods 一样被调用:
    main.randomizeCounter()
</script>

getters 一样,操作可以通过 this 访问 whole store instance 并提供完整类型(和自动完成✨)支持。 与它们不同,actions 可以是异步的,您可以在其中await 任何 API 调用甚至其他操作! 这是使用 Mande 的示例。 请注意,只要您获得“Promise”,您使用的库并不重要,您甚至可以使用浏览器的“fetch”函数:

import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
        // do something...
      } catch (error) {
        return error
      }
    },
  },
})

访问其他 store 操作

要使用另一个 store ,您可以直接在action内部使用它:

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

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    // ...
  }),
  actions: {
    async fetchUserPreferences(preferences) {
      // 在一个action内部访问其他store
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

订阅 Actions

可以使用 store.$onAction() 订阅 action 及其结果。 传递给它的回调在 action 之前执行。 after 处理 Promise 并允许您在 action 完成后执行函数(类似then)。 以类似的方式,onError 允许您在处理中抛出错误(类似catch)。

这是一个在运行 action 之前和它们 resolve/reject 之后记录的示例。

const unsubscribe = someStore.$onAction(
  ({
    name, // action 的名字
    store, // store 实例
    args, // 调用这个 action 的参数
    after, // 在这个 action 执行完毕之后,执行这个函数
    onError, // 在这个 action 抛出异常的时候,执行这个函数
  }) => {
    // 记录开始的时间变量
    const startTime = Date.now()
    // 这将在 `store` 上的操作执行之前触发
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

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

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

// 手动移除订阅
unsubscribe()

默认情况下,action subscriptions 绑定到添加它们的组件(如果 store 位于组件的 setup() 内)。 意思是,当组件被卸载时,它们将被自动删除。 如果要在卸载组件后保留它们,请将 true 作为第二个参数传递给当前组件的 detach action subscription:

<script setup>
    const someStore = useSomeStore()
    // 此订阅将在组件卸载后保留
    someStore.$onAction(callback, true)
</script>

Plugins插件


由于是底层 API,Pania Store可以完全扩展。 以下是您可以执行的操作列表:

  • 向 Store 添加新属性
  • 定义 Store 时添加新选项
  • 为 Store 添加新方法
  • 包装现有方法
  • 更改甚至取消操作
  • 实现本地存储等副作用
  • 仅适用于特定 Store

使用 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'
image.png

secret属性就被添加到了所有的store上面。这对于添加全局对象(如路由器、模式或 toast 管理器)很有用。

插件介绍

Pinia 插件是一个函数,可以选择返回要添加到 store 的属性。 它需要一个可选参数,一个 context:

export function myPiniaPlugin(context) {
  context.pinia // 使用 `createPinia()` 创建的 pinia
  context.app // 使用 `createApp()` 创建的当前应用程序(仅限 Vue 3)
  context.store // 插件正在扩充的 store
  context.options // 定义存储的选项对象传递给`defineStore()`
  // ...
}

// 解构写法
export function myPiniaPlugin({ pinia, app, store, options }) {
  // ...
}

然后使用 pinia.use() 将此函数传递给 pinia:

pinia.use(myPiniaPlugin)

插件仅适用于在将pinia传递给应用程序后创建的 store,否则将不会被应用。

扩充 store

您可以通过简单地在插件中返回它们的对象来为每个 store 添加属性:

pinia.use(() => ({ hello: 'world' }))

您也可以直接在 store 上设置属性,但如果可能,请使用return版本,以便 devtools 可以自动跟踪它们

pinia.use(({ store }) => {
  store.hello = 'world'
})

插件的任何属性 returned 都会被devtools自动跟踪,所以为了让hello在devtools中可见,如果你想调试它,请确保将它添加到store._customProperties仅在开发模式 开发工具:

// 从上面的例子
pinia.use(({ store }) => {
  store.hello = 'world'
  // 确保您的打包器可以处理这个问题。 webpack 和 vite 应该默认这样做
  if (process.env.NODE_ENV === 'development') {
    // 添加您在 store 中设置的任何 keys
    store._customProperties.add('hello')
  }
})

请注意,每个 store 都使用 reactive包装,自动展开任何 Ref (ref(), computed() , ...), 它包含了:

const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // 每个 store 都有自己的 `hello` 属性
  store.hello = ref('secret')
  // 它会自动展开
  store.hello // 'secret'

  // 所有 store 都共享 value `shared` 属性
  store.shared = sharedRef
  store.shared // 'shared'
})

这就是为什么您可以在没有 .value 的情况下访问所有计算属性以及它们是响应式的原因

添加新状态state

如果您想将新的状态属性添加到 store 或打算在 hydration 中使用的属性,您必须在两个地方添加它

请注意,这允许您共享 refcomputed 属性:

const globalSecret = ref('secret')
pinia.use(({ store }) => {
  // `secret` 在所有 store 之间共享
  store.$state.secret = globalSecret
  store.secret = globalSecret
  // 它会自动展开
  store.secret // 'secret'

  const hasError = ref(false)
  store.$state.hasError = hasError
  // 这个必须始终设置
  store.hasError = toRef(store.$state, 'hasError')

  // 在这种情况下,最好不要返回 `hasError`,因为它
  // 将显示在 devtools 的 `state` 部分
  // 无论如何,如果我们返回它,devtools 将显示它两次。
})

请注意,插件中发生的状态更改或添加(包括调用store.$patch())发生在存储处于活动状态之前,因此不会触发任何订阅

添加新的外部属性

当添加外部属性、来自其他库的类实例或仅仅是非响应式的东西时,您应该在将对象传递给 pinia 之前使用 markRaw() 包装对象。 这是一个将路由添加到每个 store 的示例:

import { markRaw } from 'vue'
// 根据您的路由所在的位置进行调整
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

在插件中调用 $subscribe

您也可以在插件中使用 store.$subscribestore.$onAction

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // 在存储变化的时候执行
  })
  store.$onAction(() => {
    // 在 action 的时候执行
  })
})

添加新选项

可以在定义 store 时创建新选项,以便以后从插件中使用它们。 例如,您可以创建一个 debounce 选项,允许您对任何操作进行去抖动:

defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // 稍后将由插件读取
  debounce: {
    // 将动作 searchContacts 防抖 300ms
    searchContacts: 300,
  },
})

然后插件可以读取该选项以包装操作并替换原始操作:

// 使用任何防抖库
import debounce from 'lodash/debunce'

pinia.use(({ options, store }) => {
  // 可以通过options访问新选项
  if (options.debounce) {
    // 我们正在用新的action覆盖这些action
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

请注意,使用设置语法时,自定义选项作为第三个参数传递:

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // 稍后将由插件读取
    debounce: {
      // 将动作 searchContacts 防抖 300ms
      searchContacts: 300,
    },
  }
)

遇到的问题汇总


  1. 在封装的axios请求文件中访问store,提示Uncaught ReferenceError: Cannot access 'store' before initialization
import { useStore } from '@/store'
const store = useStore()
class Http {
    constructor(apiList) {
        this.instance = axios.create({
            baseURL: config.baseUrl,
            headers: {
                'Authorization': store.token // --> 此处报错
            }
        })
        // ...
    }
}

错误原因:store在pinia安装之前使用了。
解决办法:

The easiest way to ensure this is always applied is to defer calls of useStore() by placing them inside functions that will always run after pinia is installed.


确保始终应用此功能的最简单方法是通过将useStore()的调用放在pinia安装后始终运行的函数中来延迟调用。

于是把store放在请求拦截函数中访问:

import { useStore } from '@/store'

class Http {
    constructor(apiList) {
        this.instance = axios.create({
            baseURL: config.baseUrl
        })
        this.instance.interceptors.request.use(function (config) {
            // 在发送请求之前做些什么, 比如修改headers-->config.headers.Authorization = 'xxx'
            const store = useStore()
            config.headers = {
                'Authorization': store.token
            }
            return config; // 必须要return config
        }, function (error) {
            // 对请求错误做些什么
            return Promise.reject(error);
        });
    }
}

等待继续补充......

上一篇 下一篇

猜你喜欢

热点阅读