【Vue3】Pinia

279 阅读3分钟

1. 安装与注册

1.1 安装 npm i pinia

1.2 注册

/src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const app = createApp(App)

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

app.mount('#app')

2. 创建基本 store 和 基本使用

2.1 新建文件

/src/store/store-name.ts

// 暴露一个枚举类型,用于给不同的 store 命名,防止重复定义
export const enum Names {
    Test = 'Test'
}

/src/store/index.ts

import { Names } from "./store-name";
import { defineStore } from "pinia";

// 调用 defineStore 定义一个 store
// 第一个参数是唯一命名空间,第二个参数是一个对象,包含了 state,getters,actions
// 注意名称的定义 useTestStore 使用 use 开头,表示是一个 hook
export const useTestStore = defineStore(Names.Test, {
    // state 一个函数,返回一个对象,对象里面存储着各种 state
    state: () => {
        return {
            name: 'lzy',
            age: 18
        }
    },
    // 类似 computed,可以修饰一些值
    getters: {

    },
    // 类似 methods,既可以同步,也可以异步提交 state
    actions: {

    }
})

2.2 基本使用 store

// App.vue
<template>
  <!-- 在模板中使用 store 中的 state -->
  <h3> Pinia-Test-State:{{ Test.name }} - {{ Test.age }} </h3>
</template>

<script setup lang="ts">
// 引入 store
import { useTestStore } from './store'

// 调用这个 store
const Test = useTestStore()

// console.log(Test) 打印看一下返回的是什么,是一个响应式的对象
</script>

打印结果

image.png

3. 修改 state 的五种方式

// App.vue
<template>
  <h3> Pinia-Test-State:{{ Test.name }} - {{ Test.age }} </h3>
  <button @click="change">修改</button>
</template>

<script setup lang="ts">
import { useTestStore } from './store'

const Test = useTestStore()

const change = () => {
  // 1. 直接修改
  // Test.name = "LZY"

  // 2. 借助 $patch,传递一个对象,对象内属性可以不全写,但不能新增
  // Test.$patch({ name: 'LZY'})  // 可以
  // Test.$patch({ name: 'LZY', age: 20})  // 也可以
  // Test.$patch({ name: 'LZY', gender: '男'})  // 不可以

  // 3. 借助 $patch,传递一个函数(参数为 store 中返回的 state),相比第二种的优势在于,可以在函数内添加一些处理逻辑
  // Test.$patch((state) => {
  //   // 可以添加一些处理逻辑
  //   state.name = 'LZY'
  //   state.age = 20
  // })

  // 4. 借助 $state,缺点在于 一定要把属性写全
  // Test.$state = {
  //   name: 'LZY',
  //   age: 20
  // }

  // 5. 借助 actions,要在 store 的 actions 定义修改函数,最好一个 state 对应一个 setter
  // Test.setName("LZY")
  // Test.setAge(20)
}

</script>

对于第 5 种方式,要在 actions 中新增方法

// /src/store/index
import { Names } from "./store-name";
import { defineStore } from "pinia";

export const useTestStore = defineStore(Names.Test, {
    state: () => {
        return {
            name: 'lzy',
            age: 18
        }
    },
    // 类似 computed,可以修饰一些值
    getters: {

    },
    // 类似 methods,既可以同步,也可以异步提交 state
    actions: {
        // 新增的方法 ↓
        setName(name: string) {
            this.name = name
        },
        setAge(age: number){
            this.age = age
        }
    }
})

4. 解构 store

4.1 普通解构出来的属性不具有响应式

<template>
  <h3> 原始值:{{ Test.name }} - {{ Test.age }} </h3>
  <h3> 解构值:{{ name }} - {{ age }} </h3>
  <button @click="change">修改</button>
</template>

<script setup lang="ts">
import { useTestStore } from './store'

const Test = useTestStore()
let { name, age } = Test

const change = () => {
  Test.age++  // store 本身的 state 是具有响应式的
  console.log(age)  // 解构出来的变量永远不会跟着变
}

</script>

4.2 借助 storeToRefs 解构出来的属性具有响应式

<template>
  <h3> 原始值:{{ Test.name }} - {{ Test.age }} </h3>
  <h3> 解构值:{{ name }} - {{ age }} </h3>
  <button @click="change">修改</button>
</template>

<script setup lang="ts">
import { useTestStore } from './store'
import { storeToRefs } from 'pinia'

const Test = useTestStore()
// 解构出响应式的属性
let { name, age } = storeToRefs(Test)

const change = () => {
  // 无论修改哪个属性,都是响应式的
  Test.age++
  age.value++
  
  console.log(Test.age, age.value)  
}

</script>

5. actions 和 getters

5.1 actions 同步函数

/src/store/index

import { Names } from "./store-name";
import { defineStore } from "pinia";

type User = {
    name: string,
    age: number
}

const result: User = {
    name: 'lzy',
    age: 18
}

export const useTestStore = defineStore(Names.Test, {
    state: () => {
        return {
            user: {},
            info: ""
        }
    },
    // 类似 computed,可以修饰一些值
    getters: {

    },
    // 类似 methods,既可以同步,也可以异步提交 state
    actions: {
        // 同步提交
        setUser () {
            this.user = result
        }
    }
})

/src/App.vue

<template>
  <h3> Actions: {{ Test.user }} </h3>
  <button @click="change">提交</button>
</template>

<script setup lang="ts">
import { useTestStore } from './store'

const Test = useTestStore()

const change = () => {
    // 调用 actions 的同步方法 setUser(),页面同步更新
    Test.setUser()
}

</script>

5.2 actions 异步函数

/src/store/index

import { Names } from "./store-name";
import { defineStore } from "pinia";

type User = {
    name: string,
    age: number
}

const login = ():Promise<User> => {
    return new Promise((resolve) => {
        setTimeout(() => resolve({
            name: 'lzy',
            age: 18
        }), 2000)
    })
}

export const useTestStore = defineStore(Names.Test, {
    state: () => {
        return {
            user: {},
            info: ""
        }
    },
    // 类似 computed,可以修饰一些值
    getters: {

    },
    // 类似 methods,既可以同步,也可以异步提交 state
    actions: {
        // 异步提交
        async setUser () {
            const result = await login()
            this.user = result  
        }
    }
})

/src/App.vue 中代码不变,页面在异步操作有结果后更新

5.3 actions 可相互调用

/src/store/index

actions: {
    // 异步提交
    async setUser () {
        const result = await login()
        this.user = result 
        this.setInfo()
    },
    setInfo() {
        this.info = "这是我的信息"
    }
}

/src/App.vue

<template>
  <!-- 可以看到 user 和 info 同步更新,都是异步操作-->
  <h3> Actions: {{ Test.user }} </h3>
  <h3> Actions 相互调用: {{ Test.info }}</h3>
  <hr />
  <button @click="change">提交</button>
</template>

<script setup lang="ts">
import { useTestStore } from './store'

const Test = useTestStore()

const change = () => {
    Test.setUser()
}
</script>

5.4 getters 计算属性

/src/store/index

getters: {
    newInfo():string {
        return `更新我的信息-${this.info}`
    }

},

/src/App.vue 直接像 state 一样获取即可

<h3> getters: {{ Test.newInfo }}</h3>

5.5 getters 可相互调用

getters: {
    newInfo():string {
        return this.setNewInfo  // 调用下面的这个属性,并返回
    },
    setNewInfo(): string {
        return `更新我的信息-${this.info}`
    }

},

6. 实例身上的 API

6.1 $reset

重置 state 成初始状态
<button @click="reset">重置</button>
const reset = () => {
  Test.$reset()
}

6.2 $subscribe

监视 state 的变化
Test.$subscribe((args, state) => {
  console.log(args)
  console.log(state)  // 这个就是 state 里面的所有数据
},{
  detached: true,  // 组件被销毁后,仍继续监听
  deep: true,  // 深度监听
  flush: "pre"  // 调用时机,更新之前/中/后
})

image.png

6.3 $onAction

监视 action 的调用
Test.$onAction((args) => {
  console.log(args)  // 打印如下
}, true)  // 第二个参数为 detached,true 表示组件卸载后,监视器依然存活

image.png

store:就是自定义的那个 store 实例
name:action 的名称
args:传递给 action 的参数
after:接收一个回调,最后时刻调用    
onError:接收一个回调,向上传递错误
store.$onAction(({ after, onError }) => {
 // 你可以在这里创建所有钩子之间的共享变量,
 // 同时设置侦听器并清理它们。
 after((resolvedValue) => {
   // 可以用来清理副作用 
   // `resolvedValue` 是 action 返回的值,
   // 如果是一个 Promise,它将是已经 resolved 的值
 })
 onError((error) => {
   // 可以用于向上传递错误
 })
})

7. pinia 持久化

/src/store/store-storage.ts

import { PiniaPluginContext } from 'pinia'
import { toRaw } from 'vue'

type Options = {
    key?: string
}

const setStorage = (key: string, value: any) => {
    localStorage.setItem(key, JSON.stringify(value))
}

const getStorage = (key: string) => {
    return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {}
}

// 自定义 piniaPlugin 持久化插件,用户可以传入一个配置项
export const piniaPlugin = (options: Options) => {
    // 由于 pinia.use() 需要接收一个函数,并且这个函数只有一个 PiniaPluginContext 类型的参数
    // 但是用户又想传入自己的配置项 options
    // 这个情况下,就可以用 函数柯里化,用户可以先传入配置项,然后返回一个函数让 pinia.use() 注册
    return (context: PiniaPluginContext) => {
        const { store } = context
        // 根据 options 生成唯一的 key
        const key = `${options.key ? options.key : 'defaultKey'}-${store.$id}`
        const data = getStorage(key)
        // 监听 state 的变化
        store.$subscribe(() => {
            // 存储到浏览器内存中,记得要用 toRaw 去除响应式
            setStorage(key, toRaw(store.$state))
        })
        return {
            ...data
        }
    }
}

/src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { piniaPlugin } from './store/store-storage'


const app = createApp(App)

const pinia = createPinia()
// 自定义 piniaPlugin 插件,然后在 pinia 身上注册
pinia.use(piniaPlugin({
    key: 'store'
}))

app.use(pinia)
app.mount('#app')