0%

Pinia的使用

官网:
bilibili 教学视频

一、环境初始化

1.创建项目

1
2
3
4
5
$ yarn create vite
# or
$ npm create vite@latest
# or
$ pnpm create vite

然后跟着提示一步步走,使用 ts

2.安装 Pinia

1
2
3
yarn add pinia
# or with npm
npm install pinia

二、基本使用

1.创建 Pinia 示例并挂载

main.js
1
2
3
4
5
6
7
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
// 创建pinia实例
const pinia = createPinia();
// 挂载到根实例上
createApp(App).use(pinia).mount("#app");

如果使用的是 Vue2,还需要安装一个插件,并将创建一个Pinia注入到应用的 root:

main.js
1
2
3
4
5
6
7
8
9
10
11
12
import { createPinia, PiniaVuePlugin } from "pinia";

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

new Vue({
el: "#app",
// 其他选项...
// ...
// 注意同一个pinia实例可以在多个Vue应用中使用
pinia,
});

2.基本使用

** `store` 是使用`defineStore()` 定义的,第一个参数是整个应用中 store 的唯一名称(id)**

建议: > 可以为**defineStore()**的返回值任意命名,但是最好使用**use**加上**store**的名称和**Store**,例如:**useUserStore****useCartStore****useProductStore**

类似于 Vue 的选项 API,也可以传递一个带有**state****actions****getters**属性的选项对象.
**state**就类似于组件的 **data** ,用来存储全局状态的,**getters**就类似于组件的 **computed**,用来封装计算属性,有缓存功能,**actions**类似于组件的 **methods**,用来封装业务逻辑,修改 **state**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { defineStore } from "pinia";

// 1.定义并导出容器
// 参数1:容器的ID,必须唯一,将来Pinia会把所有的容器挂载到根容器,每个容器的名字就是这里的ID
export const useMainStore = defineStore("main", {
/**
* 2.容器中的state
* 类似与组件的data, 用来存储全局状态
* 1.必须是函数:这样是为了在服务端渲染的时候避免交叉请求导致的数据状态污染(客户端其实无所谓)
* 2.必须是箭头函数:为了更好的ts类型推导
* 返回值:一个函数,调用该函数即可得到容器实例
*/
state: () => {
return {
count: 100,
foo: "bar",
arr: [1, 2, 3],
};
},

/**
* 3.修改state
* 类似于组件的computed,用来封装计算属性,有【缓存】功能
*/
getters: {
// 每个函数接受一个可选参数:state状态对象
count10(state) {
console.log("count10()调用了"); // 具有缓存功能
return state.count + 10;
},

// (不建议)如果不使用state而使用this,此时就不能对返回值类型做自动推导了,必须手动指定
// count10(): number {
// return this.count + 10
// }
},

/**
* 4.actions的使用
* 完全类比于Vue2组件中的methods(可以直接用this),用来【封装业务逻辑】,修改state
*/
actions: {
/**
* 注意!!不能使用箭头函数定义actions!!一定要用普通函数!!!
* why?因为箭头函数绑定了外部this
*/
changeState(num: number) {
// 可以直接使用this,像极了Vue2
// this.count++
// this.foo = 'hello'
// this.arr.push(4)

// 对于批量修改,建议使用patch做优化
this.$patch((state) => {
state.count += num;
state.foo = "hello";
state.arr.push(4);
});
},
},
});

打开 App.vue,砍掉没用的,我们直接使用项目中HelloWorld.vue组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
<HelloWorld />
</template>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

下面是HelloWorld.vue的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<template>
<p>{{ mainStore.count }}</p>
<p>{{ mainStore.foo }}</p>
<p>{{ mainStore.arr }}</p>
<p>{{ mainStore.count10 }}</p>
<p>{{ mainStore.count10 }}</p>
<p>{{ mainStore.count10 }}</p>
<hr />
<p>{{ count }}</p>
<p>{{ foo }}</p>
<p>
<button @click="handleChangeState">修改数据</button>
</p>
</template>

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

// 【哪里使用写哪里】,此时要在HelloWorld组件中用,那就写这里。这很Composition API
const mainStore = useMainStore();

// 禁止!这样会丧失响应性,因为pinia在底层将state用reactive做了处理
// const { count, foo } = mainStore
// 解决方案:将结构出的数据做ref响应式代理
const { count, foo } = storeToRefs(mainStore);

const handleChangeState = () => {
// ==============数据修改的几种方式=============
// 方式一:直接修改
// mainStore.count++

// 方式二:使用 $patch(对象) 批量修改,建议使用,底层做了性能优化
// mainStore.$patch({
// count: mainStore.count + 1,
// foo: 'hello',
// arr: [...mainStore.arr, 4] // 这就不优雅了,所以有了方式三
// })

// 方式三:使用 $patch(回调函数),这个更优雅,墙裂推荐!!!
// 回调函数中的state参数,就是Store定义时里面的state!
// mainStore.$patch((state) => {
// state.count++
// state.foo = 'hello'
// state.arr.push(4)
// })

// 方式四:逻辑较为复杂时,应封装到Store的actions中,并对外暴露接口
mainStore.changeState(10);
};
</script>

从以上几种修改 store 数据的方式,可以看出 Pinia 的使用非常的简便+灵活,也非常的Composition API。推荐使用后两种方式。

三、购物车案例

1.准备工作

需求说明

  • 商品列表
    • 展示商品列表
    • 添加到购物车
  • 购物车
    • 展示购物车商品列表
    • 展示总价格
    • 订单结算
    • 展示结算状态

页面模板

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h1>Pinia - 购物车展示</h1>
<hr />
<h2>商品列表</h2>
<ProductList></ProductList>
<hr />
<ShoppingCart></ShoppingCart>
</template>

<script setup lang="ts">
import ProductList from "./components/ProductList.vue";
import ShoppingCart from "./components/ShoppingCart.vue";
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<ul>
<li>
商品名称 - 商品价格
<button>添加到购物车</button>
</li>
<li>
商品名称 - 商品价格
<button>添加到购物车</button>
</li>
<li>
商品名称 - 商品价格
<button>添加到购物车</button>
</li>
</ul>
</template>

<script lang="ts" setup></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="cart">
<h2>我的购物车</h2>
<p>
<i>请添加一些商品到购物车</i>
</p>
<ul>
<li>商品名称 - 商品价格 × 商品数量</li>
<li>商品名称 - 商品价格 × 商品数量</li>
<li>商品名称 - 商品价格 × 商品数量</li>
</ul>
<p>商品总价</p>
<p>
<button>结算</button>
</p>
<p>结算成功 / 失败.</p>
</div>
</template>

<script lang="ts" setup></script>

数据接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* src/api/shop.ts
* Mocking client-server processing
*/

export interface IProduct {
id: number;
title: string;
price: number;
inventory: number; // 库存
}

const _products: IProduct[] = [
{ id: 1, title: "iPad 4 Mini", price: 500.01, inventory: 2 },
{ id: 2, title: "H&M T-Shirt White", price: 10.99, inventory: 10 },
{ id: 3, title: "Charli XCX -Sucker CD", price: 19.99, inventory: 5 },
];

export const getProducts = async () => {
await wait(1000);
return _products;
};

export const buyProducts = async () => {
await wait(1000);
return Math.random() > 0.5;
};

/**
* 封装了Promise版本的定时器
* @param delay 延迟时间
* @returns Promise
*/
async function wait(delay: number) {
return new Promise((resolve) => setTimeout(resolve, delay));
}

2.开始开发

定义 Store

以下代码学习点:

  • as 类型断言
  • 如何在 actions 中写异步操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { defineStore } from "pinia";
import { getProducts, IProduct } from "../api/shop";

export const useProdunctsStore = defineStore("products", {
state: () => {
return {
all: [] as IProduct[], // 所有商品列表(学习类型断言的使用)
};
},

getters: {},

actions: {
//actions既支持异步操作
async loadAllProducts() {
const ret = await getProducts();
this.all = ret;
},
// 也支持同步操作
decrementProduct(product: IProduct) {
const ret = this.all.find((item) => item.id === product.id);
if (ret) {
ret.inventory--;
}
},
},
});

下面的代码,有以下学习点:

  • type 类型合并与过滤
  • 跨容器通信的极致优雅操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { defineStore } from "pinia";
import { buyProducts, IProduct } from "../api/shop";
import { useProdunctsStore } from "./products";

/**
* {id, title, price, quantity}
*/
type CartProduct = {
// 合并
num: number;
} & Omit<IProduct, "inventory">; // Omit是过滤

export const useCartStore = defineStore("cart", {
state: () => {
return {
cartProducts: [] as CartProduct[],
checkoutStatus: null as null | string,
};
},

getters: {
// 总价
totalPrice(state) {
return state.cartProducts.reduce((total, item) => {
return total + item.price * item.num;
}, 0);
},
},

actions: {
/**
* 往购物车添加商品
* 这是一个相对复杂的逻辑,与容器中的数据强相关,所以肯定要定义在actions里面!
* @param product 需要添加的商品
*/
addProductToCart(product: IProduct) {
// 先看商品有没有库存
if (product.inventory <= 0) {
return;
}
// 检查购物车中是否已有该商品

const cartItem = this.cartProducts.find((item) => item.id === product.id);
// 如果有则让商品数量+1
if (cartItem) {
cartItem.num++;
} else {
// 如果没有则添加到购物车列表中
this.cartProducts.push({
id: product.id,
title: product.title,
price: product.price,
num: 1,
});
}

// 跟新商品库存(应该由商品容器暴露API)
const productsStore = useProdunctsStore();
productsStore.decrementProduct(product);
// 跨容器通信!!!!!竟然如此简单!!!
},
/**
* 结算
*/
async checkOut() {
const ret = await buyProducts();
this.checkoutStatus = ret ? "成功" : "失败";
if (ret) {
this.cartProducts = []; // 清空购物车
}
},
},
});

重写组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<ul>
<li v-for="item in productsStore.all">
{{ item.title }} - {{ item.price }}
<br />
<button
:disabled="!item.inventory"
@click="cartStore.addProductToCart(item)"
>
添加到购物车
</button>
</li>
</ul>
</template>

<script lang="ts" setup>
import { useCartStore } from "../store/cart";
import { useProdunctsStore } from "../store/products";

const productsStore = useProdunctsStore();
const cartStore = useCartStore();

productsStore.loadAllProducts(); // 加载所有数据
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="cart">
<h2>我的购物车</h2>
<p>
<i>请添加一些商品到购物车</i>
</p>
<ul>
<li v-for="item in cartStore.cartProducts">
{{ item.title }} - {{ item.price }} × {{ item.num }}
</li>
</ul>
<p>商品总价: {{ cartStore.totalPrice }}</p>
<p>
<button @click="cartStore.checkOut">结算</button>
</p>
<p v-show="cartStore.checkoutStatus">结算{{ cartStore.checkoutStatus }}.</p>
</div>
</template>

<script lang="ts" setup>
import { useCartStore } from "../store/cart";
const cartStore = useCartStore();
</script>
赏包辣条吃吧