Vue3 内 Pinia 了解及使用

213 阅读5分钟

官网

概念

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 支持vue2和vue3

优点:

  • Devtools 支持

    • 追踪 actions、mutations 的时间线
    • 在组件中展示它们所用到的 Store
    • 让调试更容易的 Time travel
  • 热更新

    • 不必重载页面即可修改 Store
    • 开发时可保持当前的 State
  • 插件:可通过插件扩展 Pinia 功能

  • 为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。

  • 支持服务端渲染

本文基于vue3+vite+ts+Element plus项目内容 展开从了解到实现一个简单Demo,主要vue3内容,ts内部内容较少,可以忽略不计,不影响整体 Element Plus内的使用是标签带了el- ,没有安装的,去除这个即可

安装

yarn add pinia
# 或者使用 npm
npm install pinia

引入

引入createPinia方法,将pinia挂载到Vue中

main.ts

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


const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Store

store,数据存储仓库,使用defineStore()定义。

可以对 defineStore() 的返回值进行任意命名,但最好使用 store 的名字,同时以 use 开头且以 Store 结尾。(比如 useUserStoreuseCartStoreuseProductStore)

第一个参数是你的应用中 Store 的唯一 ID,这个id是必须传入的,Pinia 将用它来连接 store 和 devtools。

创建文件:src/stores/counter.ts

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
    // 其他配置
})

defineStore()的第二个参数采用Option对象

使用 store

views文件夹下创建组件 Counter.vue,并在 App.vue内引入

App.vue

<script setup lang="ts">
import Counter from './views/Counter.vue'
</script>

<template>
  <Counter/>
</template>

创建完成后,在组件Counter.vue内使用

src/views/Counter.vue

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

// 可以在组件中的任意位置访问 `store` 变量 ✨
const counterStore = useCounterStore();
console.log("counterStore", counterStore);
</script>

打印结果:

图片.png

关于store内部属性的含义,可以看官网属性

defineStore的第二个参数(纯介绍,未在实例内)

defineStore()第二个参数可以接受两类值:Setup函数或者Option对象

下面这两种写法二选一即可

Option Store

传入一个带有 stateactionsgetters 属性的 Option 对象

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

Setup Store

Setup函数类似,可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

上面是两种方法介绍,下面例子采用 Option Store进行展开。

State

添加State

在前面配置中,counter.ts 内store创建完成,第一个是store 的id,写为 counter ,第二个参数是option对象,数据需要添加到option对象内的state属性

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    countPow: 0
  })
})

获取state

获取store内的数据展示到页面

views/Count.vue

<template>
    <div>
        <p>当前计数:{{ counterStore.count }}</p>
        <div>当前数据二次方:{{ counterStore.countPow }}</div>
    </div>
</template>

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

const counterStore = useCounterStore();
</script>

获取数据并展示到页面上

图片.png

修改数据

修改store内state的数据,需要store内数据并进行修改,但是上面的代码在数据量较少的情况下使用比较清晰,但是代码量较大数据较多时,各种store穿插,代码会显得很多

在改数据的同时,解构数据,但是state内的数据解构后,数据响应会失效,所以需要storeToRefs将state内的数据变为响应式的。

用法:

import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();

let { count, countPow } = storeToRefs(counterStore)

组件内使用:

<template>
    <div>
        <p>当前计数:{{ count }}</p>
        <div>当前数据二次方:{{ countPow }}</div>
        <el-button @click="add" type="primary">增加</el-button>
        <el-button @click="sub" type="primary">减少</el-button>
    </div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();

let { count, countPow } = storeToRefs(counterStore)

const add = () => {
    count.value++
    console.log(counterStore)
};
const sub = () => {
    count.value--
};
</script>

运行结果:

图片.png

Getter

Getter 完全等同于 store 的 state 的计算值。计算属性 computed 的使用。通过 this,你可以访问到其他任何 getter。

getters 添加:

counter.ts

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    countPow: 0
  }),
  getters: {
    doubleCount: (state) => {
      return state.count * 2;
    },
  },
})

数据获取

获取当前数据的二倍值,和获取state的方法一致

Count.vue

<div>计算当前数据二倍值:{{ doubleCount }}</div>


let { count, doubleCount, countPow } = storeToRefs(counterStore)

运行结果:

图片.png

Action

通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)action 可以是异步的

同步方法

改动一下数据count的加减计算

添加

defineStore()内的 actions 属性

stores/counter.ts

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    countPow: 0
  }),
  getters: {
    doubleCount: (state) => {
      return state.count * 2;
    },
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    }
  }
})

使用

获取方法,为了简单,我选择解构,不解构可以直接使用counterStore.increment

views/Counter.vue

<template>
    <div>
        <p>当前计数:{{ count }}</p>
        <div>计算当前数据二倍值:{{ doubleCount }}</div>
        <div>当前数据二次方:{{ countPow }}</div>
        <el-button @click="add" type="primary">增加</el-button>
        <el-button @click="sub" type="primary">减少</el-button>
    </div>
</template>

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

const counterStore = useCounterStore();

// storeToRefs 将 store内state的数据转变为响应式
let { count, doubleCount, countPow } = storeToRefs(counterStore)
let { increment, decrement } = counterStore

const add = () => {
    increment();
};
const sub = () => {
    decrement();
};

</script>

方法解构不需要 响应式,直接解构即可,效果如下:

图片.png

异步方法

异步就是请求接口类似的,添加一个模拟异步接口

actions: {
    async powCount() {
      //两秒后数据 乘于它的次方
      await delay(2000)
      this.countPow = this.count ** 2
    }
}

获取和其他方法一致:

let { increment, decrement, powCount } = counterStore

powCount()

下面是全量代码,异步代码详细内容可在里面查看!

全量示例代码

main.ts

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


const app = createApp(App)

app.use(ElementPlus)
app.use(createPinia())
app.mount('#app')

App.vue

<script setup lang="ts">
import Counter from './views/Counter.vue'
</script>

<template>
  <Counter/>
</template>

src/views/Counter.vue

<template>
    <div>
        <p>当前计数:{{ count }}</p>
        <div>计算当前数据二倍值:{{ doubleCount }}</div>
        <div>当前数据二次方:{{ countPow }}</div>
        <el-button @click="add" type="primary">增加</el-button>
        <el-button @click="sub" type="primary">减少</el-button>
        <el-button @click="pow" type="primary">异步 count的二次方</el-button>
        <div style="font-size: 12px;">两秒后增加当前数据的二次方&nbsp;
            ⚠️:0和1的二次方不变
        </div>
    </div>
</template>

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

const counterStore = useCounterStore();
console.log("counterStore", counterStore);

// storeToRefs 将 store内state的数据转变为响应式
let { count,doubleCount, countPow  } = storeToRefs(counterStore)
let { increment, decrement, powCount  } = counterStore

const add = () => {
    increment();
};
const sub = () => {
    decrement();
};
const pow = () => {
    powCount()
}
</script>

下面是两种写法具体对比,二选一即可 src/stores/counter.ts

下面这个是optionduix写法:

import { defineStore } from 'pinia'

// 模拟异步接口
const delay = function delay(interval: number) {
  typeof interval !== "number" ? interval = 1000 : null;
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("");
    }, interval);
  });
};
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    countPow: 0
  }),
  getters: {
    doubleCount: (state) => {
      return state.count * 2;
    },
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async powCount() {
      //两秒后数据 乘于它的次方
      await delay(2000)
      this.countPow = this.count ** 2
    }
  }
})

src/stores/counter.ts

下面是setup函数写法

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

const delay = function delay(interval: number) {
  typeof interval !== "number" ? interval = 1000 : null;
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("");
    }, interval);
  });
};
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const countPow = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  function decrement() {
    count.value--
  }
  const powCount = async () => {
    //两秒后数据 乘于它的次方
    await delay(2000)
    countPow.value = count.value ** 2
  }

  return { count, countPow, doubleCount, increment, decrement, powCount }
})

总结

pinia相对来说,代码简单很多,没有 mutations,action支持同步异步,而且模块定义 在 defineStore()内定好唯一id

感觉整体和mobx还是挺像的,具体就不深究了

有不足的欢迎指出!