学习和使用Vue.js非常容易,任何人都可以使用该框架构建一个简单的应用程序。 即使是新手,也可以借助Vue的文档来完成这项工作。 但是,当复杂性发挥作用时,情况会变得更加严重。 事实是,多个具有共享状态的深层嵌套组件可以快速将您的应用程序变成无法维护的混乱。
复杂应用程序中的主要问题是如何在不编写意粉代码或不产生副作用的情况下管理组件之间的状态。 在本教程中,您将学习如何使用Vuex解决问题, 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: ”,我们将在其中输出应用程序状态。
您需要做的最后一件事是在打开<body>
之后<div>
一个带有id="app"
的包装<div>
<body>
,然后将父组件放入其中:
<div id="app">
<parent/>
</div>
现在准备工作已经完成,我们已经准备好继续。
探索Vuex
国家管理
在现实生活中,我们通过使用策略来组织和构造我们要使用的内容来应对复杂性。 我们将相关内容分为不同的部分,类别等。这就像一个图书库,在其中将书籍分类并放在不同的部分中,以便我们可以轻松找到所需的内容。 Vuex将与状态相关的应用程序数据和逻辑分为四个组或类别:状态,获取器,突变和动作。
状态和突变是任何Vuex商店的基础:
-
state
是保存应用程序数据state
的对象。 -
mutations
也是包含影响状态的方法的对象。
吸气剂和动作就像状态和突变的逻辑预测:
-
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存储的全局访问,我们需要在Vue实例中添加store
属性:
new Vue ({
el: '#app',
store // register the Vuex store globally
})
现在,我们可以使用this.$store
变量从每个组件访问this.$store
。
到目前为止,如果在浏览器中使用CodePen打开项目 ,则应看到以下结果。
状态属性
状态对象包含应用程序中的所有共享数据。 当然,如果需要,每个组件也可以具有自己的私有状态。
假设您要构建一个游戏应用程序,并且需要一个变量来存储游戏的得分。 因此,您将其放在状态对象中:
state: {
score: 0
}
现在,您可以直接访问该州的分数。 让我们回到组件,并重复使用商店中的数据。 为了能够重用商店状态中的反应性数据,应使用计算属性。 因此,让我们在父组件中创建一个score()
计算属性:
computed: {
score () {
return this.$store.state.score
}
}
在父组件的模板中,放入{{ score }}
表达式:
<h1> Score: {{ score }} </h1>
现在,对两个子组件执行相同的操作。
Vuex非常聪明,只要状态发生变化,Vuex就会为我们完成所有工作,以被动方式更新score
属性。 尝试更改分数的值,并查看结果在所有三个组件中如何更新。
创建吸气剂
如上面所见,当然可以在组件内部重用this.$store.state
关键字是this.$store.state
。 但是,请设想以下情形:
- 在大型应用程序中,多个组件使用
this.$store.state.score
访问商店的状态,您决定更改score
的名称。 这意味着您必须在使用它的每个组件中更改变量的名称! - 您要使用状态的计算值。 例如,假设您想在分数达到100分时给予玩家10分的奖励。 因此,当分数达到100分时,会增加10分的奖励。 这意味着每个组件都必须包含一个函数,该函数可以重用得分并将其递增10。您将在每个组件中都有重复的代码,这根本不好!
幸运的是,Vuex提供了可解决此类情况的有效解决方案。 想象一下访问商店状态并为每个州商品提供吸气功能的集中式吸气剂。 如果需要,此获取器可以对状态项进行一些计算。 而且,如果您需要更改某些州的属性的名称,则只需在此吸气器中的一个位置进行更改。
让我们创建一个score()
:
getters: {
score (state){
return state.score
}
}
获取器将state
作为其第一个参数,然后使用它来访问状态的属性。
注意:吸气剂也将getters
作为第二个参数。 您可以使用它来访问商店中的其他获取器。
在所有组件中,修改score()
计算的属性以使用score()
代替直接使用状态的得分。
computed: {
score () {
return this.$store.getters.score
}
}
现在,如果您决定将score
更改为result
,则只需要在一个地方更新它:在score()
getter中。 在此CodePen中试用!
创建变异
突变是更改状态的唯一允许方式。 触发更改仅意味着在组件方法中进行更改。
突变几乎是由名称定义的事件处理函数。 变异处理程序函数将state
作为第一个参数。 您也可以传递另一个第二个参数,该参数称为突变的payload
。
让我们创建一个increment()
突变:
mutations: {
increment (state, step) {
state.score += step
}
}
突变不能直接调用! 要执行变异,应使用相应变异的名称和可能的其他参数调用commit()
方法。 它可能只是一个,就像我们的step
一样,或者可能有多个包裹在一个对象中。
通过创建一个名为changeScore()
的方法,让我们在两个子组件中使用increment()
突变:
methods: {
changeScore (){
this.$store.commit('increment', 3);
}
}
我们提交的是突变,而不是直接更改this.$store.state.score
,因为我们要显式跟踪突变所做的更改。 这样,我们使我们的应用程序逻辑更加透明,可跟踪且易于推理。 另外,它使实现诸如Vue DevTools或Vuetron之类的工具成为可能,该工具可以记录所有变异,获取状态快照以及执行时间旅行调试。
现在,让我们使用changeScore()
方法。 在两个子组件的每个模板中,创建一个按钮,然后向其添加click事件监听器:
<button @click="changeScore">Change Score</button>
当您单击按钮时,状态将增加3,并且此更改将反映在所有组件中。 现在,我们已经有效地实现了直接的跨组件通信,这是使用Vue.js内置的“关闭道具,增加事件”机制无法实现的。 在我们的CodePen示例中进行检查。
创建动作
动作只是提交突变的功能。 它间接更改状态,从而允许执行异步操作。
让我们创建一个incrementScore()
操作:
actions: {
incrementScore: ({ commit }, delay) => {
setTimeout(() => {
commit('increment', 3)
}, delay)
}
}
动作将context
作为第一个参数,其中包含商店中的所有方法和属性。 通常,我们只是使用ES2015参数解构来提取所需的部分。 commit
方法是我们经常需要的一种方法。 动作也获得第二个有效负载参数,就像变异一样。
在ChildB
组件中,修改changeScore()
方法:
methods: {
changeScore (){
this.$store.dispatch('incrementScore', 3000);
}
}
要调用一个动作,我们使用带有相应动作名称和其他参数的dispatch()
方法,就像对变量一样。
现在, ChildA
组件中的Change Score按钮将分数增加ChildB
组件中的相同按钮将执行相同的操作,但要延迟3秒。 在第一种情况下,我们正在执行同步代码,并且使用了变异,但是在第二种情况下,我们正在执行异步代码,而我们需要使用一个动作。 在我们的CodePen示例中查看其工作方式。
Vuex映射助手
Vuex提供了一些有用的帮助程序,可以简化状态,吸气剂,突变和动作的创建过程。 无需手动编写这些功能,我们可以告诉Vuex为我们创建它们。 让我们看看它是如何工作的。
而不是像这样编写score()
计算属性:
computed: {
score () {
return this.$store.state.score
}
}
我们只需要像这样使用mapState()
帮助器:
computed: {
...Vuex.mapState(['score'])
}
并为我们自动创建了score()
属性。
吸气剂,突变和作用也是如此。
要创建score()
,我们使用mapGetters()
帮助器:
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允许我们将存储对象拆分为单独的模块。 每个模块可以包含其自己的状态,变异,动作,获取器和其他嵌套模块。 创建必要的模块后,我们将其注册到商店中。
让我们看看它的作用:
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
}
})
在上面的示例中,我们创建了两个模块,每个子组件一个。 这些模块只是普通对象,我们在商店内部的modules
对象中注册了scoreBoard
和resultBoard
。 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);
}
}
})
如您所见,将存储拆分为模块可以使其更加轻巧和可维护,同时仍保持其强大的功能。 签出更新后的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)
}
}
}
在上面的示例中,我们使两个模块的属性和方法名称相同。 现在,我们可以使用以模块名称为前缀的属性或方法。 例如,如果我们要使用resultBoard
模块中的score()
吸气剂,则resultBoard
这样输入: resultBoard/score
。 如果我们想要来自scoreBoard
模块的score()
吸气剂,则可以这样输入: 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存储拆分为单独的文件
在上一节中,我们通过将存储分为模块在某种程度上改进了应用程序结构。 我们使商店更整洁,更有条理,但所有商店代码及其模块仍位于同一大文件中。
因此,下一步的逻辑步骤是将Vuex存储拆分为单独的文件。 想法是为商店本身有一个单独的文件,为商店的每个对象(包括模块)有一个文件。 这意味着对于状态,获取器,变异,动作以及每个单独的模块( 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与计算属性非常相似,当我们需要在运行时过滤或计算某些内容时,它是理想的解决方案。
Vuex存储是反应性的,组件无法直接更改存储的状态。 改变状态的唯一方法是提交突变,这是同步事务。 每个变异仅应执行一个动作,必须尽可能简单,并且仅负责更新状态。
异步逻辑应封装在动作中。 每个动作可以执行一个或多个突变,并且一个突变可以通过多个动作进行。 动作可能很复杂,但它们永远不会直接更改状态。
最后,模块化是可维护性的关键。 为了处理复杂性并使我们的代码模块化,我们使用“分而治之”原理和代码拆分技术。
结论
而已! 您已经了解Vuex背后的主要概念,并准备开始在实践中应用它们。
为了简洁起见,我特意省略了Vuex的一些细节和功能,因此您需要阅读完整的Vuex文档以了解有关Vuex及其功能集的所有信息。