先来看早期 vue 上一个关于 Vuex 5.x 的 RFC :
- 同时支持 composition api 和 options api 的语法;
- 去掉 mutations,只有 state、getters 和 actions;
- 不支持嵌套的模块,通过组合 store 来代替;
- 更完善的 Typescript 支持;
- 清晰、显式的代码拆分;
而 Pinia 正是基于 RFC 所生成的一个玩物。
它的定位和特点也很明确:
- 直观,像定义组件一样地定义 store,并且能够更好地组合它们;
- 完整的 Typescript 支持;
- 关联 Vue Devtools 钩子,提供更好地开发体验;
- 模块化设计,能够构建多个 stores 并实现自动地代码拆分;
- 极其轻量(1kb),甚至感觉不到它的存在 ;
- 同时支持同步和异步 actions;
接触
用 vite 快速起一个 vue 模板的项目:
yarn create @vitejs/app pinia-learning --template vue-ts
cd pinia-learning
yarn
yarn dev
项目运行起来后,安装 pinia 并初始化一个 store:
yarn add pinia
在 src/main.ts 下定义引用 pinia 插件:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
createApp(App).use(createPinia()).mount('#app')
了解(State)
defineStore
之后就可以定义我们的 store 并在组件中使用,我们新建 src/store/index.ts 文件并定义一个 store:
import { defineStore } from 'pinia'
export default defineStore({
id: 'app',
state () {
return {
name: '码农'
}
}
})
在 App.vue 引入上述文件就可以使用该 store:
<template>
<div>{{ store.name }}</div>
</template>
<script setup lang="ts">
import useAppStore from './store/index'
const store = useAppStore()
console.log(store)
</script>
defineComponent <==> defineStore、id <==> name、 state <==> setup,直观,像定义组件一样地定义 store 到这里是能够体会到该特性的含义了。上述代码是在 composition api 中 setup 的用法,在 options api 中使用跟 Vuex 类似,通过 mapState 或者 mapWritableState 辅助函数来读写 state:
<template>
<div>{{ this.username }}</div>
<div>{{ this.interests.join(',') }}</div>
</template>
<script lang="ts">
import { mapState, mapWritableState } from 'pinia'
import infoStore from '../store/info'
export default {
name: 'HelloWorld',
computed: {
// 只读计算属性
...mapState(infoStore, ['interests']),
// 读写计算属性
...mapWritableState(infoStore, {
username: 'name'
})
},
mounted () {
this.interests.splice(1, 1, '足球')
this.username = 'Jouryjc'
}
}
</script>
storeToRefs
那么后半句是啥意思呢?并且能够更好地组合它们 。举个例子,马上就 11.11 ,当然得往购物车里面塞几本“葵花宝典”,于是乎就需要一个 cart 的 store:
import { defineStore } from 'pinia'
export default defineStore('cart', {
state () {
return {
books: [
{
name: '金瓶梅',
price: 50
},
{
name: '微服务架构设计模式',
price: 139
},
{
name: '数据密集型应用系统设计',
price: 128
}
]
}
}
})
然后在 AppStore 组合 cartStore :
// store/index.ts
import { defineStore } from 'pinia'
import useCartStore from './cart'
export default defineStore('app', {
state () {
// 直接使用 cartStore
const cartStore = useCartStore()
return {
name: '码农',
books: cartStore.books
}
}
})
最终被 App 组件消费。⚠️ 直接解构 store 会使其失去响应式,为了在保持其响应式的同时从 store 中提取属性要使用 storeToRefs ,如下述代码所示:
<template>
<div>{{ name }}</div>
<p>购物车清单:</p>
<p v-for="book of books" :key="book.name">
书名:{{ book.name }} 价格:{{ book.price }}
</p>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import useAppStore from './store/index'
// ⚠️⚠️ 返回的是一个 reactive 对象,不能直接解构哦,使用 pinia 提供的 storeToRefs API
const { name, books } = storeToRefs(useAppStore())
</script>
$patch
除了直接修改 store.xxx 的值,还可以通过 $patch 修改多个字段信息;下面在例子中添加购买数量、总价,并添加付款人:
<template>
<div>{{ name }}</div>
<p>购物车清单:</p>
<p v-for="book of books" :key="book.name">
书名:{{ book.name }} 价格:{{ book.price }} 数量: {{ book.count }}
<button @click="add(book)">+</button>
<button @click="minus(book)">-</button>
</p>
<button @click="batchAdd">全部加到10本</button>
<p>总价:{{ price }}</p>
<button @click="reset()">重置</button>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
import useAppStore from "./store/index";
import type { BookItem } from "./store/cart";
const store = useAppStore();
const { name, books, price } = storeToRefs(store);
const reset = () => {
store.$reset();
};
const add = (book: BookItem) => {
// 直接修改 store.book
book.count++;
};
const minus = (book: BookItem) => {
// 直接修改 store.book
book.count--;
};
const batchAdd = () => {
// 通过 $patch 方法修改 store 多个字段
store.$patch({
name: '小I',
books: [
{
name: "金瓶梅",
price: 50,
count: 10,
},
{
name: "微服务架构设计模式",
price: 139,
count: 10,
},
{
name: "数据密集型应用系统设计",
price: 128,
count: 10,
},
],
});
};
</script>
例子中添加了 “全部加到10本”和 “重置”按钮,点击前者会将全部书籍数量添加到 10 本,点击后者会重置成 0,下面是执行效果:
假如你只想将《微服务架构设计模式》的数量修改成10,通过 $patch 传对象的方法需要这么操作:
<template>
<button @click="batchAddMicroService">微服务加到10本</button>
</template>
<script>
const batchAddMicroService = () => {
store.$patch({
name: '小I',
books: [
{
name: "金瓶梅",
price: 50,
count: 0,
},
{
name: "微服务架构设计模式",
price: 139,
count: 10,
},
{
name: "数据密集型应用系统设计",
price: 128,
count: 0,
},
],
});
}
</script>
可以看到,就算你只是修改数组(集合)的第二项,还是需要将整个 books 数组传入,于是就产生了将函数作为 $patch 参数的写法:
<script>
const batchAddMicroService = () => {
store.$patch((state) => {
state.books[1].count = 10;
});
}
</script>
上述代码重写了 batchAddMicroService 方法。
$subscribe
该方法跟 vuex 的 subscribe 类似,用于监听 state 及其 mutation 动作。上述例子中我们订阅 appStore 的状态:
const store = useAppStore();
store.$subscribe((mutation, state) => {
console.log(mutation);
console.log(state);
});
Getters
Getters 就是 store 的计算属性(computed)。大部分时候,Getter 通过 state 值去做计算,这种情况下 TypeScript 能够正确的推断出类型。例如:
export default defineStore('app', {
state: () => {
const userInfoStore = useUserInfoStore()
const cartStore = useCartStore()
return {
name: userInfoStore.name,
books: cartStore.books
}
},
getters: {
price: (state) => {
return state.books.reduce((init: number, curValue: BookItem) => {
return init += curValue.price * curValue.count
}, 0)
}
}
})
我们将 state 和 getters 都改成箭头函数,这样就能在 App.vue 中正确推断出 price 的类型。
如果在 getters 中使用 this 去访问 state 的话,需要显式声明返回值才能正确标记类型,我们来试试:
export default defineStore('app', {
// ...
getters: {
price () {
return this.books.reduce((init: number, curValue: BookItem) => {
return init += curValue.price * curValue.count
}, 0)
}
}
})
我们给 price 显示声明返回类型:
export default defineStore('app', {
// ...
getters: {
price (): number {
return this.books.reduce((init: number, curValue: BookItem) => {
return init += curValue.price * curValue.count
}, 0)
}
}
})
此时又能正确地提示 price 的类型。
Getters 其他用法比如组合 Getters、在 setup 或 options api 中使用、传参等等都跟 State 类似,本节就不展开细述。
Actions
Actions 相当于组件里的 methods。双 11 买东西当然免不了折扣,商家也在折扣这环节上设计了活动,能够让顾客自己随机一个折扣比率,于是在 store 中的 actions 下定义 changeDiscountRate 方法:
export default defineStore('app', {
state: () => {
const userInfoStore = useUserInfoStore()
const cartStore = useCartStore()
const discountRate = 1
return {
name: userInfoStore.name,
books: cartStore.books,
discountRate
}
},
actions: {
changeDiscountRate () {
this.discountRate = Math.random() * this.discountRate
}
}
})
跟 Getters 一样,actions 中也通过 this 去获取整个 store。我们通过异步 actions 让修改折扣有一个延迟效果:
function getNewDiscountRate (rate: number): Promise<number> {
return new Promise ((resolve) => {
setTimeout(() => {
resolve(rate * Math.random())
}, 1000)
})
}
export default defineStore('app', {
// ...
actions: {
async changeDiscountRate () {
this.discountRate = await getNewDiscountRate(this.discountRate)
}
}
})
$onAction
当我们想统计 actions 的时间或者记录折扣点击总次数的时候,$onAction 订阅器能够很方便地实现,下面是一个官方的示例:
// App.vue
const unsubscribe = store.$onAction(
({
name,
store,
args,
after,
onError,
}) => {
const startTime = Date.now()
console.log(`Start "${name}" with params [${args.join(', ')}].`)
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
function getNewDiscountRate (rate: number): Promise<number> {
return new Promise ((resolve, reject) => {
setTimeout(() => {
// 这里通过reject结束promise
reject(rate * Math.random())
}, 1000)
})
}
export default defineStore('app', {
// ...
actions: {
async changeDiscountRate () {
try {
this.discountRate = await getNewDiscountRate(this.discountRate)
} catch (e) {
// 示例执行这部分逻辑
throw Error(e)
}
}
}
})
网友评论