红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

Vue3 学习笔记 6:Pinia

2025年9月14日 4点热度 0人点赞 0条评论

Pinia 是一个 Vue3 的集中式状态(数据)管理库。

搭建测试项目

先创建一个 Vue3 项目,添加两个组件。

src\components\Count.vue:

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }}</h2>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="add">加</button>
    <button @click="minus">减</button>
  </div>
</template>
​
<script setup lang="ts" name="Count">
  import { ref } from "vue";
  // 数据
  let sum = ref(1) // 当前求和
  let n = ref(1) // 用户选择的数字
​
  // 方法
  function add(){
    sum.value += n.value
  }
  function minus(){
    sum.value -= n.value
  }
</script>
​
<style scoped>
  .count {
    background-color: skyblue;
    padding: 10px;
    border-radius: 10px;
    box-shadow: 0 0 10px;
  }
  select,button {
    margin: 0 5px;
    height: 25px;
  }
</style>

src\components\LoveTalk.vue:

<template>
  <div class="talk">
    <button @click="getLoveTalk">获取一句土味情话</button>
    <ul>
      <li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li>
    </ul>
  </div>
</template>
​
<script setup lang="ts" name="LoveTalk">
import { reactive } from 'vue'
import axios from "axios";
import { nanoid } from 'nanoid'
// 数据
let talkList = reactive([
  { id: 'ftrfasdf01', title: '今天你有点怪,哪里怪?怪好看的!' },
  { id: 'ftrfasdf02', title: '草莓、蓝莓、蔓越莓,今天想我了没?' },
  { id: 'ftrfasdf03', title: '心里给你留了一块地,我的死心塌地' }
])
// 方法
async function getLoveTalk() {
  // 发请求,下面这行的写法是:连续解构赋值+重命名
  let { data: { content: title } } = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
​
  // 把请求回来的字符串,包装成一个对象
  let obj = { id: nanoid(), title }
  // 放到数组中
  talkList.unshift(obj)
}
</script>
​
<style scoped>
.talk {
  background-color: orange;
  padding: 10px;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}
</style>

组件使用了两个外部包,安装:

npm i axios
npm i nanoid

为了能够使用 setup 标签的name属性还需要添加一个插件vite-plugin-vue-setup-extend,具体可以参考Vue3 学习笔记 2:组合式 API。

修改根组件src\App.vue,加载两个子组件:

<template>
  <div class="talk">
    <button @click="getLoveTalk">获取一句土味情话</button>
    <ul>
      <li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li>
    </ul>
  </div>
</template>
​
<script setup lang="ts" name="LoveTalk">
import { reactive } from 'vue'
import axios from "axios";
import { nanoid } from 'nanoid'
// 数据
let talkList = reactive([
  { id: 'ftrfasdf01', title: '今天你有点怪,哪里怪?怪好看的!' },
  { id: 'ftrfasdf02', title: '草莓、蓝莓、蔓越莓,今天想我了没?' },
  { id: 'ftrfasdf03', title: '心里给你留了一块地,我的死心塌地' }
])
// 方法
async function getLoveTalk() {
  // 发请求,下面这行的写法是:连续解构赋值+重命名
  let { data: { content: title } } = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
​
  // 把请求回来的字符串,包装成一个对象
  let obj = { id: nanoid(), title }
  // 放到数组中
  talkList.unshift(obj)
}
</script>
​
<style scoped>
.talk {
  background-color: orange;
  padding: 10px;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}
</style>

引入 Pinia

安装 pinia:

npm i pinia

修改src\main.ts,让 Vue 应用使用 Pinia:

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')

store

要使用 Pinia,需要为需要统一管理的数据创建一个存储库(store),通常位于src\store目录下。

创建一个存储库src\store\count.ts:

import {defineStore} from 'pinia'
export const useCountStore = defineStore('count', {
  state: () => {
    return {sum: 0}
  },
})

存储库通过pinia的defineStore方法定义,与hooks类似,向外暴露的函数名习惯性命名为useXxxStore。

defineStore接收两个参数,第一个为存储库的分类名称,通常是存储库的名称,这里是count。第二个是一个对象,其state属性是存储库统一管理的数据。state属性必须定义为一个方法,其返回值是一个对象,即具体管理的数据。

读取 state

<template>
  <div class="count">
    <h2>当前求和为:{{ countStore.sum }}</h2>
    <!-- ... -->
  </div>
</template>
​
<script setup lang="ts" name="Count">
import { ref } from "vue";
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
// ...
</script>

通过useCountStore方法获取到的是一个reactive定义的响应式对象(Proxy),需要读取的state数据可以通过对应的属性(这里是.sum)获取,这是一个ref定义的响应式对象。

如果reactive的属性是一个ref定义的响应式对象,访问其值的时候不需要使用.value。

通过浏览器插件 Vue dev tools 可以直观看到 Pinia 中存储的 state 数据及其变化:

image-20250914132023770

Edge 浏览器似乎对该插件的支持不好,很容易出现识别不到数据的问题,建议使用 Firefox 调试 Vue 项目。

除了这种简单方式,其实 store 中的 state 数据都被封装在$state属性中,所以也可以:

    <h2>当前求和为:{{ countStore.$state.sum }}</h2>

效果是相同的。

修改 state

最简单的方式是直接修改 state 属性值,比如:

function add() {
  countStore.sum += n.value
}
function minus() {
  countStore.sum -= n.value
}

当然同样可以通过$state:

countStore.$state.sum += n.value

如果要修改的数据很多,比如:

export const useCountStore = defineStore('count', {
  state: () => {
    return {
        sum: 0,
        name: 'zhangsan',
        age: 18,
        address: 'beijing'
    }
  },
})

可以使$patch方法一次性修改多个 state 数据:

function add() {
  countStore.$patch({ 
    sum: countStore.sum + n.value,
    name: '张三',
    age: countStore.age + 1,
    address: '上海'
  })
}

最后一种方式是通过 store 的 action 修改 state 数据。先定义 action:

import { defineStore } from "pinia";
export const useCountStore = defineStore("count", {
  actions: {
    increment(step: number) {
      this.sum = this.sum + step;
    },
  },
  state: () => {
    // ...
  },
});

调用 action 修改数据:

function add() {
  countStore.increment(n.value)
}

这种方式的好处是可以在 action 中实现复杂逻辑并实现复用。

storeToRefs

如果使用 store 中的数据时都需要使用 xxxStore.xxx 就会显得很不方便,可以利用结构赋值来进行简化:

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }}</h2>
    <h2>{{ name }} {{ age }} {{ address }}</h2>
    <!-- ... -->
  </div>
</template>

<script setup lang="ts" name="Count">
import { ref, toRefs } from "vue";
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
const { sum, name, age, address } = toRefs(countStore)
// ...
</script>

需要注意的是,从reactive定义的响应式对象中结构出响应式对象的属性,需要使用toRefs。

虽然这样做可行,但不够优雅,因为toRefs会将countStore的所有属性都包装成ref定义的响应式对象,包括方法。因此 Pinia 提供一个storeToRefs,使用它可以仅将store中的state数据包装成ref定义的响应式数据,以用于结构赋值:

import { storeToRefs } from 'pinia'
const countStore = useCountStore()
const { sum, name, age, address } = storeToRefs(countStore)

getters

可以定义 getter 来进一步处理数据并返回,其用途有点类似于计算属性。

定义一个 getter:

import { defineStore } from "pinia";
export const useCountStore = defineStore("count", {
  // ...
  state: () => {
    return {
      sum: 0,
      name: "zhangsan",
      age: 18,
      address: "beijing",
    };
  },
  getters: {
    double: (state) => state.sum * 2,
  },
});

为store添加一个getter属性,并在其中添加一个double命名的方法,该方法返回一个值,其值是sum的两倍。这个方法可以接收一个参数,实际上这个参数就是 store 的数据集,可以通过它获取 getter 需要处理的数据。除此以外,也可以像 action 中一样,使用this对象获取数据:

  getters: {
    double: (state) => state.sum * 2,
    bigSum(): number {
      return this.sum * 1000;
    },
  },

使用this存在一些限制,比如这里不能使用匿名函数定义bigSum。

可以像使用普通 state 数据一样使用 getter:

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }} double:{{ double }} bigSum:{{ bigSum }}</h2>
    <h2>{{ name }} {{ age }} {{ address }}</h2>
    <!-- ... -->
</template>

<script setup lang="ts" name="Count">
// ...
const { sum, name, age, address, double, bigSum } = storeToRefs(countStore)
// ...
</script>

订阅

store 有一个方法$subscribe,通过它可以监视 store 中数据的变化:

loveTalkStore.$subscribe(() => {
  console.log('loveTalkStore', loveTalkStore.talkList)
})

其用途类似于watch方法。

可以利用它实现数据的持久化,比如在组件src\components\LoveTalk.vue中添加订阅:

loveTalkStore.$subscribe(() => {
  // 数据发生变化时将数据保存到本地存储
  localStorage.setItem('talkList', JSON.stringify(loveTalkStore.talkList))
})

这样每次 store 中的数据修改后,就会保存到本地存储。

修改 store 中的 state 定义,让数据从本地存储加载:

// ...
export const useLoveTalkStore = defineStore("loveTalk", {
  // ...
  state: () => {
    let talkList = []
    // 如果本地存储中有,从本地存储加载数据
    const storedTalkList = localStorage.getItem("talkList")
    if (storedTalkList) {
      talkList = JSON.parse(storedTalkList);
    }
    else{
        // 如果没有,就使用默认数据
        talkList = [
          { id: "ftrfasdf01", title: "今天你有点怪,哪里怪?怪好看的!" },
          { id: "ftrfasdf02", title: "草莓、蓝莓、蔓越莓,今天想我了没?" },
          { id: "ftrfasdf03", title: "心里给你留了一块地,我的死心塌地" },
        ];
    }
    return { talkList };
  },
});

组合式

之前定义 store 都使用的是选项式风格,也可以使用组合式风格定义 store:

import { defineStore } from "pinia";
import axios from "axios";
import { nanoid } from "nanoid";
import { reactive } from "vue";
export const useLoveTalkStore = defineStore("loveTalk", () => {
  let talkListData = []
  if (localStorage.getItem("talkList")) {
    talkListData = JSON.parse(localStorage.getItem("talkList") || "");
  } else {
    talkListData = [
      { id: "ftrfasdf01", title: "今天你有点怪,哪里怪?怪好看的!" },
      { id: "ftrfasdf02", title: "草莓、蓝莓、蔓越莓,今天想我了没?" },
    ]
  }
  const talkList = reactive<{ id: string; title: string }[]>(talkListData);
  async function getLoveTalk() {
    let {
      data: { content: title },
    } = await axios.get("http://api.uomg.com/api/rand.qinghua?format=json");

    // 把请求回来的字符串,包装成一个对象
    let obj = { id: nanoid(), title };
    // 放到数组中
    talkList.unshift(obj);
  }
  return { talkList,getLoveTalk };
});

此时defineStore的第二个参数不是对象而是匿名方法,该匿名方法的用法类似于setup方法,其中定义的数据就相当于 state 数据,方法就相当于 action,最后通过返回值返回即可。

The End.

参考资料

  • 尚硅谷Vue3入门到实战

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: pinia vue
最后更新:2025年9月14日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号