vue.js非常容易学习和使用,任何人都可以使用该框架 构建一个简单的应用程序。即使是新手,在 Vue 文档的帮助下,也可以完成这项工作。然而,当复杂性开始发挥作用时,事情就会变得更加严重。事实是,具有共享状态的多个深度嵌套的组件可以迅速将您的应用程序变成无法维护的混乱。
复杂应用程序中的主要问题是如何在不编写意大利面条式代码或产生副作用的情况下管理组件之间的状态。在本教程中,您将学习如何使用 Vuex解决这个问题:一个用于构建复杂 Vue.js 应用程序的状态管理库。
什么是 Vuex?
Vuex 是一个状态管理库,专为构建复杂的大型 Vue.js 应用程序而调整。它为应用程序中的所有组件使用全局集中存储,利用其反应系统进行即时更新。
Vuex 存储的设计方式是不可能从任何组件更改其状态。这确保了只能以可预测的方式改变状态。因此,您的存储成为单一的事实来源:每个数据元素只存储一次并且是只读的,以防止应用程序的组件破坏其他组件访问的状态。
为什么需要 Vuex?
你可能会问:为什么我首先需要 Vuex?我不能将共享状态放在常规javascript文件中并将其导入到我的 Vue.js 应用程序中吗?
当然可以,但是与普通的全局对象相比,Vuex 存储有一些显着的优势和好处:
Vuex 商店是反应式的。一旦组件从中检索到状态,它们将在每次状态更改时响应地更新其视图。
组件不能直接改变商店的状态。更改存储状态的唯一方法是显式提交突变。这可确保每个状态更改都留下可跟踪的记录,从而使应用程序更易于调试和测试。
借助 Vuex 与Vue 的 DevTools 扩展的集成,您可以轻松地调试您的应用程序。
Vuex 商店让您鸟瞰所有事物在您的应用程序中是如何连接和影响的。
即使组件层次结构发生变化,维护和同步多个组件之间的状态也更容易。
Vuex 使直接的跨组件通信成为可能。
如果组件被销毁,Vuex 存储中的状态将保持不变。
Vuex 入门
在我们开始之前,我想澄清几件事。
首先,要学习本教程,您需要对 Vue.js 及其组件系统有很好的了解,或者至少对该框架有最少的经验。
此外,本教程的目的不是向您展示如何构建一个实际的复杂应用程序;目的是将您的注意力更多地集中在 Vuex 概念以及如何使用它们来构建复杂的应用程序上。出于这个原因,我将使用非常简单明了的示例,没有任何冗余代码。一旦你完全掌握了 Vuex 的概念,你将能够将它们应用到任何复杂程度。
最后,我将使用 ES2015 语法。如果你不熟悉它, 你可以在这里学习。
现在,让我们开始吧!
建立一个 Vuex 项目
开始使用 Vuex 的第一步是在你的机器上安装 Vue.js 和Vuex。有几种方法可以做到这一点,但我们将使用最简单的一种。只需创建一个 html 文件并添加必要的 CDN 链接:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <!-- Put the css code here --> </head> <body> <!-- Put the HTML template code here --> <script src="https://unpkg.com/vue"></script> <script src="https://unpkg.com/vuex"></script> <script> // Put the Vue code here </script> </body> </html>
我使用了一些 CSS 来使组件看起来更好,但您不必担心 CSS 代码。它只会帮助您对正在发生的事情有一个直观的了解。只需将以下内容复制并粘贴到 <head>
标签内:
<style> #app { background-color: yellow; padding: 10px; } #parent { background-color: green; width: 400px; height: 300px; position: relative; padding-left: 5px; } h1 { margin-top: 0; } .child { width: 150px; height: 150px; position:absolute; top: 60px; padding: 0 5px 5px; } .childA { background-color: red; left: 20px; } .childB { background-color: blue; left: 190px; } </style>
现在,让我们创建一些要使用的组件。在 <script>
标签内,就在结束 </body>
标签的正上方,放置以下 Vue 代码:
Vue.component('ChildB',{ template:` <div class="child childB"> <h1> Score: </h1> </div>` }) Vue.component('ChildA',{ template:` <div class="child childA"> <h1> Score: </h1> </div>` }) Vue.component('Parent',{ template:` <div id="parent"> <childA/> <childB/> <h1> Score: </h1> </div>` }) new Vue ({ el: '#app' })
在这里,我们有一个 Vue 实例、一个父组件和两个子组件。每个组件都有一个标题“ Score: ”,我们将在其中输出应用程序状态。
您需要做的最后一件事是 在打开后立即放置一个包装<div>
,然后将父组件放入其中:id="app"
<body>
<div id="app"> <parent/> </div>
准备工作现已完成,我们准备继续前进。
探索 Vuex
状态管理
在现实生活中,我们通过使用策略来组织和构建我们想要使用的内容来处理复杂性。我们将相关的东西组合在不同的部分、类别等中。这就像一个图书库,其中的书籍被分类并放在不同的部分,以便我们可以轻松找到我们要查找的内容。Vuex 将与状态相关的应用程序数据和逻辑分为四组或类别:状态、getter、突变和动作。
状态和突变是任何 Vuex 存储的基础:
state
是一个保存应用程序数据状态的对象。mutations
也是一个包含影响状态的方法的对象。
Getter 和 action 就像状态和突变的逻辑投影:
getters
包含用于抽象访问状态的方法,并在需要时执行一些预处理工作(数据计算、过滤等)。actions
是用于触发突变和执行异步代码的方法。
让我们探索下图以使事情更清楚:
在左侧,我们有一个 Vuex 商店的示例,我们将在本教程的后面创建它。在右侧,我们有一个 Vuex 工作流程图,它显示了不同的 Vuex 元素如何协同工作并相互通信。
为了改变状态,特定的 Vue 组件必须提交突变(例如 this.$store.commit('increment', 3)
),然后,这些突变会改变状态(score
变成3
)。之后,由于 Vue 的响应式系统,getter 会自动更新,并在组件的视图中呈现更新(使用 this.$store.getters.score
)。
突变不能执行异步代码,因为这将导致无法在 Vue DevTools 等调试工具中记录和跟踪更改。要使用异步逻辑,您需要将其放入操作中。在这种情况下,组件将首先在执行异步代码的地方分派动作(this.$store.dispatch('incrementScore', 3000)
),然后这些动作将提交突变,这将改变状态。
创建一个 Vuex 商店骨架
现在我们已经探索了 Vuex 的工作原理,让我们为我们的 Vuex 存储创建骨架。将以下代码放在ChildB
组件注册上方:
const store = new Vuex.Store({ state: { }, getters: { }, mutations: { }, actions: { } })
为了从每个组件提供对 Vuex 存储的全局访问,我们需要store
在 Vue 实例中添加属性:
new Vue ({ el: '#app', store // register the Vuex store globally })
this.$store
现在,我们可以使用变量从每个组件访问存储 。
到目前为止,如果您在浏览器中使用 CodePen 打开项目,您应该会看到以下结果。
状态属性
状态对象包含应用程序中的所有共享数据。当然,如果需要,每个组件也可以有自己的私有状态。
想象一下,您要构建一个游戏应用程序,并且需要一个变量来存储游戏的分数。所以你把它放在状态对象中:
state: { score: 0 }
现在,您可以直接访问该州的分数。让我们回到组件并重用存储中的数据。为了能够重用来自 store 状态的响应式数据,您应该使用计算属性。所以让我们在父组件中创建一个score()
计算属性:
computed: { score () { return this.$store.state.score } }
在父组件的模板中,放入{{ score }}
表达式:
<h1> Score: {{ score }} </h1>
现在,对两个子组件执行相同的操作。
Vuex 非常聪明,它会为我们完成所有工作,以便score
在状态发生变化时响应地更新属性。尝试更改分数的值并查看结果如何在所有三个组件中更新。
创建getter
当然,您可以this.$store.state
在组件中重用关键字,正如您在上面看到的那样。但想象以下场景ios:
在大型应用程序中,多个组件使用 访问存储的状态
this.$store.state.score
,您决定更改 的名称score
。这意味着您必须更改每个使用它的组件内的变量名称!您想使用状态的计算值。例如,假设您想在分数达到 100 分时给玩家 10 分的奖励。因此,当分数达到 100 分时,会增加 10 分奖励。这意味着每个组件都必须包含一个重用分数并将其增加 10 的函数。您将在每个组件中重复代码,这一点都不好!
幸运的是,Vuex 提供了一个有效的解决方案来处理这种情况。想象一下访问 store 的 state 并为每个 state 的 item 提供 getter 函数的集中式 getter。如果需要,这个 getter 可以对状态项应用一些计算。如果您需要更改某些状态属性的名称,您只需在一个地方更改它们,在这个 getter 中。
让我们创建一个score()
getter:
getters: { score (state){ return state.score } }
getter 接收 state
作为它的第一个参数,然后使用它来访问状态的属性。
注意:Getter 也 getters
作为第二个参数接收。您可以使用它来访问商店中的其他 getter。
在所有组件中,修改score()
计算属性以直接使用score()
getter 而不是状态的分数。
computed: { score () { return this.$store.getters.score } }
现在,如果您决定将其更改score
为result
,则只需在一个地方更新它:在score()
getter 中。在这个 CodePen 中试试吧!
创建突变
突变是改变状态的唯一允许方式。触发更改只是意味着在组件方法中提交突变。
突变几乎是一个按名称定义的事件处理函数。突变处理函数接收 astate
作为第一个参数。您也可以传递额外的第二个参数,称为 payload
for the mutation。
让我们创建一个 increment()
突变:
mutations: { increment (state, step) { state.score += step } }
突变不能直接调用!要执行突变,您应该 commit()
使用相应突变的名称和可能的附加参数调用该方法。它可能只有一个,就像 step
我们的例子一样,或者可能有多个包裹在一个对象中。
让我们increment()
通过创建一个名为 的方法来使用两个子组件中的突变changeScore()
:
methods: { changeScore (){ this.$store.commit('increment', 3); } }
我们正在提交一个突变而不是this.$store.state.score
直接更改,因为我们想显式地跟踪突变所做的更改。通过这种方式,我们使我们的应用程序逻辑更加透明、可追溯且易于推理。此外,它还可以实现诸如Vue DevTools 或Vuetron 之类的工具,这些工具可以记录所有突变、获取状态快照和执行时间旅行调试。
现在,让我们changeScore()
使用该方法。在两个子组件的每个模板中,创建一个按钮并为其添加一个点击事件监听器:
<button @click="changeScore">Change Score</button>
当你点击按钮时,状态会增加 3,这个变化会反映在所有组件中。现在我们已经有效地实现了直接的跨组件通信,这是 Vue.js 内置的“props down, events up”机制无法实现的。在我们的CodePen 示例中查看它 。
创建动作
一个动作只是一个提交突变的函数。它间接更改状态,从而允许执行异步操作。
让我们创建一个 incrementScore()
动作:
actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit('increment', 3) }, delay) } }
Actions 获取context
作为第一个参数,其中包含存储中的所有方法和属性。通常,我们只是通过使用ES2015 参数解构来提取我们需要的部分。该commit
方法是我们经常需要的方法。动作也有第二个有效载荷参数,就像突变一样。
在 ChildB
组件中,修改changeScore()
方法:
methods: { changeScore (){ this.$store.dispatch('incrementScore', 3000); } }
为了调用一个动作,我们使用 dispatch()
带有相应动作名称和附加参数的方法,就像突变一样。
现在,组件中的“更改分数” 按钮 ChildA
将使分数增加 3。 ChildB
组件中的相同按钮将执行相同的操作,但会延迟 3 秒。在第一种情况下,我们正在执行同步代码并使用突变,但在第二种情况下,我们正在执行异步代码,我们需要使用操作。在我们的 CodePen 示例中了解它是如何工作的。
Vuex 映射助手
Vuex 提供了一些有用的帮助器,它们可以简化创建状态、getter、突变和动作的过程。我们可以告诉 Vuex 为我们创建它们,而不是手动编写这些函数。让我们看看它是如何工作的。
而不是像这样编写score()
计算属性:
computed: { score () { return this.$store.state.score } }
我们只是mapState()
像这样使用助手:
computed: { ...Vuex.mapState(['score']) }
该score()
属性是为我们自动创建的。
getter、mutation 和 action 也是如此。
要创建score()
getter,我们使用 mapGetters()
helper:
computed: { ...Vuex.mapGetters(['score']) }
要创建 changeScore()
方法,我们使用这样的 mapMutations()
帮助器:
methods: { ...Vuex.mapMutations({changeScore: 'increment'}) }
当用于带有有效负载参数的突变和操作时,我们必须在定义事件处理程序的模板中传递该参数:
<button @click="changeScore(3)">Change Score</button>
如果我们想changeScore()
使用动作而不是突变,我们可以mapActions()
这样使用:
methods: { ...Vuex.mapActions({changeScore: 'incrementScore'}) }
同样,我们必须在事件处理程序中定义延迟:
<button @click="changeScore(3000)">Change Score</button>
注意:所有映射助手都返回一个对象。因此,如果我们想将它们与其他本地计算属性或方法结合使用,我们需要将它们合并到一个对象中。幸运的是,使用对象扩展运算符 ( ...
),我们可以在不使用任何实用程序的情况下做到这一点。
在我们的 CodePen 中, 您可以看到如何在实践中使用所有映射助手的示例。
使商店更加模块化
似乎复杂性问题不断阻碍我们前进。我们之前通过创建 Vuex 商店解决了这个问题,在那里我们使状态管理和组件通信变得容易。在那家商店里,我们将所有东西都放在一个地方,易于操作且易于推理。
然而,随着我们的应用程序的增长,这个易于管理的存储文件变得越来越大,因此更难维护。同样,我们需要一些策略和技术来改进应用程序结构,使其恢复到易于维护的形式。在本节中,我们将探讨几种可以帮助我们完成这项工作的技术。
使用 Vuex 模块
Vuex 允许我们将 store 对象拆分为单独的模块。每个模块都可以包含自己的状态、突变、动作、getter 和其他嵌套模块。在我们创建了必要的模块之后,我们在商店中注册它们。
让我们看看它的实际效果:
const childB = { state: { result: 3 }, getters: { result (state) { return state.result } }, mutations: { increase (state, step) { state.result += step } }, actions: { increaseResult: ({ commit }, delay) => { setTimeout(() => { commit('increase', 6) }, delay) } } } const childA = { state: { score: 0 }, getters: { score (state) { return state.score } }, mutations: { increment (state, step) { state.score += step } }, actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit('increment', 3) }, delay) } } } const store = new Vuex.Store({ modules: { scoreBoard: childA, resultBoard: childB } })
在上面的示例中,我们创建了两个模块,每个子组件一个。模块只是普通对象,我们将其注册为store 内scoreBoard
的resultBoard
对象。modules
的代码 childA
与前面示例中商店中的代码相同。在 的代码中 childB
,我们添加了一些值和名称的更改。
现在让我们调整 ChildB
组件以反映resultBoard
模块中的更改。
Vue.component('ChildB',{ template:` <div class="child childB"> <h1> Result: {{ result }} </h1> <button @click="changeResult()">Change Result</button> </div>`, computed: { result () { return this.$store.getters.result } }, methods: { changeResult () { this.$store.dispatch('increaseResult', 3000); } } })
在 ChildA
组件中,我们唯一需要修改的就是changeScore()
方法:
Vue.component('ChildA',{ template:` <div class="child childA"> <h1> Score: {{ score }} </h1> <button @click="changeScore()">Change Score</button> </div>`, computed: { score () { return this.$store.getters.score } }, methods: { changeScore () { this.$store.dispatch('incrementScore', 3000); } } })
如您所见,将 store 拆分为模块使其更加轻量级和可维护性,同时仍保持其强大的功能。查看更新的 CodePen 以查看它的实际效果。
命名空间模块
如果您希望或需要为模块中的特定属性或方法使用相同的名称,那么您应该考虑对它们进行命名空间。否则,您可能会观察到一些奇怪的副作用,例如执行所有具有相同名称的操作,或者获取错误的状态值。
要命名 Vuex 模块,只需将namespaced
属性设置为true
.
const childB = { namespaced: true, state: { score: 3 }, getters: { score (state) { return state.score } }, mutations: { increment (state, step) { state.score += step } }, actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit('increment', 6) }, delay) } } } const childA = { namespaced: true, state: { score: 0 }, getters: { score (state) { return state.score } }, mutations: { increment (state, step) { state.score += step } }, actions: { incrementScore: ({ commit }, delay) => { setTimeout(() => { commit('increment', 3) }, delay) } } }
在上面的示例中,我们使两个模块的属性和方法名称相同。现在我们可以使用以模块名称为前缀的属性或方法。例如,如果我们想使用模块中的score()
getter resultBoard
,我们可以这样输入: resultBoard/score
. 如果我们想要模块中的score()
getter scoreBoard
,那么我们可以这样输入scoreBoard/score
:
现在让我们修改我们的组件以反映我们所做的更改。
Vue.component('ChildB',{ template:` <div class="child childB"> <h1> Result: {{ result }} </h1> <button @click="changeResult()">Change Result</button> </div>`, computed: { result () { return this.$store.getters['resultBoard/score'] } }, methods: { changeResult () { this.$store.dispatch('resultBoard/incrementScore', 3000); } } }) Vue.component('ChildA',{ template:` <div class="child childA"> <h1> Score: {{ score }} </h1> <button @click="changeScore()">Change Score</button> </div>`, computed: { score () { return this.$store.getters['scoreBoard/score'] } }, methods: { changeScore () { this.$store.dispatch('scoreBoard/incrementScore', 3000); } } })
正如您在我们的CodePen 示例中看到的那样,我们现在可以使用我们想要的方法或属性并获得我们期望的结果。
将 Vuex 存储拆分为单独的文件
在上一节中,我们通过将 store 拆分为模块,在一定程度上改进了应用程序结构。我们使商店更整洁、更有条理,但所有商店代码及其模块仍然位于同一个大文件中。
因此,下一个合乎逻辑的步骤是将 Vuex 存储拆分为单独的文件。这个想法是为存储本身创建一个单独的文件,并为它的每个对象(包括模块)创建一个文件。这意味着状态、getter、突变、动作以及每个单独的模块(store.js
、 state.js
、getters.js
等)都有单独的文件。您可以在下一节的末尾看到此结构的示例。
使用 Vue 单文件组件
我们已经将 Vuex 商店尽可能地模块化。接下来我们可以对 Vue.js 组件应用相同的策略。.vue
我们可以将每个组件放在一个带有扩展名的独立文件中 。要了解其工作原理,您可以访问 Vue 单文件组件文档页面。
因此,在我们的例子中,我们将拥有三个文件: Parent.vue
、 ChildA.vue
和 ChildB.vue
.
最后,如果我们结合所有三种技术,我们将得到以下或类似的结构:
├── index.html └── src ├── main.js ├── App.vue ├── components │ ├── Parent.vue │ ├── ChildA.vue │ ├── ChildB.vue └── store ├── store.js ├── state.js ├── getters.js ├── mutations.js ├── actions.js └── modules ├── childA.js └── childB.js
在我们的教程 GitHub repo中,您可以看到具有上述结构的已完成项目。
回顾
让我们回顾一下关于 Vuex 需要记住的一些要点:
Vuex 是一个状态管理库,可以帮助我们构建复杂的大型应用程序。它为应用程序中的所有组件使用全局集中存储。为了抽象状态,我们使用 getter。Getter 与计算属性非常相似,当我们需要在运行时过滤或计算某些内容时,它是一个理想的解决方案。
Vuex store 是响应式的,组件不能直接改变 store 的状态。改变状态的唯一方法是提交突变,这是同步事务。每个突变应该只执行一个动作,必须尽可能简单,并且只负责更新一部分状态。
异步逻辑应该封装在动作中。每个动作可以提交一个或多个突变,一个突变可以由多个动作提交。动作可能很复杂,但它们从不直接改变状态。
最后,模块化是可维护性的关键。为了处理复杂性并使我们的代码模块化,我们使用“分而治之”的原则和代码拆分技术。
结论
而已!您已经了解了 Vuex 背后的主要概念,并准备开始在实践中应用它们。
为了简洁起见,我故意省略了 Vuex 的一些细节和特性,所以你需要阅读完整的 Vuex 文档 以了解有关 Vuex 及其特性集的所有信息。
- 状态管理
- 创建一个 Vuex 商店骨架
- 状态属性
- 创建getter
- 创建突变
- 创建动作
- 使用 Vuex 模块
- 命名空间模块
- 将 Vuex 存储拆分为单独的文件
- 使用 Vue 单文件组件