Pinia 是一个 Vue3 的集中式状态(数据)管理库。
搭建测试项目
先创建一个 Vue3 项目,添加两个组件。
src\components\Count.vue
:
<template>
<div class="count">
<h2>当前求和为:</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"></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
,具体可以参考。
修改根组件src\App.vue
,加载两个子组件:
<template>
<div class="talk">
<button @click="getLoveTalk">获取一句土味情话</button>
<ul>
<li v-for="talk in talkList" :key="talk.id"></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>当前求和为:</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 数据及其变化:
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.
文章评论