Vue状态管理模式之Vuex和Pinia


Store 是什么?

Store (如 VuexPinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个始终存在并且每个人都可以读取和写入的组件。

它有三个概念stategetteraction,我们可以假设这些概念相当于组件中的 datacomputedmethods

Vue 官方推荐了两种状态管理模式,一个是Vuex,一个是Pinia。虽说一个是为Vue2专门开发,一个是为Vue3开发,但实际上,不管是Vue2,还是Vue3,VuexPinia都可以使用。只不过官方目前更推荐使用Pinia

PiniaVue 核心团队维护,对 Vue 2Vue 3 都可用。

现有用户可能对 Vuex 更熟悉,它是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia

事实上,Pinia 最初正是为了探索 Vuex 的下一个版本而开发的,因此整合了核心团队关于 Vuex 5 的许多想法。最终,我们意识到 Pinia 已经实现了我们想要在 Vuex 5 中提供的大部分内容,因此决定将其作为新的官方推荐

注:并非所有的应用都需要访问全局状态。

一、Vuex介绍

1.Vuex的基本使用

Vuexvue中用来进行全局状态管理的,说的简单一点就是类似于一个全局变量,可以实现任意组件关系之间的数据读取操作,它有四个对象:stategettersmutationsactions

  • state:state类似于vue对象中的data,用来定义变量
  • getters:getters类似于vue中的computed计算属性,当state中的数据需要经过加工后再使用时,可以使用getters加工
  • mutations:mutations是定义方法,整个Vuex只有mutations中可以修改数据
  • actions:actions也是定义方法,但是跟mutations不一样的就是它可以支持异步,如果我们的数据是后端提供过来的,需要缓存,我们可以在actions请求数据,在actions中拿到数据调用mutations,由mutations去修改存储数据。

vuex数据流流程图,如下

1-1.Vue2里使用vuex

main.js引入

import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({
    store,
    render: h => h(App)
})
.$mount('#app')

store文件夹index.js

//推荐使用模块化
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        name:"hello world"
    },

    getters: {...},

    mutations: {...},

    actions:{...},

    modules: {...},
})

// 当前页面直接使用(不推荐直接使用,推荐使用mapState解构)
computed: {
    name() {
        return this.$store.state.name;
    }//hello world
}

1-2.Vue3里使用vuex

main.js引入

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App)
.use(store)
.mount('#app')

store文件夹index.js

import { createStore } from 'vuex'

export default createStore({
    state: {
        name:"hello world"
    },

    getters: {...},

    mutations: {...},

    actions:{...},

    modules: {...},
})

// 当前页面直接使用
import { useStore} from 'vuex'
const store = useStore();
const name=store.state.name //hello world

Action扩展:

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation。 当你了解了Modules ,你就知道 context 对象为什么不是 store 实例本身了。

store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise
以下来自官方案例:

actions: {
    actionA ({ commit }) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                commit('someMutation')
                resolve()
            }, 1000)
        })
    }
}

现在你可以:

store.dispatch('actionA').then(() => {
    // ...
})

在另外一个 action 中也可以:

actions: {
    actionB ({ dispatch, commit }) {
        return dispatch('actionA').then(() => {
            commit('someOtherMutation')
        })
    }
}

最后,如果我们利用 async / await,我们可以如下组合 action

// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
    async actionA ({ commit }) {
        commit('gotData', await getData())
    },
    async actionB ({ dispatch, commit }) {
        await dispatch('actionA') // 等待 actionA 完成
        commit('gotOtherData', await getOtherData())
    }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

小结:

组件中读取Vuex中的数据:$store.state.sum

组件中修改Vuex中的数据:$store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)

2.模块化(modules)的使用

2-1.单页面使用模块化

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
// 在同一个index页面写多个模块(modules)
const countAbout = {
    namespaced:true,//开启命名空间
    // state 写法一(推荐使用)
    state: () => ({ 
        sum:1,
        count:1
    }),
    // state 写法二(推荐使用)
    state() {
        return {
            sum:1,
            count:1
        };
    },
    //写法二 另一种写法
    state:()=> {
        return {
            sum:1,
            count:1
        };
    },
    // state 写法三(同一页面写多个模块,不推荐使用,参考data(){})
    state:{
        sum:1,
        count:1
    },
    mutations: { ... },
    actions: { ... },
    getters: {
        bigSum(state){
            return state.sum * 10
        }
    }
}

const personAbout = {
    namespaced:true,//开启命名空间
    state() {
        return {
            list:[],
            allPerson:1
        };
    },
    mutations: { 
        addCount(state,n){
            state.allPerson +=n
        },
    },
    actions: { 
        addPerson({state,commit},n){
            commit("addCount", n);
        },
    },
}

const store = new Vuex.Store({
    modules: {
        countAbout,
        personAbout
    }
})

export default store

2-2.多页面模块化

多页面模块( modules )引入,结构目录如下:

+  |- store                            // store文件夹
+    |- modules                        // 模块化文件夹
+       |- index1.js                   // 对应的功能1文件
+       |- index2.js                   // 对应的功能2文件
+    |- index.js                       // vuex 入口js文件
// 单独页面,此处可以不需要使用方法来定义state
const state = {
    sum:"",
    count:1
};

const getters = {
    getterCount(state){
        return state.count+1;
    }
};

const mutations = {
    maddCount(state,n){
        state.count +=n
    },
    msumCount(state,m){
        state.sum+=m
    }
};

const actions = {
    aaddCount({state,commit,rootState},n){
        commit("maddCount", n);
    },
    //或是 参数n 非必需,如下
    async asumCount(context){
        const res = await getAllData();//getAllData为后台接口
        context.commit("msumCount", res);
    }
};

export default {
    namespaced:true,//开启命名空间
    state,
    getters,
    mutations,
    actions
};

//store文件夹下的index.js 页面
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 自动引入store/modules
let req = require.context("./modules/", false, /\.js$/);
let myModules = req
    .keys()
    .map((path) => {
        return {
            [path.slice(2, -3)]: req(path).default //'./index1.js'.replace(/^\.\/(.*)\.\w+$/, '$1') => index1
        };
    })
    .reduce((pre, cur) => ({ ...pre, ...cur }), {});

export default new Vuex.Store({
    modules: {
        ...myModules
    },
});

3.四个map辅助函数的使用

目标:简化获取store数据

3-1.mapState方法

映射state中的数据为计算属性

// 官方定义
computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

// 实际使用(推荐,避免当前页面使用computed,多次定义)
computed: {
    //直接使用
    count() {
        return this.$store.state.count
    } 

    // 借助mapState生成计算属性,有2种方法
    //1. 对象写法
    ...mapState({sum:'sum',count:'count'}),

    //2. 数组写法(推荐使用)
    ...mapState(['sum','count']),
},

3-2.mapGetters方法

映射getters中的数据为计算属性

computed: {
    //直接使用
    count() {
        return this.$store.getters.getterCount //getterCount为store里 getters的方法,但这里不需要加括号
    } 

    // 借助mapGetters生成计算属性,有2种方法
    //1. 对象写法
    ...mapGetters({getterCount:'getterCount'}),

    //2. 数组写法
    ...mapGetters(['getterCount'])
},

3-3.mapActions方法

Vuex中的actions的函数映射到组件的methods中,生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

methods:{
    //正常使用
    actionsAdd(n){
        this.$store.dispatch("aaddCount",n) // aaddCount 是定义在store里 actions下的一个方法 
    }

    //使用 mapActions 方法
    // 1.注册 actions 的方法,有2种注册方式
    // 1.1 对象形式
    // 冒号左侧的 aadd 是事件函数的名称,可以自定义
    // 冒号右侧的 aaddCount 是actions的名称
    ...mapActions({aadd:'aaddCount',asum:'asumCount'})

    //  1.2 数组形式
    //将 `this.aaddCount(n)` 映射为 `this.$store.dispatch('aaddCount', n)`
    ...mapActions(['aaddCount','asumCount'])

    // 2.使用
    addAll(n){//当前页面的方法
        this.aadd(n)
        //或
        this.aaddCount(n)
    }
}

3-4.mapMutations方法

Vuex中的mutations的函数映射到组件的methods中,生成与mutations对应的方法,即:包含$store.commit(xxx)的函数

methods:{
    //正常使用
    mutationsAdd(n){
        this.$store.commit("maddCount",n) // maddCount 是定义在store里 mutations下的一个方法 
    }

    //使用 mapMutations 方法
    // 1.注册 mutations 的方法,有2种注册方式

    // 1.1 对象形式
    // 冒号左侧的 addCount 是事件函数的名称,可以自定义
    // 冒号右侧的 maddCount 是mutation的名称
    ...mapMutations({addCount:'maddCount',sumCount:'msumCount'}),

    // 1.2 数组形式
    // 将 `this.maddCount()` 映射为 `this.$store.commit('maddCount')`
    ...mapMutations(['maddCount','msumCount']),

    // 2.使用
    sumAll(n){//当前页面的方法
        this.addCount(n)
        //或
        this.msumCount(n)
    }
}

备注:mapActionsmapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象。

以上都是在Vue2中使用map方法的案例,Vue3官方文档并没有提供给我们一个好的处理方式,所以需要我们自己想办法解决。那么Vue3中如何使用辅助函数map呢?

3-5.Vue3使用辅助函数map

注:非 steup写法,直接参考Vue2写法,这里主要讲在 steup里使用map函数

import { computed, defineComponent, onMounted } from 'vue';
import { useStore, mapState, mapGetters, mapActions, mapMutations} from 'vuex'

const store = useStore();

// 解构state
const storeStateFns = mapState(["name", "age", "sex"]);//多个state

const storeState = {};
Object.keys(storeStateFns).forEach(key => {
    //需要注意的是这里需要帮函数显示绑定this,因为在setup中是没有绑定this的
    const fn = storeStateFns[key].bind({ $store: store });
    storeState[key] = computed(fn);
});

// 解构getters
const getterSumCount = computed(
    mapGetters(['sumCount']).sumCount.bind({ $store: store })
);

// 解构mutations
const mutationSumAll = mapMutations(['sumAll']).sumAll.bind({
    $store: store
});

// 解构actions
const actionSumAdd = mapActions(['sumAdd']).sumAdd.bind({
    $store: store
});

4.命名空间

官方描述: 默认情况下,模块内部的 actionmutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 actionmutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。

如果你希望使用全局 stategetter,``rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

modules: {
    foo: {
        namespaced: true,

        getters: {
            // 在这个模块的 getter 中,`getters` 被局部化了
            // 你可以使用 getter 的第四个参数来调用 `rootGetters`
            someGetter (state, getters, rootState, rootGetters) {
                getters.someOtherGetter // -> 'foo/someOtherGetter'
                rootGetters.someOtherGetter // -> 'someOtherGetter'
                rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
            },
            someOtherGetter: state => { ... }
        },

        actions: {
            // 在这个模块中, dispatch 和 commit 也被局部化了
            // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
            someAction ({ dispatch, commit, getters, rootGetters }) {
                getters.someGetter // -> 'foo/someGetter'
                rootGetters.someGetter // -> 'someGetter'
                rootGetters['bar/someGetter'] // -> 'bar/someGetter'

                dispatch('someOtherAction') // -> 'foo/someOtherAction'
                dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

                commit('someMutation') // -> 'foo/someMutation'
                commit('someMutation', null, { root: true }) // -> 'someMutation'
            },
            someOtherAction (ctx, payload) { ... }
        }
    }
}

你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
    computed: {
        // 在 `some/nested/module` 中查找
        ...mapState({
            a: state => state.a,
            b: state => state.b
        })
    },
    methods: {
        // 在 `some/nested/module` 中查找
        ...mapActions(['foo','bar'])
    }
}

开启命名空间后,组件中读取state数据

//方式一:自己直接读取
this.$store.state.personAbout.list
//方式二:借助mapState读取:
...mapState('countAbout',['sum','count']),
...mapState({
    lists:state=>state.personAbout.list
})

开启命名空间后,组件中读取getters数据

//方式一:自己直接读取
this.$store.getters['personAbout/bigSum']
//方式二:借助mapGetters读取:
...mapGetters('countAbout',['bigSum'])
...mapGetters('countAbout',{bigsum:'bigSum'})

5.开启命名空间后,组件中调用dispatch

//方式一:自己直接dispatch
this.$store.dispatch('personAbout/addPerson',person)
//方式二:借助mapActions:
...mapActions('personAbout',{addPerson:'addPerson'})
...mapActions('personAbout',['addPerson'])

6.开启命名空间后,组件中调用commit

//方式一:自己直接commit
this.$store.commit('personAbout/addCount',person)
//方式二:借助mapMutations:
...mapMutations('personAbout',{addCount:'addCount'}),
...mapMutations('personAbout',['addCount']),

小结

1. // 先从vuex中结解构出四个方法 
import {mapState, mapMutations, mapGetters, mapActions} from 'vuex'
2. // 在computed计算属性中映射state数据和getters计算属性
computed: {
    ...mapState('模块名', ['name', 'age'])
    ...mapGetters('模块名', ['getName'])
}
3. // 在methods方法中映射mutations和actions方法
methods: {
    ...mapMutations('模块名', ['方法名1','方法名2'])
    ...mapActions('模块名', ['方法名1','方法名2'])
}
4. //这些数据和方法都可以通过this来调用和获取

5.Vuex解决了浏览器存储什么问题

  1. Vuex可以监听数据的变化。当Vuex数值发生变化时,其他组件处可以响应式地监听到该数据的变化,当数据改变时,项目中引用到该数据(并且正在监听的)的地方都会发生改变
  2. 可以存储任意形式的数据。浏览器存储中,数据只能以字符串的形式传入,对于不是string格式的数据需要采用JSON.parse()JSON.stringify()去读写
  3. 可以进行模块化存储。使用moudle模块化开发,可以对存储数据进行归类,避免存储内容过于臃肿
    没有数据存储大小限制。Vuex是存储在内存中的,而storage存储在本地中,有一定的存储大小限制(cookie 4K,localStorage、sessionStorage 5M)存储内容过多会消耗内存空间,导致页面变卡。

二、Pinia介绍

相比于 VuexPinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。

1.Pinia的基本使用

Vuex 相比,Pinia中只有 stategettersactions,没有了mutationsmodulesPinia不必以嵌套(通过modules引入)的方式引入模块,因为它的每个store便是一个模块,如storeA,storeB… 。

main.js引入

//vue版本为Vue3,vue2中推荐使用vuex
import { createApp } from "vue";
import App from "./App.vue";
import {createPinia} from 'pinia'
const pinia = createPinia()
createApp(App)
.use(pinia)
.mount("#app");

store文件夹index.js

import { defineStore } from "pinia";
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。是必须传入的, Pinia 将用它来连接 store 和 devtools
export const useCounterStore = defineStore("counter", {
    //为了完整类型推理,推荐使用箭头函数
    state: () => {
        return {
            //所有这些属性都将自动推断出它们的类型
            name: "hello pinia",
            count:1
        };
    },
    // 也可以这样定义
    // state: () => ({ count: 0 })
    getters: { ... },
    actions: { 
        increment() {
            this.count++
        },
    },
});

// 当前页面直接使用
import { useCounterStore } from '@/store/index.js'
const counter = useCounterStore()
console.log(counter.name); //hello pinia
counter.count++

为实现更多高级用法,你甚至可以使用一个函数(与组件 setup() 类似)来定义一个 Store

export const useCounterStore = defineStore('counter', () => {
    const count = ref(0)
    function increment() {
        count.value++
    }

    return { count, increment }
})

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

相比于VuexPinia是可以直接修改状态的,并且调试工具能够记录到每一次state的变化。

import { useCounterStore } from '@/store/index.js'
const counter = useCounterStore()
console.log(counter.name); //hello pinia

//直接修改数据
counter.name = 'hello world'
console.log(counter.name); //hello world

2.使用 Store

<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>

请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value,就像 setup 中的 props 一样,如果你写了,但不能解构它:

<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store 
name // 将始终是 "Eduardo" 
doubleCount // 将始终是 0 
setTimeout(() => {
  store.increment()
}, 1000)

// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>

为了从 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>

目前响应式失去的几个情况:

1、解构 props 对象,因为它会失去响应式

2、直接赋值reactive响应式对象

3、vuex/pinia中组合API赋值

详情查看:Vue3 解构赋值失去响应式引发的思考

3.$patch方法的使用

Pinia使用$patch方法。不仅支持修改单个state状态,还可以修改多个state中的值。同时$patch还可以使用函数的方式进行修改状态。

import { useCounterStore } from '@/store/index.js'
const counter = useCounterStore()
console.log(counter.name); //hello pinia
console.log(counter.count); //1

// 使用 $patch 修改状态
counter.$patch({
    count:storeA.count + 1
})

// 或者使用 action 代替
counter.increment()

console.log(counter.count);//2

// 使用 函数的方式 进行修改状态
counter.$patch((state) => {
    state.name = 'hello world'
    state.count = 2
})

4.map辅助函数的使用

我们可以使用 mapStores()mapState()mapActions()来定义或是修改state

官方描述:如果你还不熟悉 setup() 函数和组合式 API,别担心,Pinia 也提供了一组类似 Vuex 的 映射 state 的辅助函数 或是 通过映射辅助函数来使用 Pinia。你可以用和之前一样的方式来定义 Store,然后通过 mapStores()mapState()mapActions() 访问:

const useCounterStore = defineStore('counter', {
    state: () => ({ 
        count: 0 
    }),
    getters: {
        double: (state) => state.count * 2,
    },
    actions: {
        increment() {
            this.count++
        },
    },
})

const useUserStore = defineStore('user', {
  // ...
})

import { mapState } from 'pinia'
export default {
    computed: {
        // 其他计算属性
        // ...
        // 允许访问 this.counterStore 和 this.userStore
        ...mapStores(useCounterStore, useUserStore)
        // 允许读取 this.count 和 this.double
        ...mapState(useCounterStore, ['count', 'double']),
    },
    methods: {
        // 允许读取 this.increment()
        ...mapActions(useCounterStore, ['increment']),
    },
}

getters 里的变量可以通过 mapState直接获取

5.重置 state

使用选项式 API 时,你可以通过调用 store$reset() 方法将 state 重置为初始值。

// 页面使用
const store = useStore()

store.$reset()

$reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。

在 Setup Stores 中,您需要创建自己的 $reset() 方法:

export const useCounterStore = defineStore('counter', () => {
    const count = ref(0)

    function $reset() {
        count.value = 0
    }

    return { count, $reset }
})

三、 Vuex与Pinia的差异性

1.state 参数写法

Pinia 推荐使用 完整类型推断的箭头函数,Vuex里state的写法,参考上述内容

state: () => {
    return {
      // 所有这些属性都将自动推断其类型
        counter: 0,
        name: 'Eduardo',
        isAdmin: true,
    }
}

2.getters参数变更

Pinia getters没有了部分参数,如getters, rootState, rootGetters

3.actions可以处理同步和异步

actions里可以处理同步和异步,同时去掉了commit,可以在页面里直接使用counter.$patch((state) => {})替代actions修改状态,并且是能够记录每一次state的变化。

4.mutations和modules移除

具体差异性参考如下:

// Vuex module in the 'auth/user' namespace
import { Module } from 'vuex'
import { api } from '@/api'
import { RootState } from '@/types' // if using a Vuex type definition

interface State {
    firstName: string
    lastName: string
    userId: number | null
}

const storeModule: Module<State, RootState> = {
    namespaced: true,
    state: {
        firstName: '',
        lastName: '',
        userId: null
    },
    getters: {
        firstName: (state) => state.firstName,
        fullName: (state) => `${state.firstName} ${state.lastName}`,
        loggedIn: (state) => state.userId !== null,
        // combine with some state from other modules
        fullUserDetails: (state, getters, rootState, rootGetters) => {
            return {
                ...state,
                fullName: getters.fullName,
                // read the state from another module named `auth`
                ...rootState.auth.preferences,
                // read a getter from a namespaced module called `email` nested under `auth`
                ...rootGetters['auth/email'].details
            }
        }
    },
    actions: {
        async loadUser ({ state, commit }, id: number) {
            if (state.userId !== null) throw new Error('Already logged in')
            const res = await api.user.load(id)
            commit('updateUser', res)
        }
    },
    mutations: {
        updateUser (state, payload) {
            state.firstName = payload.firstName
            state.lastName = payload.lastName
            state.userId = payload.userId
        },
        clearUser (state) {
            state.firstName = ''
            state.lastName = ''
            state.userId = null
        }
    }
}

export default storeModule
// Pinia Store
import { defineStore } from 'pinia'
import { useAuthPreferencesStore } from './auth-preferences'
import { useAuthEmailStore } from './auth-email'
import vuexStore from '@/store' // for gradual conversion, see fullUserDetails

interface State {
    firstName: string
    lastName: string
    userId: number | null
}

export const useAuthUserStore = defineStore('auth/user', {
    // convert to a function
    state: (): State => ({
        firstName: '',
        lastName: '',
        userId: null
    }),
    getters: {
        // firstName getter removed, no longer needed
        fullName: (state) => `${state.firstName} ${state.lastName}`,
        loggedIn: (state) => state.userId !== null,
        // must define return type because of using `this`
        fullUserDetails (state): FullUserDetails {
            // import from other stores
            const authPreferencesStore = useAuthPreferencesStore()
            const authEmailStore = useAuthEmailStore()
            return {
                ...state,
                // other getters now on `this`
                fullName: this.fullName,
                ...authPreferencesStore.$state,
                ...authEmailStore.details
            }

            // alternative if other modules are still in Vuex
            // return {
            //   ...state,
            //   fullName: this.fullName,
            //   ...vuexStore.state.auth.preferences,
            //   ...vuexStore.getters['auth/email'].details
            // }
        }
    },
    actions: {
        // no context as first argument, use `this` instead
        async loadUser (id: number) {
            if (this.userId !== null) throw new Error('Already logged in')
            const res = await api.user.load(id)
            this.updateUser(res)
        },
        // mutations can now become actions, instead of `state` as first argument use `this`
        updateUser (payload) {
            this.firstName = payload.firstName
            this.lastName = payload.lastName
            this.userId = payload.userId
        },
        // easily reset state using `$reset`
        clearUser () {
            this.$reset()
        }
    }
})

组件内部使用

// Vuex
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'

export default defineComponent({
    setup () {
        const store = useStore()

        const firstName = computed(() => store.state.auth.user.firstName)
        const fullName = computed(() => store.getters['auth/user/firstName'])

        return {
            firstName,
            fullName
        }
    }
})
// Pinia
import { defineComponent, computed } from 'vue'
import { useAuthUserStore } from '@/stores/auth-user'

export default defineComponent({
    setup () {
        const authUserStore = useAuthUserStore()

        const firstName = computed(() => authUserStore.firstName)
        const fullName = computed(() => authUserStore.fullName)

        return {
            // you can also access the whole store in your component by returning it
            authUserStore,
            firstName,
            fullName
        }
    }
})

组件外部使用

// Vuex
import vuexStore from '@/store'

router.beforeEach((to, from, next) => {
    if (vuexStore.getters['auth/user/loggedIn']) next()
    else next('/login')
})
// Pinia
import { useAuthUserStore } from '@/stores/auth-user'

router.beforeEach((to, from, next) => {
    // Must be used within the function!
    const authUserStore = useAuthUserStore()
    if (authUserStore.loggedIn) next()
    else next('/login')
})

参考来源:


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录