Pinia 是 Vue 的一个状态管理工具,它的功能和 Vuex 差不多,主要用于 Vue3 的状态管理,但也可以用于 Vue2。目前 Vue 官方推荐的状态管理工具也是 Pinia,它相比 Vuex 来说,在扩展性和对 TypeScript 的支持要更好。

我这里搭配 Pinia 使用的是 Vue3,使用的写法是 script setup 组合式 API,如果你对 Vue3 的 script setup 的组合式 API 还不太了解的话,可以先看一下 Vue3 的 setup 语法糖

安装和使用

进入 Vue 项目目录,使用 npm 安装:

npm install pinia --save

main.js 中引入和注册 Pinia:

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

const app = createApp(App);

const pinia = createPinia();  // 创建 pinia
app.use(pinia);  // 注册 pinia

app.mount('#app');

编写 Store

Store 是 Pinia 的核心仓库,里面包含了你需要在应用中使用的各种状态和操作状态的方法。

为了方便管理,你可以在 src 目录中创建一个 store 目录,把 store 相关的 JS 文件都放到 store 目录中。

下面是一个简单的 store:

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      count: 1,
      text: '呵呵'
    }
  },
  actions: {
    chCount() {
      this.count ++;
    },
    chText() {
      this.text = '我的博客 misterma.com';
    }
  }
});

你需要引入一个 defineStore 来创建 store。

在导出模块的时候,按照 Pinia 官方的命名规范,需要以 use 开头,store 结尾。

defineStore 的第一个参数 ID 是你的 store 仓库名称,第二个参数就是选项配置。在选项配置中 state 就是状态,你可以把需要存放的数据放到 state 中,它比较类似于组件中的 data ,写法也和组件中的 data 差不多。actions 就是操作 state 状态的方法,和组件中的 methods 比较类似。

我上面的 Store 中配置了 counttext 两个状态,在 actions 中编写了 chCountchText 两个方法来更改 counttext

获取 Store 状态

在你需要使用 store 的组件中引入编写的 store,然后调用 store:

<template>
  <div>
      <p>{{ store.text }}</p>
      <p>{{ store.count }}</p>
  </div>
</template>

<script setup>
import { useTestStore } from './store';

const store = useTestStore();
// 在控制台输出 store 的状态
console.log(store.count);
console.log(store.text);
</script>

这里获取的 store 是一个使用 reactive 包装的对象,在读取 store 状态的时候不需要在 value 中获取,可以直接读取状态。

如果你还不太了解 reactive 的话,可以看一下 Vue3 的 setup 语法糖 中的 响应式

更改 Store 状态

在实例化 store 后可以直接调用 actions 里的方法来更改状态:

<template>
  <div>
      <p>{{ store.text }}</p>
      <p>{{ store.count }}</p>
      <button @click="buttonClick">更改 Store 状态</button>
  </div>
</template>

<script setup>
import { useTestStore } from './store';

const store = useTestStore();

function buttonClick() {
  store.chCount();
  store.chText();
}
</script>

Action 相关

action 类似于组件里的 method,操作 state 状态的方法就写在 actions 中。

上面简单的写了 chTextchCount 两个方法来更改 state 里的 textcount ,更改的内容也是写死的,如果你需要动态获取内容更改的话,actions 里的方法也可以接收参数:

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      count: 1,
      text: '呵呵'
    }
  },
  actions: {
    chText(text) {
      this.text = text;
    }
  }
});

下面调用 actions 中的 chText 动态传入内容更改:

import { useTestStore } from './store';

const store = useTestStore();
store.chText('Hello');

action 是支持异步操作的,你甚至可以在 action 中发送 HTTP 请求。

下面使用 Fetch 在 action 中发送 HTTP 请求,然后把请求到的内容传给 state 里的 text

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      text: '呵呵'
    }
  },
  actions: {
    async chText() {
      // 发送 fetch 请求
      const data = await fetch('data.txt');
      // 把请求到的数据转换为普通 text 文本传给 state 的 text
      this.text = await data.text();
    }
  }
});

Getter

Getter 类似于组件里的 computed 计算属性,它可以对 state 里的数据做一些计算处理。

下面是一个简单的 store

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      userGroup: 1
    }
  }
});

state 中包含一个 userGroup ,我需要根据这个 userGroup 的数字输出 1:普通用户2:管理员3:超级管理员 ,如果在组件中先获取 userGroup 的值在判断输出的话,可能会是下面这样:

<template>
  <div>{{ userGroup }}</div>
</template>

<script setup>
import { useTestStore } from './store';
import { ref } from 'vue';

const store = useTestStore();
const userGroup = ref('');

if (store.userGroup === 1) {
  userGroup.value = '普通用户';
}else if (store.userGroup === 2) {
  userGroup.value = '管理员';
}else if (store.userGroup === 3) {
  userGroup.value = '超级管理员';
}else {
  userGroup.value = '账号异常';
}
</script>

这种复杂的判断如果直接放到组件里的话,还是比较影响代码可读性的,而且如果多个组件都需要判断输出的话,也需要写很多遍。

下面把这些判断的代码放到 store 的 Getter 中:

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      userGroup: 1
    }
  },
  getters: {
    userGroupName(state) {
      if (state.userGroup === 1) {
        return '普通用户';
      } else if (state.userGroup === 2) {
        return '管理员';
      } else if (state.userGroup === 3) {
        return '超级管理员';
      } else {
        return '账号异常';
      }
    }
  }
});

我在组件中只需要调用 getters 里的 userGroupName 就可以输出计算后的 userGroup

<template>
  <div>{{ store.userGroupName }}</div>
</template>

<script setup>
import { useTestStore } from './store';
const store = useTestStore();
</script>

组合式 Setup Store

上面的 store 用的都是选项式的写法,Pinia 也支持组合式的写法,下面通过组合式编写一个简单的 store:

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useTestStore = defineStore('test', () => {
  // 相当于 state
  const userGroup = ref(1);

  // 相当于 actions
  function changeGroup(group) {
    userGroup.value = group;
  }

  // 相当于 getters
  const userGroupName = computed(() => {
    if (userGroup.value === 1) {
      return '普通用户';
    } else if (userGroup.value === 2) {
      return '管理员';
    } else if (userGroup.value === 3) {
      return '超级管理员';
    } else {
      return '账号异常';
    }
  });

  // 把状态和方法暴露出去
  return { userGroup, changeGroup, userGroupName };
});

在组件中还是一样的调用:

<template>
  <div>
    {{ store.userGroup }}
    {{ store.userGroupName }}
    <button @click="store.changeGroup(3)">更改 userGroup</button>
  </div>
</template>

<script setup>
import { useTestStore } from './store';
const store = useTestStore();
</script>

在组合式写法中 ref 就是 state 状态,函数就是 actions 方法,computed 就是 getters

在 Vue2 中使用 Pinia

我这里使用的是 Vue2.7,可以直接使用 Pinia。Vue2.7 以下的版本因为没有 setup 组合式 API,用起来可能会比较麻烦。

main.js 中引入和注册 pinia:

import Vue from 'vue';
import App from './App.vue';
import { createPinia, PiniaVuePlugin } from 'pinia';

Vue.use(PiniaVuePlugin);
const pinia = createPinia();

new Vue({
  render: h => h(App),
  pinia
}).$mount('#app');

在 Vue2 中除了引入 createPinia 外还需要引入 PiniaVuePlugin ,使用 Vue.use 注册 PiniaVuePlugin ,实例化 vue 的时候传入 createPinia

store 的编写和 Vue3 是一样的,下面是一个简单的 store:

import { defineStore } from 'pinia';

export const useTestStore = defineStore('test', {
  state() {
    return {
      count: 1,
      text: '在 Vue2 中使用 Pinia'
    }
  },
  actions: {
    chCount() {
      this.count ++;
    },
    chText(text) {
      this.text = text;
    }
  }
});

下面在组件中使用 store:

<template>
  <div id="app">
    {{ store.text }}
    {{ store.count }}
    <button @click="buttonClick">更改count</button>
  </div>
</template>

<script>
import { useTestStore } from './store';

export default {
  name: 'App',
  setup() {
    // 实例化 store
    const store = useTestStore();
    // 把 store 暴露到 this
    return { store };
  },
  methods: {
    buttonClick() {
      // 调用 store 中的 chCount 来更改 count
      this.store.chCount();
    }
  }
}
</script>

在要使用 store 的组件中引入编写的 store,在 setup 函数中实例化 store,然后把 store 暴露到 this

如果你的 Vue3 使用的是选项式 API 也可以用上面的方式使用 store。