抛弃Vuex,拥抱Pinia。

667 阅读5分钟

拥抱Pinia

Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。

0.Pinia对比Vuex

Pinia API 与 Vuex ≤4 有很大不同,即:

  • 没有mutations。mutations被认为是非常冗长的。最初带来了vue-devtool集成,但这不再是问题。
  • 不再有模块的嵌套结构。您仍然可以通过在另一个store中导入和使用store来隐式嵌套store,但 Pinia 通过设计提供扁平结构,同时仍然支持store之间的交叉组合方式。您甚至可以拥有store的循环依赖关系。
  • 更好的TypeScript支持。无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。
  • 不再需要注入、导入函数、调用它们,享受自动补全!
  • 无需动态添加stores,默认情况下它们都是动态的,您甚至不会注意到。请注意,您仍然可以随时手动使用store来注册它,但因为它是自动的,所以您无需担心。
  • 没有命名空间模块。鉴于store的扁平架构,“命名空间”store是其定义方式所固有的,您可以说所有stores都是命名空间的。

1. Pinia快速入门

1.1 安装

yarn add pinia
# or with npm
npm install pinia

如果您的应用程序使用Vue2,您还需要安装组合式API包: @vue/composition-api

如果您使用的是Vue CLI,您可以试试这个非官方插件

1.2 初始化配置

Vue3 + Vite2:

1.在src/main.ts文件中

import { createApp } from 'vue'
// 1.安装后 导入
import { createPinia } from 'pinia'

import App from './App.vue'

// 2. 创建pinia实例
const pinia = createPinia()
const app = createApp(App)

// 3. use 挂载
app.use(pinia)
app.mount('#app')

2.在src/store/index.ts文件中

import { defineStore } from "pinia";
// 1.定义容器
// 参数1: 容器的ID,必须唯一,将来pinia会把所有容器挂载到根容器
// 参数2: 选项对象
export const useMainStore = defineStore("main", {
  /**
   * 类似于组件的data, 用来存储全局状态
   * 1. 必须是函数,这样是为了在服务端渲染的时候避免交叉请求导致的数据状态污染
   * 2. 必须是箭头函数,这样是为了更好的 TS 类型推导
   * @returns  一个函数,调用得到容器实例
   */
  state: () => ({
      counter: 1,
      name: 'coderY',
      arr: [1,2,3]
  }),
  /**
   * 类似于组件的computed,用来封装计算属性,有缓存的功能
   */
  getters: {},
  /**
   * 类似于组件的methods,封装业务逻辑(同步,异步都可以),修改state
   */
  actions: {},
});

2. State

类似于组件的data,用来存储全局状态

2.1 访问State

方式1:数据量不多的情况下可以直接访问。

<template>
  <div>{{ storeMain.counter }}</div>
  <div>{{ storeMain.name }}</div>
</template>

<script lang="ts" setup>
    import { useMainStore } from '../store'
    const storeMain = useMainStore();
</script>

<style lang="scss" scoped>
</style>

2.2 使用storeToRefs解构数据

方式2:数据量多的情况下,需要单独解构的话可以使用pinia提供的storeToRefs

<template>
  <div>{{ counter }}</div>
  <div>{{ name }}</div>
  <div>{{ arr }}</div>
</template>

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

    const storeMain = useMainStore();
    // 如果不使用storeToRefs,解构出来的数据不是响应式的
    // 这里用toRefs也可以,Pinia 内部也是用的 toRefs,只不过做了一层包装。
    const { counter , name , arr } = storeToRefs(storeMain);
</script>

<style lang="scss" scoped>
</style>

2.3 修改State

<template>
  <div>{{ storeMain.counter }}</div>
  <div>{{ storeMain.name }}</div>
  <div>{{ storeMain.arr }}</div>
  <button @click="handleChangeState3"> 修改 </button>
</template>

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

    const storeMain = useMainStore();
    // 方式1:最简单的方式
    // 修改一次,视图就更新一次,如果数据量大,造成的开销会很大,不建议
    const handleChangeState1 = () => {
        storeMain.counter++;
        storeMain.name = 'test';
    }
    
    // 方式2: 如果要修改多个数据 $patch(对象)批量更新
    // 一次性修改多个数据 建议使用$patch 在性能上有优化 一次性修改好数据,更新一次视图
    const handleChangeState2 = () => {
        storeMain.$patch({
            // 这里不能写为 暂时不清楚原因 有知道的欢迎评论区做客
            // counter: storeMain.counter++; 
            counter: storeMain.counter + 1,
            name: storeMain.name = 'test',
            arr: [ ...storeMain.arr , 4] 
        })
    }
    
    // 方式3: 如果要修改多个数据 $patch(函数) 批量更新
    // 一次性修改多个数据 建议使用$patch 在性能上有优化 一次性修改好数据,更新一次视图
    const handleChangeState3 = () => {
        storeMain.$patch( (state) => {
            state.counter++;
            state.name = 'lisi'
            state.arr.push(4);
        })
    }

</script>

<style lang="scss" scoped>
</style>

我们还可以通过 actions 去修改 state,action 里可以直接通过 this 访问,比较常用的做法。

src/store/index.ts

import { defineStore } from "pinia";

export const useMainStore = defineStore("main", {
  state: () => {
    return {
      counter: 1,
      name: "张三",
      arr: [1, 2, 3],
    };
  },
  getters: {},
  actions: {
    // actions : 不能使用箭头函数,因为箭头函数绑定外部this
    changeState(num: number) {
       this.counter += num;
       this.name = "李四";
       this.arr.push(4);
     },
  },
});

组件内调用actions

<template>
  <div>{{ storeMain.counter }}</div>
  <div>{{ storeMain.name }}</div>
  <div>{{ storeMain.arr }}</div>
  <button @click="handleChangeState4(10)"> 修改 </button>
</template>

<script lang="ts" setup>
    import { useMainStore } from '../store'
    
    const storeMain = useMainStore();
    // 方式4:actions
    const handleChangeState4 = (num: number) => {
         storeMain.changeState(num);
    }
</script>

<style lang="scss" scoped>
</style>

3. getters

类似于组件的computed,用来封装计算属性,有缓存的功能。

src/store/main.ts

import { defineStore } from "pinia";

export const useMainStore = defineStore("main", {
  state: () => {
    return {
      counter: 1,
      name: "张三",
      arr: [1, 2, 3],
    };
  },
  getters: {
    // 函数接受一个可选参数 state 状态对象
    counterDouble(state) {
      console.log('counterDouble调用了');
      return state.counter * 2;
    }
    
    // 细节1:如果在getters中使用了this (并且没有传递state可选参数) 则必须手动指定 返回值类型 
    // 否则ts会有报错提示 
    // counterDouble(): number {
    //   console.log('counterDouble调用了');
    //   return this.counter + 10;
    //},
    
    // 细节2:getters中使用this 参数写了state却没有使用 ts不会有报错提示
    // counterDouble(state) {
    //   console.log('counterDouble调用了');
    //   return this.counter - 10;
    // }
  },
  actions: {
    // actions : 不能使用箭头函数,因为箭头函数绑定外部this
    changeState(num: number) {
      this.counter += num;
      this.name = "李四";
      this.arr.push(4);
    },
  },
});

组件内

<template>
  <button @click="handleChangeState">修改数据</button>
  <div>{{ storeMain.counter }}</div>
  <div>{{ storeMain.name }}</div>
  <div>{{ storeMain.arr }}</div>
  <hr />
  
  <!-- 调用三次getters -->
  <div>双倍getters{{ storeMain.counterDouble }}</div>
  <div>双倍getters{{ storeMain.counterDouble }}</div>
  <div>双倍getters{{ storeMain.counterDouble }}</div>
</template>

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

const storeMain = useMainStore();

const handleChangeState = () => {
  storeMain.changeState(10)
}

</script>

<style lang="scss" scoped>
</style>

gif.gif

在模板中我们调用了三次getters中的函数,但是只打印了一次,不难看出,我们getters是有缓存功能的,数据发生变动的时候,才会去重新调用。

4. actions

类似于组件的methods,用来封装业务逻辑(同步,异步都可以)。

处理同步、异步请求

src/api/test.ts

export const getData = async () => {
  const res = await handleAsyncFns(1000)
  return res
}

// 假设异步请求数据
async function handleAsyncFns(delay: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('data...')
    }, delay);
  });
}

src/store/index.ts

import { defineStore } from "pinia";
import { getData } from '../api/test'

interface stateType  {
  counter: number;
  name: string;
  arr: number[];
  data: unknown;
}

export const useMainStore = defineStore("main", {
  state: (): stateType => ({
      counter: 1,
      name: "张三",
      arr: [1, 2, 3],
      data: 'none'
  }),
  getters: {},
  actions: {
    // 同步请求
    changeState(num: number) {
      this.counter += num;
      this.name = "李四";
      this.arr.push(4);
    },
    // 异步请求
    async getApiData() {
      const res = await getData()
      this.data = res;
      console.log(res)
    }
  },
});

组件内

<template>
  <button @click="handleChangeState">修改数据</button>
  <div>{{ storeMain.counter }}</div> 
</template>

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

const storeMain = useMainStore();
const handleChangeState = () => {
  // 同步操作
  storeMain.changeState(10)
  // 异步操作 假设是网络请求 都可以在actions里面调用
  storeMain.getApiData()
  // 看看state里面的数据
  console.log(storeMain.data)
}
</script>

<style lang="scss" scoped>
</style>

actions之间的相互调用

src/api/test.ts

export const getData = async () => {
  const res = await handleAsyncFns(1000)
  return res
}

// 假设异步请求数据
async function handleAsyncFns(delay: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('data...')
    }, delay);
  });
}

src/store/base.ts

import { defineStore } from "pinia";

interface baseState {
  baseCounter: number;
}

export const useBaseStore = defineStore({
  id: "base",
  state: (): baseState => ({
    baseCounter: 1,
  }),
  getters: {},
  //提供给其他actions 调用的
  actions: {
    handleUpdateState() {
      setTimeout(() => {
        this.baseCounter++;
      }, 500);
    },
  },
});

src/store/index.ts

import { defineStore } from "pinia";

// 导入要调用的actions
import { useBaseStore } from './base'

import { getData } from '../api/test'

interface stateType  {
  counter: number;
  name: string;
  arr: number[];
  data: unknown;
}

export const useMainStore = defineStore("main", {
  state: (): stateType => ({
      counter: 1,
      name: "张三",
      arr: [1, 2, 3],
      data: 'none'
  }),
  getters: {},
  actions: {
    // 调用的方法
    updataOtherActions() {
      const useBaseMain = useBaseStore();
      useBaseMain.handleUpdateState()
    },
    // 同步请求
    changeState(num: number) {
      this.counter += num;
      this.name = "李四";
      this.arr.push(4);
    },
    // 异步请求
    async getApiData() {
      const res = await getData()
      this.data = res;
      // 调用其他actions
      this.updataOtherActions();
      console.log(res)
    }
  },
});

组件内

<template>
  <button @click="handleChangeState">修改数据</button>
  <div>{{ storeMain.counter }}</div> 
  <hr />

  <div>{{ baseMain.baseCounter }}</div>
</template>

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

const storeMain = useMainStore();
const baseMain = useBaseStore();

// 这里顺带调用了其他acitons
const handleChangeState = () => {
  // 同步操作
  storeMain.changeState(10)
  // 异步操作 假设是网络请求 都可以在actions里面调用
  storeMain.getApiData()
  // 看看state里面的数据
  console.log(storeMain.data)
}
</script>

<style lang="scss" scoped>
</style>

5. 数据持久化

5.1 pinia-plugin-persist插件安装

yarn add pinia-plugin-persist
# or with npm
npm install pinia-plugin-persist

5.2 pinia-plugin-persist插件使用

5.3 导入

src/main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia' 

// pinia 数据持久化插件的导入
import piniaPluginPersist from 'pinia-plugin-persist'

import App from './App.vue'

const pinia = createPinia()
// pinia 数据持久化插件的挂载
pinia.use(piniaPluginPersist)

const app = createApp(App)
app.use(pinia)
app.mount('#app')
5.4 配置

在需要持久化的store配置即可,如果有多个store,则需要配置多次。

src/store/index.ts

import { defineStore } from "pinia";

interface stateType {
  counter: number;
  name: string;
}

export const useMainStore = defineStore("main", {
  state: (): stateType => ({
    counter: 1,
    name: "张三"
  }),
  getters: {},
  actions: {
    changeState(num: number) {
      this.counter += num;
      this.name = "李四";
    }
  },
  // 开启数据缓存
  persist: {
    enabled: true,
    strategies: [
      {
        // 自定义 存储时的key
        key: "indexKey",
        // 默认是存储在 sessionStorage里面, 我们进行修改
        storage: localStorage,
        // 指定需要持久化的数据 , 不在指定数据的不会进行持久化
        paths: ["counter"]
      },
    ],
  },
});

组件内

<template>
  <button @click="handleChangeState">修改数据</button>
  <div>{{ storeMain.counter }}</div> 
  <div>{{ storeMain.name }}</div> 
</template>

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

const storeMain = useMainStore();

const handleChangeState = () => {
  storeMain.changeState(10)
}
</script>

<style lang="scss" scoped>
</style>

gif.gif