什么是Pinia
官方文档:https://pinia.vuejs.org/zh/introduction.html
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。如果你熟悉组合式 API 的话,你可能会认为可以通过一行简单的 export const state = reactive({})
来共享一个全局状态。对于单页应用来说确实可以,但如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。 而如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:
Devtools 支持
- 追踪 actions、mutations 的时间线
- 在组件中展示它们所用到的 Store
- 让调试更容易的 Time travel
- 热更新
- 不必重载页面即可修改 Store
- 开发时可保持当前的 State
插件:可通过插件扩展 Pinia 功能
为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。支持服务端渲染
基础示例
// stores/counter.js
import { defineStore } from 'pinia'
// useCounterStore 就相当于vuex 中store
// 第一个参数是你的应用中 Store 的唯一 ID
export const useCounterStore = defineStore('counter', () => {
// count 表示的就是state中的数据
const count = ref(0)
// increment()方法可以看作是actions,同时支持异步的直接修改state中的数据,刨除了mutations
function increment() {
count.value++
}
return { count, increment }
})
然后你就可以在一个组件中使用该 store 了:
<script setup>
import { useCounterStore } from '@/stores/counter'
// useCounterStore是向外暴露的一个store,而counter是唯一id
const counter = useCounterStore()
counter.count++
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
</script>
<template>
<!-- 直接从 store 中访问 state -->
<div>Current Count: {{ counter.count }}</div>
</template>
那么对于vuex中之间留下的辅助函数依旧可以使用
// defineComponent 为 vue3中的新语法,就是一个计算属性
export default defineComponent({
computed: {
// 其他计算属性
// ...
// 允许访问 this.counterStore 和 this.userStore
...mapStores(useCounterStore, useUserStore)
// 允许读取 this.count 和 this.double
...mapState(useCounterStore, ['count', 'double']),
},
methods: {
// 允许读取 this.increment()
...mapActions(useCounterStore, ['increment']),
},
})
在pinia
中,去除了 modules的概念,每一个store都是一个独立的模块,并且支持组合式风格的API
安装:npm install pinia
案例演示
演示效果:根组件渲染Number
,Son1组件 Son2组件可以修改Number
使得全局都是响应式变化
步骤:
- 安装Pinia
npm install pinia
- 在main.js中注册
import { createApp } from 'vue'
// 导入 pinia
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia() // 创建Pinia实例
app.use(pinia) // 将Pinia 挂载到App上
app.mount('#app')
- 创建一个
store/xxxxx.js
表示为一个store
// 引入pinia
import { defineStore } from 'pinia'
import {ref} from 'vue'
// 定义一个 store
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,
//同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useCountStore = defineStore('countStore',()=>{
// 顶一个响应式变量
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 向外提供 这三个属性方法
return { count,increment , decrement}
})
- 在根组件使用
<template>
<div>
<h3>APP.vue 根组件 -{{ countStore.count }}</h3>
<Son1Com></Son1Com>
<Son2Com></Son2Com>
</div>
</template>
<script setup>
import Son1Com from './components/Son1Com.vue'
import Son2Com from './components/Son2Com.vue'
// 引入
import { useCountStore } from './stores/counter'
// 获取到 useCountStore 的一个对象
const countStore = useCountStore()
</script>
- 在子组件Son1 和Son2中使用
<template>
<div>
我是Son1 - {{ countStore.count }} - <button @click="countStore.increment()">+</button>
</div>
</template>
<script setup>
import { useCountStore } from '../stores/counter'
const countStore = useCountStore()
</script>
定义Store
官网:https://pinia.vuejs.org/zh/core-concepts/
在Store中支持选项式和组合式,为了迎合vue3,这里采用组合式定义,选项式与vuex中定义几乎一致
在Store中:
- ref() 就是 state 属性
- computed() 就是 getters
- function() 就是 actions
import {ref ,computed} from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
// 声明一个计算属性
const computedCount = computed(()=>{return count.value * 20})
return { count, increment , computedCount }
})
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
Action异步实现
action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!
案例引入:
编写方式:异步action函数的写法和组件中获取异步数据的写法完全一致
接口地址:http://geek.itheima.net/v1_0/channels
需求:在Pinia中获取频道列表数据,并把数据渲染APP组件的模板中
步骤:
安装axios,同时封装axios
// request.js
import axios from 'axios';
const request = axios.create({
// 在这里可以添加自定义的配置
baseURL: 'http://geek.itheima.net/v1_0/', // 你的 API 地址
timeout: 5000, // 请求超时时间(毫秒)
});
// 添加拦截器等其他配置
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么 (默认axios 会多包装一层data,需要响应拦截器中处理一下)
return response.data
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export default request;
定义api接口
import request from "../utils/request";
// 获取频道列表
export const getChannels = () => {
return request.get('/channels')
}
定义一个store/channle.js 用于声明 pinia
import { defineStore } from 'pinia'
import {ref} from 'vue'
import {getChannels} from '../api/channel'
export const useChannelStore = defineStore('channel',()=>{
// 声明 频道列表数据
const channels = ref([])
// 声明操作 频道列表的方法
const getChannelsList = async () => {
// 发起请求
const res = await getChannels()
channels.value = res.data.channels
}
return {channels ,getChannelsList}
})
在页面中调用:
<script setup>
// 引入
import {useChannelStore} from './stores/channel'
// 获取pinia
const channelStore = useChannelStore()
// 调用发起请求,getChannelsList方法调用后,会将数据封装到channels中,页面直接渲染即可
channelStore.getChannelsList()
</script>
<template>
<div>
<ul>
<li v-for="item in channelStore.channels" :key="item">
{{ item }}
</li>
</ul>
</div>
</template>
Pinia - 持久化
从上诉的案例中,都没有持久化,只要页面刷新了,所对应的store都被重置了,在实际开发场景中是不允许的,
我们要配合本地去实现pinia的持久化
以下是基本原生的持久化,又更好的策略后面再说
- 定义要给store.js 用于本地化
// utils/stores.js
export const setChannels = (obj) => {
// 将数据存入本地
localStorage.setItem('channels', JSON.stringify(obj))
}
export const getChannelsObj = () => {
// 从本地获取数据,如果为空,返回一个默认值数组
const obj = JSON.parse(localStorage.getItem('channels'))
const defaultList = []
return obj?obj:defaultList
}
- 声明一个store.js 文件定义pinia
import { defineStore } from 'pinia'
import {ref} from 'vue'
import {getChannels} from '../api/channel'
import {setChannels , getChannelsObj} from '../utils/stores'
export const useChannelStore = defineStore('channel',()=>{
// 声明 频道列表数据
const channels = ref(getChannelsObj()) // 从本地获取数据
// 声明操作 频道列表的方法
const getChannelsList = async () => {
// 发起请求
const res = await getChannels()
channels.value = res.data.channels
setChannels(res.data.channels) // 将值存入到本地
}
return {channels ,getChannelsList}
})
这样就持久化了
Pinia持久化插件
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
-
安装
npm i pinia-plugin-persistedstate
-
将插件添加到 pinia 实例上,再mian.js文件中
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
- 创建 Store 时,将 persist 选项设置为 true。
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
persist: true, // 表示持久化
},
)
更多的配置项:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/config.html#paths
该插件的默认配置如下:
使用 localStorage
进行存储
store.$id
作为 storage 默认的 key
使用 JSON.stringify/JSON.parse 进行序列化/反序列化
整个 state 默认将被持久化
如何你不想使用默认的配置,那么你可以将一个对象传递给 Store 的 persist 属性来配置持久化。
如果想自定义名称:
import { defineStore } from 'pinia'
export const useStore = defineStore('store', {
state: () => ({
someState: '你好 pinia',
}),
persist: {
key: 'my-custom-key',//这个 Store 将被持久化存储在 localStorage 中的 my-custom-key key 中。
},
})
总结:
Pinia 是 用来做什么的?
- 新一代的状态管理工具,代替vuex
Pinia中还需要mutaiton吗?
- 不需要,actions即支持同步也支持异步
Pinia 如何实现getter?
- 基于computed计算属性函数
const getChannels = computed(()=>channels.value)
Pinia产生的Store如何解构赋值数据保持响应式?
- 通过
storeToRefs()
Pinia 如何快速实现持久化
- 使用插件即可