VueX 会发生什么?
已发表: 2022-03-10Vuex 是 Vue 应用程序状态管理的解决方案。 下一个版本——Vuex 4——在正式发布之前正在完成最后的步骤。 此版本将带来与 Vue 3 的完全兼容性,但不会添加新功能。 虽然 Vuex 一直是一个强大的解决方案,并且是许多开发人员在 Vue 中进行状态管理的首选,但一些开发人员希望看到更多的工作流问题得到解决。 然而,就在 Vuex 4 刚刚面世之际,Kia King Ishii(Vue 核心团队成员)正在谈论他对 Vuex 5 的计划,我对所看到的内容感到非常兴奋,不得不与您分享全部。 请注意,Vuex 5 计划尚未最终确定,因此在 Vuex 5 发布之前可能会发生一些变化,但如果它最终与您在本文中看到的内容大致相似,那应该是对开发人员体验的重大改进。
随着 Vue 3 及其组合 API 的出现,人们一直在寻找手工构建的简单替代方案。 例如,您可能不需要 Vuex演示了一个相对简单但灵活且健壮的模式,用于使用组合 API 以及provide/inject
来创建共享状态存储。 然而,正如 Gabor 在他的文章中所说,这个(和其他替代方案)应该只用于较小的应用程序,因为它们缺少所有与代码无关的东西:社区支持、文档、约定、良好的 Nuxt 集成和开发人员工具。
最后一个问题一直是我最大的问题之一。 Vue devtools 浏览器扩展一直是调试和开发 Vue 应用程序的绝佳工具,而“时间旅行”失去 Vuex 检查器对于调试任何非平凡的应用程序来说都是一个相当大的损失。

值得庆幸的是,有了 Vuex 5,我们就可以吃到蛋糕了。 它将更像这些组合 API 替代方案,但保留使用官方状态管理库的所有好处。 现在让我们来看看会发生什么变化。
定义商店
在我们对 Vuex store 做任何事情之前,我们需要定义一个。 在 Vuex 4 中,存储定义如下所示:
import { createStore } from 'vuex' export const counterStore = createStore({ state: { count: 0 }, getters: { double (state) { return state.count * 2 } }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })
每个 store 有四个部分: state
存储数据, getters
为你提供计算状态, mutations
用于改变 state, actions
是从 store 外部调用以完成与 store 相关的任何事情的方法。 通常,动作不只是提交一个突变,如本例所示。 相反,它们用于执行异步任务,因为突变必须是同步的,或者它们只是实现更复杂或多步骤的功能。 动作也不能自己改变状态; 他们必须使用一个mutator。 那么 Vuex 5 是什么样的呢?
import { defineStore } from 'vuex' export const counterStore = defineStore({ name: 'counter', state() { return { count: 0 } }, getters: { double () { return this.count * 2 } }, actions: { increment () { this.count++ } } })
这里有一些变化需要注意。 首先,我们使用defineStore
而不是createStore
。 这种差异可以忽略不计,但出于语义原因,我们将在稍后讨论。 接下来,我们需要为商店提供一个我们以前不需要的name
。 过去,模块有自己的名字,但它们不是由模块本身提供的; 它们只是添加它们的父商店分配给它们的属性名称。 现在,没有模块。 相反,每个模块将是一个单独的商店并有一个名称。 这个名字被 Vuex 注册中心使用,我们稍后会谈到。
之后,我们需要让state
成为一个返回初始状态的函数,而不是仅仅将其设置为初始状态。 这类似于组件上的data
选项。 我们编写getters
的方式与我们在 Vuex 4 中所做的方式非常相似,但不是使用state
作为每个 getter 的参数,您可以使用this
来获取 state。 同样, actions
不需要担心传入的context
对象:他们可以使用this
来访问所有内容。 最后,没有mutations
。 相反,突变与actions
相结合。 Kia 指出,突变常常只是简单的设置器,使它们变得毫无意义地冗长,因此他们删除了它们。 他没有提到直接从 store 外部改变状态是否“可以”,但是我们绝对被允许和鼓励直接从一个动作改变状态,并且 Flux 模式不赞成直接改变状态。
注意:对于那些更喜欢组合 API 而不是用于创建组件的选项 API 的人,您会很高兴地了解到还有一种方法可以以类似于使用组合 API 的方式创建商店。
import { ref, computed } from 'vue' import { defineStore } from 'vuex' export const counterStore = defineStore('counter', { const count = ref(0) const double = computed(() => count.value * 2) function increment () { count.value++ } return { count, double, increment } })
如上所示,名称作为defineStore
的第一个参数传入。 其余的看起来就像组件的组合函数。 这将产生与前面使用选项 API 的示例完全相同的结果。
让商店实例化
在 Vuex 4 中,与 Vuex 3 相比,情况发生了变化,但我只关注 v4 以防止事情失控。 在 v4 中,当您调用createStore
时,您已经实例化了它。 然后,您可以通过app.use
或直接在您的应用中使用它:
import { createApp } from 'vue' import App from './App.vue' // Your root component import store from './store' // The store definition from earlier const app = createApp(App) app.use(store) app.mount('#app') // Now all your components can access it via `this.$store` // Or you can use in composition components with `useStore()` // ----------------------------------------------- // Or use directly... this is generally discouraged import store from './store' store.state.count // -> 0 store.commit('increment') store.dispatch('increment') store.getters.double // -> 4
这是 Vuex 5 比 v4 更复杂的一件事。 现在,每个应用程序都可以获得一个单独的 Vuex 实例,这确保每个应用程序可以拥有相同商店的单独实例,而无需在它们之间共享数据。 如果您想在应用程序之间共享商店实例,您可以共享一个 Vuex 实例。
import { createApp } from 'vue' import { createVuex } from 'vuex' import App from './App.vue' // Your root component const app = createApp(App) const vuex = createVuex() // create instance of Vuex app.use(vuex) // use the instance app.mount('#app')
现在您的所有组件都可以访问 Vuex 实例。 然后将它们导入到要使用它们的组件中,并使用 Vuex 实例来实例化和注册它们,而不是直接给出你的商店定义:

import { defineComponent } from 'vue' import store from './store' export default defineComponent({ name: 'App', computed: { counter () { return this.$vuex.store(store) } } })
调用$vuex.store
,在 Vuex 实例中实例化并注册 store。 从那时起,任何时候你在那个 store 上使用$vuex.store
,它都会返回已经实例化的 store 而不是再次实例化它。 您可以直接在createVuex()
创建的 Vuex 实例上调用store
方法。
现在可以通过this.counter
在该组件上访问您的商店。 如果您为组件使用组合 API,则可以使用useStore
代替this.$vuex.store
:
import { defineComponent } from 'vue' import { useStore } from 'vuex' // import useStore import store from './store' export default defineComponent({ setup () { const counter = useStore(store) return { counter } } })
将 store 直接导入组件并在那里实例化它有利有弊。 它允许您进行代码拆分并仅在需要的地方延迟加载存储,但现在它是直接依赖项,而不是由父级注入(更不用说每次要使用它时都需要导入它)。 如果您想使用依赖注入在整个应用程序中提供它,特别是如果您知道它将在应用程序的根目录中使用代码拆分无济于事,那么您可以使用provide
:
import { createApp } from 'vue' import { createVuex } from 'vuex' import App from './App.vue' import store from './store' const app = createApp(App) const vuex = createVuex() app.use(vuex) app.provide('store', store) // provide the store to all components app.mount('#app')
你可以将它注入任何你将要使用它的组件中:
import { defineComponent } from 'vue' export default defineComponent({ name: 'App', inject: ['store'] }) // Or with Composition API import { defineComponent, inject } from 'vue' export default defineComponent({ setup () { const store = inject('store') return { store } } })
我对这种额外的冗长并不感到兴奋,但它更明确、更灵活,这是我的粉丝。 这种类型的代码通常在项目开始时立即编写一次,然后就不会再打扰您了,尽管现在您需要提供每个新商店或每次要使用时都导入它,但是导入或注入代码模块是我们通常必须使用其他任何东西的方式,所以它只是让 Vuex 更符合人们已经倾向于工作的方式。
使用商店
除了喜欢使用组合 API 以与组件相同的方式定义 store 的灵活性和新方法之外,还有一件事让我比其他任何事情都更兴奋:如何使用 store。 这是在 Vuex 4 中使用 store 的样子。
store.state.count // Access State store.getters.double // Access Getters store.commit('increment') // Mutate State store.dispatch('increment') // Run Actions
State
、 getters
、 mutations
和actions
都通过不同的属性或方法以不同的方式处理。 这具有明确性的优势,我之前称赞过,但是这种明确性并没有真正为我们带来任何好处。 当你使用命名空间模块时,这个 API 只会变得更难使用。 相比之下,Vuex 5 看起来就像你通常希望的那样工作:
store.count // Access State store.double // Access Getters (transparent) store.increment() // Run actions // No Mutators
所有的东西——状态、getter 和动作——都可以直接在 store 的根目录下使用,使用起来很简单,而且省去了很多冗长,实际上消除了对选项 API 或编写使用mapState
、 mapGetters
、 mapActions
和mapMutations
的所有需要用于组合 API 的额外computed
语句或简单函数。 这只是使 Vuex 商店的外观和行为就像您自己构建的普通商店一样,但它获得了插件、调试工具、官方文档等的所有好处。
组成商店
我们今天要看的 Vuex 5 的最后一个方面是可组合性。 Vuex 5 没有可以从单个商店访问的命名空间模块。 这些模块中的每一个都将被拆分为一个完全独立的商店。 对于组件来说,这很简单:他们只需导入他们需要的任何商店,然后启动并使用它们。 但是,如果一家商店想与另一家商店互动怎么办? 在 v4 中,命名空间使整个事情复杂化,因此您需要在commit
和dispatch
调用中使用命名空间,使用rootGetters
和rootState
,然后逐步进入要从中访问 getter 和状态的命名空间。 这是它在 Vuex 5 中的工作方式:
// store/greeter.js import { defineStore } from 'vuex' export default defineStore({ name: 'greeter', state () { return { greeting: 'Hello' } } }) // store/counter.js import { defineStore } from 'vuex' import greeterStore from './greeter' // Import the store you want to interact with export default defineStore({ name: 'counter', // Then `use` the store use () { return { greeter: greeterStore } }, state () { return { count: 0 } }, getters: { greetingCount () { return `${this.greeter.greeting} ${this.count}' // access it from this.greeter } } })
使用 v5,我们导入我们希望使用的商店,然后使用use
注册它,现在它可以在整个商店中以您提供的任何属性名称访问。 如果您使用商店定义的组合 API 变体,事情会更简单:
// store/counter.js import { ref, computed } from 'vue' import { defineStore } from 'vuex' import greeterStore from './greeter' // Import the store you want to interact with export default defineStore('counter', ({use}) => { // `use` is passed in to function const greeter = use(greeterStore) // use `use` and now you have full access const count = 0 const greetingCount = computed(() => { return `${greeter.greeting} ${this.count}` // access it like any other variable }) return { count, greetingCount } })
没有更多的命名空间模块。 每个商店都是独立的,单独使用。 您可以使用use
使一个商店在另一个商店中可用以组合它们。 在这两个示例中, use
基本上与之前的vuex.store
机制相同,它们确保我们使用正确的 Vuex 实例来实例化存储。
打字稿支持
对于 TypeScript 用户来说,Vuex 5 最大的优点之一是简化使得向所有内容添加类型变得更加简单。 旧版本的 Vuex 几乎不可能实现的抽象层,现在,使用 Vuex 4,它们增加了我们使用类型的能力,但是仍然需要大量的手动工作才能获得相当数量的类型支持,而在 v5 中,您可以将您的类型内联,就像您希望和期望的那样。
结论
Vuex 5 看起来几乎完全符合我(可能还有其他许多人)所希望的那样,而且我觉得它来得还不够快。 它简化了 Vuex 的大部分内容,消除了一些相关的精神开销,并且只会在增加灵活性的地方变得更加复杂或冗长。 在下面留下评论,说明您对这些更改的看法以及您可能会做出哪些改变或另外做出哪些改变。 或者直接去源头添加一个 RFC(Request for Comments)到列表中,看看核心团队的想法。