uni-app状态管理之vuex
一、介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
uni-app 内置了 Vuex。
每一个 Vuex 应用的核心就是 store(仓库),它包含着你的应用中大部分的状态 (state)。
状态管理有5个核心:state、getter、mutation、action、module。
1、优势与使用场景
- Vuex的状态存储是响应式的,可跟踪每一个状态变化,一旦它改变,所有关联组件都会自动更新相对应的数据。
- 共享数据,解决了非父子组件的消息传递(将数据存放在state中)。
- 统一状态管理,减少了请求次数,有些情景可以直接从内存中的state获取数据。
2、使用 Vuex 需要遵守的规则
-
应用层级的状态应该集中到单个
store
对象中。 -
提交
mutation
是更改状态的唯一方法,并且这个过程是同步的。 -
异步逻辑都应该封装到
action
里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store
文件太大,只需将 action
、mutation
和 getter
分割到单独的文件。
对于大型应用,我们会希望把 Vuex
相关代码分割到模块中。下面是项目结构示例:
├── pages
├── static
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules # 模块文件夹
├── cart.js # 购物车模块
└── products.js # 产品模块
├── App.vue
├── main.js
├── manifest.json
├── pages.json
└── uni.scss
二、uni-app中配置vuex的步骤
1、创建vuex主脚本
在 uni-app
项目根目录下,新建 store
目录,在此目录下新建 index.js
文件。在 index.js
文件配置如下:
// 页面路径:store/index.js import { createStore } from 'vuex' const store = createStore({ state:{ // 存放状态 "username":"zzz", "age": 18 } }) export default store
如果需要对store进行二次计算(加工),Vuex
允许我们在 store
中定义“getter”
(可以认为是 store
的计算属性),对 state
的加工,是派生出来的数据。 可以在多组件中共享 getter
函数,这样做还可以提高运行效率。
在 store
上注册 getter
,getter
方法接受以下参数:
- state, 如果在模块中定义则为模块的局部状态
- getters, 等同于 store.getters
import { createStore } from 'vuex' const store = createStore({ state: { // 存放状态 "username": "zzz", "age": 18 }, getters: { myFriend(state) { return `${state.username}的朋友是小明`; }, xiaomingAge(state, getters) { // state :可以访问数据 // getters:访问其他函数,等同于 store.getters return `${getters.myFriend},他年龄是:${state.age}` } } }); export default store
2、注册到app
在 main.js
中导入文件。
import App from './App' import { createSSRApp } from 'vue' import store from '@/store/index.js';
export function createApp() { const app = createSSRApp(App) app.use(store) return { app } }
3、在组件中使用
<template> <view class="content"> <text class="title">{{ name }}</text> </view> </template> <script setup lang="ts"> import { computed, onMounted, ref } from 'vue'; import store from '@/store/index.js'; const name = computed(() => store.state.username) // vue2的话可以直接使用this.$store.state,或者 mapState const myFriend = computed(() => store.getters.myFriend) // vue2的话可以直接使用this.$store.getters,或者 mapGetters const xiaomingAge = computed(() => store.getters.xiaomingAge) console.log('name: ', name.value) console.log('myFriend: ', myFriend.value) console.log('xiaomingAge: ', xiaomingAge.value) </script> <style> .content { display: flex; flex-direction: column; align-items: center; justify-content: center; } </style>
4、mutations修改store的状态
Vuex中store数据改变的唯一方法就是mutation。
通俗的理解,mutations
里面装着改变数据的方法集合,处理数据逻辑的方法全部放在 mutations
里,使数据和视图分离。
Vuex 中的 mutation
非常类似于事件:每个 mutation
都有一个事件类型 (type) 和 对应的回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state
作为第一个参数。
你不能直接调用一个 mutation 回调函数 (handler)。要唤醒一个 mutation 回调函数 (handler),你需要以相应的 type 调用 store.commit 方法。
-
mutation
可以传递一个参数
// 页面路径:store/index.js import { createStore } from 'vuex' const store = createStore({ state: { // 存放状态 "username": "zzz", "age": 18 }, mutations: { changeName(state, newName) { state.username = newName; } } }); export default store
// 页面路径:pages/index/index.vue <template> <view class="content"> <text>my name is: {{ name }}</text> <text @click="changeMyName">点击改名啦</text> </view> </template> <script setup lang="ts"> import { computed, onMounted, ref } from 'vue'; import store from '@/store/index.js'; const name = computed(() => store.state.username) function changeMyName() { store.commit('changeName', 'new name') } </script> <style> .content { display: flex; flex-direction: column; align-items: center; justify-content: center; }</style>
注意:Mutation 必须是同步函数,如果是类似下面这样异步的话:
mutations: { someMutation (state) { api.callAsyncMethod(() => { state.count++ }) } }
我们就不知道什么时候状态会发生改变,所以也就无法追踪了,这与 mutation
的设计初心相悖,所以强制规定它必须是同步函数。
一条重要的原则就是要记住** mutation 必须是同步函数**,如果有异步逻辑需要处理,则可以使用action。
5、action异步间接更改状态
action
类似于 mutation
,不同在于:
- action 提交的是
mutation
,通过mutation
来改变state
,而不是直接变更状态。 - action 可以包含任意异步操作。
action
函数接受一个与 store
实例具有相同方法和属性的 context
对象,因此你可以调用 context.commit
提交一个 mutation
,或者通过 context.state
和 context.getters
来获取 state
和 getters
。
actions
通过 store.dispatch
方法触发。
// store/index.js import { createStore } from 'vuex' const store = createStore({ state: { "age": 18 }, mutations: { setAge(state, newAge) { state.age = newAge; } }, actions: { setAgeAction({ commit }, newAge) { // 模拟异步 setTimeout(() => { commit('setAge', newAge) }, 1000) } } }); export default store
// page/index/index.vue <template> <view class="content"> <text>我的年龄: {{ age }}</text> <button @click="changeAge">更改年龄</button> </view> </template> <script setup lang="ts"> import { computed, onMounted, ref } from 'vue'; import store from '@/store/index.js'; const age = computed(() => store.state.age) function changeAge() { store.dispatch('setAgeAction', 16) } </script> <style> .content { display: flex; flex-direction: column; align-items: center; justify-content: center; } </style>
一般来说,异步的作用在于处理接口是否调用成功,例如有这样的一个场景:
我们在某APP进行购物,把购物车的商品放在state进行全局管理,如果你勾选了购物车里面的某个商品进行购买支付,当年点击给钱的时候,会去调用支付接口,等接口支付成功后才会把已经支付的这个商品移除出购物车,那么这个时候就必须要使用action进行异步处理,大概逻辑如下:
import { createStore } from 'vuex' const store = createStore({ state: { cart: { added: [] // 已经添加到购物车的商品id } }, mutations: { checkoutSuccess(state, productIds) { state.cart.added = state.cart.added.filter(item => !productIds.includes(item)); }, checkoutFailed(state, productIds) { state.cart.added = productIds; } }, actions: { pay ({ commit, state }, productIds) { // productIds: 已经勾选需要支付的商品id const savedCartItems = [...state.cart.added] // 备份当前购物车 // 购物 API 接受一个成功回调和一个失败回调 api.pay(productIds).then(res => { if (res.resultCode === '200') { // 成功则把已支付的商品清理掉 commit('checkoutSuccess', productIds) } }).catch(() => { // 失败则回退 commit('checkoutFailed', savedCartItems) }); } } }); export default store
6、Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store
对象就有可能变得相当臃肿。
为了解决以上问题,Vuex
允许我们将 store
分割成模块(module)。每个模块拥有自己的 state
、mutation
、action
、getter
、甚至是嵌套子模块——从上至下进行同样方式的分割:
1、在 store
文件夹下新建 modules
文件夹,并在下面新建 moduleA.js
和 moduleB.js
文件用来存放 vuex
的 modules
模块。
├── components # 组件文件夹
└── myButton
└── myButton.vue # myButton组件
├── pages
└── index
└── index.vue # index页面
├── static
├── store
├── index.js # 我们组装模块并导出 store 的地方
└── modules # 模块文件夹
├── moduleA.js # 模块moduleA
└── moduleB.js # 模块moduleB
├── App.vue
├── main.js
├── manifest.json
├── pages.json
└── uni.scss
2、在 main.js
文件中引入 store
。
// 页面路径:main.js import {createSSRApp} from 'vue' import store from './store' export function createApp() { const app = createSSRApp(App) app.use(store) return { app } }
3、在项目根目录下,新建 store
文件夹,并在下面新建 index.js
文件,作为模块入口,引入各子模块。
// 页面路径:store/index.js import {createStore} from 'vuex' import moduleA from '@/store/modules/moduleA' import moduleB from '@/store/modules/moduleB' export default createStore({ modules: { moduleA, moduleB } })
4、子模块 moduleA
页面内容。
// 子模块moduleA路径:store/modules/moduleA.js export default { state: { text:"我是moduleA模块下state.text的值" }, getters: { }, mutations: { }, actions: { } }
5、子模块 moduleB
页面内容。
// 子模块moduleB路径:store/modules/moduleB.js export default { state: { timestamp: 1608820295//初始时间戳 }, getters: { timeString(state) {//时间戳转换后的时间 var date = new Date(state.timestamp); var year = date.getFullYear(); var mon = date.getMonth()+1; var day = date.getDate(); var hours = date.getHours(); var minu = date.getMinutes(); var sec = date.getSeconds(); var trMon = mon<10 ? '0'+mon : mon var trDay = day<10 ? '0'+day : day return year+'-'+trMon+'-'+trDay+" "+hours+":"+minu+":"+sec; } }, mutations: { updateTime(state){//更新当前时间戳 state.timestamp = Date.now() } }, actions: { } }
6、在页面中引用组件 myButton ,并通过 mapState
读取 state
中的初始数据。
<!-- 页面路径:pages/index/index.vue --> <template> <view class="content"> <view>{{text}}</view> <view>时间戳:{{timestamp}}</view> <view>当前时间:{{timeString}}</view> <myButton></myButton> </view> </template> <script> import {mapState,mapGetters} from 'vuex' export default { computed: { ...mapState({ text: state => state.moduleA.text, timestamp: state => state.moduleB.timestamp }), ...mapGetters([ 'timeString' ]) } } </script>
7、在组件 myButton
中,通过 mutations
操作刷新当前时间。
<!-- 组件路径:components/myButton/myButton.vue --> <template> <view> <button type="default" @click="updateTime">刷新当前时间</button> </view> </template> <script> import {mapMutations} from 'vuex' export default { data() { return {} }, methods: { ...mapMutations(['updateTime']) } } </script>