点亮你的 Vue 技术栈(六):Vuex 初体验「上手指南」

632 阅读7分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

🥰了解 Vuex!

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension

🔮简单来说,Vuex 提供了一种集中管理数据的方式。

🤔为什么是 Vuex?

抛开 Vuex 不谈,实现组件间的数据共享的方式其实不少,在之前的文章有讲过组件之间数据共享方式:props$emiteventBus...

为什么还要"多此一举"地开发 Vuex 呢?

想象一下,如果你的项目里有很多页面,页面间存在层层嵌套关系,且它们都共享同一个状态,那么当来自不同视图的行为需要变更同一个状态时,我们常常需要采用父子组件直接引用或通过事件变更,而这些方式都非常脆弱,通常会导致无法维护的代码。

于是,我们想把组件之间的共享状态抽取为全局状态,不论组件身在何处,都可以直接获取。

这时候,Vuex 就诞生了!

Vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中 vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。

🎴建议:小项目不用 vuex,这会额外增加许多页面,比起不使用 vuex 多了许多内容;而大项目建议使用 vuex 对数据进行管理。

🚀快速上手

安装

npm install vuex --save

配置

安装后要配置 vuex,在 src 目录下新建 store 文件夹,然后紧接着创建 index.js 文件。

每一个 Vuex 的核心就是 store (仓库),store 基本上就是一个容器,包含着应用中大部分的状态 state;且 Vuex 中的状态 state 是响应式存储的。

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    name: '掘了~',
    books: [
      { id: 1, name: 'Go' },
      { id: 2, name: 'C++' },
      { id: 3, name: 'Python' },
    ]
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

export default store

stategettersmutationsactionsmodules 这都是些什么?别急,下文会逐个讲解。

和 vue-router 一样,还需要将 store 对象挂载到 Vue 示例上,修改 main.js

import Vue from 'vue'
import App from './App.vue'
// 引入 src/store/index.js 中导出的 store 对象
import store from './store'

Vue.config.productionTip = false

new Vue({
  // 将store对象挂载到vue示例上
  store,
  render: h => h(App)
}).$mount('#app')

使用

通过调用 this.$store.state.XXXX 可以直接获取到 store 仓库中的状态。

<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>{{ books }}</h2>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$store.state.name)
  },
  computed: {
    name() {
      return this.$store.state.name
    },
    books(){
      return this.$store.state.books
    },
  },
}
</script>

启动项目,演示下效果:

🎥先行预告:我们没有办法直接修改 store 中的状态 state,只能通过显示提交 (commit) mutation 改变状态,这样使得我们方便追踪每一个状态的变化。

OK,快速上手 Vuex 就暂告一段落(仅仅为了感受下 Vuex 的便捷),接下来直入主题谈谈 Vuex 的四大核心吧!

👨‍💻核心概念

Vuex 官网上的这张图几乎囊括了这部分所要讲的核心。

现在先混个眼熟,等看完这部分的内容后再重新回看该图,相信你会对 Vuex 有新的认识并且留下更深刻的印象。

State

相信看到这,你已经能简单使用 Vuex 来共享数据了,而现在所要讲的 State 就是用来存储共享状态的。

其实在上文『快速上手』这一部分已经演示过如何存储获取 state,这里简单回顾一下:

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

Vue.use(Vuex)

export default new Vuex.Store({
  // 定义共享状态
  state: {
    name: '掘了~',
  },
})

⭐使用 this.$store.state.XXXX 即可获取仓库中的状态。

<script>
export default {
  mounted() {
    console.log(this.$store.state.name)
  },
}
</script>

官方建议

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在 computed 计算属性中返回某个状态。

所以,上述代码可替换为:

<script>
export default {
  computed: {
    mounted() {
      console.log(this.name)
  	},
    name() {
      return this.$store.state.name
    }
  },
}
</script>

mapState

你是否会感觉每次写 this.$store.state.XXXX 都很冗长且厌烦?官方也提供了它的「解决方案」,简化了使用方式:

<script>
// ES6解构赋值: 从vuex中导入mapState
import { mapState } from 'vuex'

export default {
  computed: {
    // ... : 展开运算符
    // 数组形式
    ...mapState(['name'])
  },
}
</script>

之后便可像访问计算属性 this.name 直接获取共享状态。

⭐甚至可以为该状态取别名,如下:

<template>
  <div>
    <h2>{{ aliasName }}</h2>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  // 为 name 状态取别名为 aliasName
  computed: {
    // 对象形式
    ...mapState({ aliasName: 'name' }),
  },
}
</script>

Getters

🙄想象这么一个场景:store 中的 name 已经在各页面渲染了,然后产品经理提了个新需求,在所有 name 前加上 "Hello"。

👩‍💻行嘛,于是你在每个 this.$store.state.name 前逐一加上 "Hello",就在你花了一小时改好后,产品经理又说不行,还是改成 "Welcome" 比较洋气。

😡你: xxx(此处省略 1W 字)

显然这种方式应付不了需求的变更「可维护较差」。追根溯源,于是你就会想:有没有一种办法能直接加工/过滤 store 中的 name 呢?

这时,官方提供的 Getters 就闪亮登场了!

基本用法

在 store 对象中定义 getters 属性,以此来获取加工后的 name

⭐注:参数 stategetters 中方法的必填参数(没有 state 参数你无法获取共享状态)

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '掘了~',
  },
  getters: {
    modifiedName(state) {
      return 'Hello, ' + state.name
    }
  },
})

组件中通过 this.$store.getters.XXXX 调用:

<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>{{ infoName }}</h2>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['name']),
    infoName() {
      return this.$store.getters.modifiedName
    },
  },
}
</script>

来看看演示效果:

😎那可不可以由客户自定义这部分信息呢?当然可以,你只需要让 getters 中返回值之前再嵌套一个 (带参数) 函数即可,代码如下:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '掘了~',
  },
  getters: {
    modifiedName(state) {
      // 嵌套返回一个函数
      return (diyInfo) => {
        return `${diyInfo}, ${state.name}`
      }
    }
  },
})

使用时参入要展示的信息即可。

<template>
  <div>
    <h2>{{ infoName }}</h2>
  </div>
</template>

<script>
export default {
  data() {
    return {
      welcomeInfo: 'Welcome'
    }
  },
  computed: {
    infoName() {
      return this.$store.getters.modifiedName(this.welcomeInfo)
    },
  },
}
</script>

看到效果后,产品经理表示很满意!

mapGetters

同样,一直写 this.$store.getters.XXXX 想必也不舒服,官方提供了 mapGetters 辅助函数将 storegetters 中的属性映射到计算属性中!

⭐注:以 modifiedName(state) { return 'hello,' + state.name} 为例!

<template>
  <div>
    <h2>{{ name }}</h2>
    <h2>{{ modifiedName }}</h2>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  computed: {
    ...mapState(['name']),
    ...mapGetters(['modifiedName'])
  },
}
</script>

同样,还可以如 mapState 一样,用 mapGetters 为其定义别名,用法同上。

computed: {
  // aliasModified 是 modifiedName 的别名
  ...mapGetters({ aliasModified: 'modifiedName' })
}

😁了解以上内容后,我们便学会了 2 种读取值的操作:"原生读 state" 和 "修饰读 getters",接下来学习如何修改值

Mutations

上文曾经提及修改 state 的方法,不知你是否还记得?相信你已经回头看了一眼,但还是不懂不能直接修改是怎么一回事,往下看:

// 错误的修改方式
this.$store.state.name = '掘掘子!'

🚫以上是一种错误的修改值的方式!因为 Vuex 允许随意地获取值,但不允许随意修改仓库中的值。

看着没错呀!为什么说是错误的修改方式?

👩‍💻想象这么一个场景:假设你在掘金阅读他人的文章,你可以引用他的内容 (即 state),但如果你发现他的文章中出现错误的地方,你能直接修改他的文章吗?显然不行!你会在他的文章评论区提醒 (commit) 他,然后他收到通知后,由他自己修改文章中的错误。

同理,Vuex 也是如此,于是,Mutations 来了!并且提交 commit mutation 就是官方提供修改值的唯一方式

虽然看起来似乎有点繁琐,但它能集中监控数据的变化!

基本用法

来吧,展示!

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '掘了~',
  },
  getters: {
  },
  mutations: {
    // 官方建议 payload 为一个对象!
    alterName(state, payload) {
      state.name = payload.name
    }
  },
})

🌈其中 state 为默认参数,调用时无需占位,而 payload第一个需要传入的形参,同时官方建议该 payload 为一个对象!当然,如果不需要参数则无需编写 payload

然后来看看组件如何提交/修改状态:

关键代码: this.$store.commit('xxx')

<script>
export default {
  methods: {
    changeName() {
      console.log(`Old: ${this.$store.state.name}`);
      // commit-mutation: 调用时也传递一个对象
      this.$store.commit('alterName', { name: '掘掘子!' })
      console.log(`New: ${this.$store.state.name}`);
    },
  }
}
</script>

以上实现的是带有参数的 Mutations 方法,也可实现不带参数的。

演示效果:

mapMutations

😏同上,你如果不想一直用 this.$store.commit('方法名', 参数) 这种写法,可以采用 mapMutations 替代。

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'

export default {
  methods: {
    // 将 alterName 映射为 methods 中的方法, 可直接调用!
    ...mapMutations(['alterName'])
  }
}
</script>

...mapMutations 直接将 alterName 映射为 methods 中的方法,可直接调用:

<button @click="alterName({name: '掘掘子~'})">修改姓名</button>

你也可以给 mutations 中的方法取别名:

methods: {
  // changeName 是 alterName 的别名, 可直接调用: @click=changeName()
  ...mapMutations({ changeName: 'alterName' })
}

Mutation 必须是同步函数

🎈注意:Mutations 中的 mutation 只能存放同步操作,即 mutation 只能是同步函数

mutation混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?

所以在 Vuex 中,mutation 都是同步事务

this.$store.commit('alterName')
// 任何由 "alterName" 导致的状态变更都应该在此刻完成。

⭐为了处理异步操作,让我们来看一看 Actions

Actions

⭐Actions 类似 Mutations,不同的是 Actions 可以包含异步操作,其本质就是提交 Mutations,而不是直接变更状态。

基本用法

使用 setTimeout 模拟异步任务,延时 1s 提交 mutation 以修改 name

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '掘了~',
  },
  getters: {
  },
  mutations: {
    // 同步操作
    alterName(state) {
      state.name = '掘掘子~'
    }
  },
  actions: {
    altering(context) {
      // 异步定时操作
      setTimeout(() => {
        context.commit('alterName')
      }, 1000)
    }
  }
})

组件使用 this.$store.dispatch('xxx') 分发 Actions:

<template>
  <div>
    <button @click="alter">修改姓名</button>
  </div>
</template>

<script>
export default {
  methods: {
    alter() {
      // dispatch altering()
      this.$store.dispatch('altering')
    }
  }
}
</script>

触发效果:

🌅看懂了吧,Actions 本质还是提交 Mutations,异步操作直接在 Actions 中消化

如果你想模仿 Mutations 传递参数,也可以在 Actions 中添加形参,如下:

mutations: {
  alterName(state, payload) {
    state.name = payload.name
  }
},
actions: {
  altering(context, payload) {
    setTimeout(() => {
      context.commit('alterName', { name: payload.name })
    }, 1000)
  }
}

调用一下叭

this.$store.dispatch('altering', { name: '掘掘子~' })

官方建议

actions 中,可以单独将 commit 解构出来,方便后续操作。

actions: {
  // 解构 commit
  altering({ commit }, payload) {
    setTimeout(() => {
      commit('alterName', { name: payload.name })
    }, 1000)
  }
}

mapActions

如果厌倦了 this.$store.dispatch('xxx') 形式的写法,可以通过解构 mapActions 将相应的方法置于 methods 中。

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  methods: {
    // 调用: @click="altering({name: '掘掘子'})
    ...mapActions(['altering']),
  },
  computed: {
  },
}
</script>

同理,你也可以给它取个别名:

methods: {
  // 调用: @click="alter({name: '掘掘子'})
  ...mapActions({ alter: 'altering'}),
}

组合 Action

Action 通常是异步的,那么我们怎么知道 Action 什么时候结束呢?

更重要的是,我们怎么才能组合 Action 以实现更加复杂的异步流程呢?

🥏这部分内容详见官方:组合 Action

Modules

Vuex 使用单一状态树,用一个对象就包含了全部状态 state。至此它便作为一个“唯一数据源”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 stategettersmutationsactions、甚至是嵌套子模块。

⭐熟悉 StateGettersMutationsActions 之后,Modules 大同小异,这部分会加快节奏!

接下来直入正题:如何实现模块化。

import Vue from 'vue'
import Vuex from 'vuex'
// 可单独将cart和good分别抽离成一个js文件, 也可都同放在store/index.js下
// import cart from './cart.js'
// import good from './good.js'

Vue.use(Vuex)

// account模块
const account = {
  // 开启命名空间: 方便 ...mapXXX 的使用
  namespaced: true,
  state: {
    who: 'xxx'
  },
  getters: {
    isAdmin(state) { ... }	// => getters['account/isAdmin']
  },
  mutations: {
    login(state, payload) { ... }	// => commit('account/login')
  },
  actions: {
    login(context, payload) { ... }	// => dispatch('account/login')
  }
}

// cart模块: 同上
const cart = {
  // 开启命名空间: 方便 ...mapXXX 的使用
  namespaced: true,
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  }
}

export default new Vuex.Store({
  // index.js 中的原生store也可存放!
  state: { ... },
  getters: { ... },
  mutations: { ... },
  actions: { ... },
  // 注册各个模块
  modules: {
    account,
    cart
  }
})

👑一般我们都会添加 namespaced: true 以开启命名空间,这样模块被注册后,它的所有 gettersmutationsactions 都会根据模块注册的名字调整命名。

🧩补充:以下内容皆以开启命名空间account 模块为例,其它举一反三

读取 state

// 方式一: state
this.$store.state.account.who
// 方式二: 通过mapState读取
...mapState('account', ['who'])
...mapState('account', { whoIs: 'who' })	// 取别名

修饰 getter

// 方式一: getters
this.$store.getters['account/isAdmin']
// 方式二: 通过mapGetters读取
...mapGetters('account', ['isAdmin'])
...mapGetters('account', { whetherAdmin: 'isAdmin' })	// 取别名

修改 mutation

// 方式一: commit
this.$store.commit('account/login', payload)
// 方式二: 通过mapMutations同步修改
...mapMutations('account', ['login'])
...mapMutations('account', { isLogin: 'login'})		// 取别名

异步 action

// 方式一: dispatch
this.$store.dispatch('account/login', payload)
// 方式二: 通过mapActions异步操作
...mapActions('account', ['login'])
...mapActions('account', { isLogin: 'login'})		// 取别名

OK,以上就是有关 Modules 的内容了,相信熟悉 State、Getters、Mutations、Actions 基础后,这部分内容应该可以一笔带过。

🌈学到了什么?

通篇看完后,相信你对 Vuex 已经有一个全局认知了。

你应该也不会陌生 Vuex 的安装、配置,也知道如何从 state 读取值,加工 state 中的数据 (getters),修改状态 (mutations),在遇到异步操作时还会使用 actions 来触发 mutations,甚至你还会分割模块以提高项目的可维护性。

// 巩固一下
export default {
  methods: {
    // mapMutations
    ...mapMutations(['alterName']),
    ...mapMutations({ changeName: 'alterName' }),
    // mapActions
    ...mapActions(['altering']),
    ...mapActions({ alter: 'altering' }),
  },
  computed: {
    // mapState
    ...mapState(['name']),
    ...mapState({ aliasName: 'name' }),
    // mapGetters
    ...mapGetters(['modifiedName']),
    ...mapGetters({ aliasModified: 'modifiedName' }),
  },
}

如果还有疑惑的地方,可以多看几遍文章;学会了,就赶紧到项目中实践起来叭!

🖊结语

🔎因为笔者也是当天学的 Vuex,所以本文如有不妥之处,还望斧正。

🌈如果看完这篇文章还有疑问,或想深入了解 Vuex,推荐一些文章/视频供大家学习:

👑 / END / 成功,只比未成功,多坚持了一次!