Vuex 是一个库,我们可以和 Vue.js 一起使用它来管理应用中的不同状态。 如果您正在构建一个不需要在组件之间进行太多数据交换的小型应用,那么最好不要使用这个库。 然而,随着应用的增长,复杂性也随之增长。 应用中将有几个组件,最明显的是,您将需要在一个组件与另一个组件之间交换数据,或者在多个组件之间共享相同的数据。 这时 Vuex 来帮忙了。
Vue.js 还提供了一个emit
方法来在不同的组件之间传递数据,我们在前面的章节中使用过。 随着应用的增长,您可能还希望在更新数据时跨多个组件更新数据。
因此,Vuex 提供了一个集中的地方来存储应用中的所有数据。 每当数据发生变化时,这组新的数据就会存储在这个集中的地方。 此外,所有想要使用该数据的组件都将从存储中获取。 这意味着我们有一个单一的源来存储所有的数据,并且我们构建的所有组件都能够访问该数据。
让我们先了解一下 Vuex 的一些术语:
- State:这是一个包含数据的对象。 Vuex 使用单一的状态树,这意味着它是一个包含应用的所有数据片段的单一对象。
- getter:用于从状态树中获取数据。
- 突变:它们是改变状态树中的数据的方法。
- 动作:它们是执行突变的功能。
我们将在本章逐一讨论。
在传统的多网页应用中,当我们构建一个网页应用并通过导航到浏览器打开一个网站时,它请求 web 服务器获取该页面并将其提供给浏览器。 当我们点击同一个网站上的一个按钮时,它再次请求 web 服务器获取另一个页面并再次提供它。 这个过程发生在我们在网站上的每一次互动中。 所以,基本上,每一次互动都会重新加载网站,这会消耗大量的时间。
下面是一个示例图,解释了多页面应用是如何工作的:
当从浏览器发送请求时,请求被发送到服务器。 然后服务器返回 HTML 内容并提供一个全新的页面。
Multi Page Applications(MPA)也可以提供几个好处。 这不是选择 MPA 或单页应用(SPA)的问题,而是取决于您的应用的内容。 如果你的应用包含大量的用户交互,你应该使用 SPA; 但是,如果应用的唯一目的是为用户提供内容,则可以使用 MPA。 我们将在本章的后面探索更多关于 spa 和 MPAs 的内容。
与传统的 MPAs 相反,spa 是专门为基于 web 的应用设计的。 当您第一次在浏览器中加载网站时,SPA 将获取所有数据。 一旦获取了所有的数据,就不需要获取更多的数据了。 当任何其他交互完成后,这些数据将通过互联网获取,而无需向服务器发送请求,也无需重新加载页面。 这意味着 spa 比传统的 spa 快得多。 但是,由于 spa 在第一次加载时立即获取所有内容,所以第一个页面的加载时间可能会比较慢。 一些集成了 SPA 的应用是 Gmail、Facebook、GitHub、Trello 等。 通过将内容放在单个页面上,而不是让用户等待他们想要的信息,spa 都是为了让用户体验更好。
下面是 spa 工作原理的示例图:
该网站有所有的内容,它需要在第一页加载。 当用户点击某个东西时,它只是获取该特定区域的信息,并只刷新网页的这一部分。
SPA 和 MPA 有不同的用途。 根据您的需要,您可能希望使用其中一种。 在启动应用之前,确保清楚要构建的应用的类型。
如果您想使您的应用具有 SEO 友好性,那么 MPAs 是最好的方法。 谷歌可以通过搜索您在每个页面上分配的关键字来抓取应用的不同页面,这在 SPA 中是不可能的,因为它只有一个页面。
使用 MPAs 有几个缺点:
- MPA 的开发工作要比 SPA 大得多,因为前端和后端是紧密耦合的。
- MPAs 的前端和后端紧密耦合,这使得在前端和后端开发人员之间分离工作变得更加困难。
水疗中心提供了很多好处:
- 减少服务器响应时间e: spa 获取网站第一次加载所需的所有数据。 使用这样的应用,服务器不需要重新加载网站上的资源。 如果需要获取新数据,它只从服务器获取更新的信息片段,这与多页面应用不同,显著减少了服务器的响应时间。
- 更好的用户交互:服务器响应时间的减少最终改善了用户体验。 每一次交互,用户都会得到一个更快呈现的页面,这意味着满意的客户。
- 更改 UI 的灵活性:spa 没有耦合的前端和后端。 这意味着我们可以更改前端并完全重写它,而不必担心会破坏服务器端的任何东西。
- 数据缓存:spa 将数据缓存到本地存储。 它第一次只发出一个请求并保存数据。 这使得该应用即使在网络中断的情况下也可以使用。
使用水疗中心也有一些缺点:
- spa 对 SEO 不友好。 由于所有操作都在单个页面上完成,所以可爬行性非常低。
- 您不能与他人共享特定的信息片段,因为只有一个到页面的链接。
- 与 MPAs 相比,spa 的安全性问题要大得多。
Vuex 是一个状态管理库,专门设计用于使用 Vue.js 构建的应用。 这是对 Vuex 的集中国家管理。
我们在介绍中对这些核心概念略知一二。 现在,让我们深入了解这些概念的更多细节:
前面的图是一个简单的图,它解释了 Vuex 是如何工作的。 最初,一切都储存在一种状态中,这是真理的单一来源。 每个视图组件都从这个状态获取数据。 每当需要改变某些东西时,动作就会对数据进行突变,并将其存储回状态:
当我们在浏览器中打开应用时,将加载所有的 Vue 组件。 当我们点击一个按钮从一个组件中获取某些信息时,该组件分派一个对数据执行突变的操作。 成功完成突变后,更新状态对象并使用新的值。 然后,我们可以使用组件的新状态并在浏览器中显示它。
我们将开始一个新的应用来学习 Vuex 的基础知识。 让我们开始吧。
让我们首先创建一个新的应用:
$ vue init webpack vuex-tutorial
前面的代码片段将询问您一些关于应用设置的问题。 你可以选择你想保留什么。 我将采用以下配置:
安装完成后,导航到项目目录:
$ cd vuex-tutorial
接下来要做的是运行以下命令:
$ npm install
然后,运行以下命令:
$ npm run dev
上述命令将启动您的服务器,并在localhost:8080
中打开一个端口。
下一步是安装vuex
。 运行以下命令:
$ npm install --save vuex
现在,让我们创建一个store
文件夹来管理应用中的vuex
。
在src
目录下,创建一个store
文件夹和store.js
文件。 然后在store.js
文件中添加如下内容:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
在前面的代码块中,行Vue.use(Vuex)
导入 Vuex 库。 否则,我们将无法使用任何vuex
功能。 现在,让我们构建一个存储对象。
在同一个store.js
文件中,添加以下代码行:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
count: 0
}
export const store = new Vuex.Store({
state
})
在前面的代码中,我们将名为count
的变量的默认状态设置为0
,并通过存储导出 Vuex 状态。
现在我们需要修改src/main.js
:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import { store } from './store/store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
components: { App },
template: '<App/>'
})
前面的代码导入了我们刚刚创建的存储文件,我们可以在我们的 vue 组件中访问这个变量。
让我们继续创建一个将获取该存储数据的组件。 当我们用 Vue 创建一个新应用时,就会创建一个默认组件。 如果我们查看src/components
目录,我们会发现一个名为HelloWorld.vue
的文件。 让我们使用相同的组件HelloWorld.vue
,或者创建一个新组件。 让我们修改这个文件以访问我们在状态中定义的count
。
在src/components/HelloWorld.vue
中添加以下代码:
<template>
<div class="hello">
<h1>{{ $store.state.count }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
下面是最终的文件夹结构:
上面的截图应该打印HelloWorld.vue
组件中 count 的默认值。 如果你导航到http://localhost:8080/#/
,你应该会看到以下截图:
在前面的截图中,我们直接使用$
操作符访问商店中的 count 变量,这不是首选的方法。 我们已经学习了使用状态的基本原理。 现在,访问变量的正确方法是使用 getter。
getter
是一个用于从存储区访问对象的函数。 让我们创建一个getter
方法来获取存储中的计数。
在store.js
中添加以下代码:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
count: 0
}
const getters = {
fetchCount: state => state.count
}
export const store = new Vuex.Store({
state,
getters
})
在前面的代码中,我们添加了一个名为fetchCount
的方法,该方法返回count
的当前值。 现在,要在我们的 vue 组件HelloWorld.vue
中访问它,我们需要用以下代码更新内容:
<template>
<div class="hello">
<h1>The count is: {{ fetchCount }}</h1>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'HelloWorld',
computed: mapGetters([
'fetchCount'
])
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
我们必须从 Vuex 导入一个名为mapGetters
的模块,该模块用于导入我们在store.js
中作为getter
方法创建的fetchCount
方法。 现在,通过重新加载浏览器来检查数字; 这也应该打印计数为0
:
让我们接着看mutations
。 mutations
是对存储状态进行修改的方法。 我们将像定义getters
一样定义mutations
。
在store.js
中,添加以下行:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
count: 0
}
const getters = {
fetchCount: state => state.count
}
const mutations = {
increment: state => state.count++,
decrement: state => state.count--
}
export const store = new Vuex.Store({
state,
getters,
mutations
})
我们在前面的代码中添加了两个不同的mutation
函数。 increment
方法将计数增加 1,而decrement
方法将计数减少 1。 这就是我们引入行动的地方。
动作是分发突变函数的方法。 动作执行mutations
。 因为actions
是异步的,mutations
是同步的,所以使用actions
来改变状态总是一个好的做法。 现在,就像getters
和mutations
一样,让我们也定义actions
。 在同一个文件中,即store.js
,添加以下代码行:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
count: 0
}
const getters = {
fetchCount: state => state.count
}
const mutations = {
increment: state => state.count++,
decrement: state => state.count--
}
const actions = {
increment: ({ commit }) => commit('increment'),
decrement: ({ commit }) => commit('decrement')
}
export const store = new Vuex.Store({
state,
getters,
mutations,
actions
})
在前面的代码中,我们添加了两个不同的递增和递减函数。 由于这些方法提交了mutations
,我们将需要传递一个参数以使commit
方法可用。
现在我们需要使用前面定义的actions
,并在我们的 vue 组件中HelloWorld.vue
中使用它们:
<template>
<div class="hello">
<h1>The count is: {{ fetchCount }}</h1>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'HelloWorld',
computed: mapGetters([
'fetchCount'
]),
methods: mapActions([
'increment',
'decrement'
])
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
为了调用这些操作,我们创建两个按钮。 在HelloWorld.vue
中,我们添加以下代码行:
<template>
<div class="hello">
<h1>The count is: {{ fetchCount }}</h1>
<button class="btn btn-primary" @click="increment">Increase</button>
<button class="btn btn-primary" @click="decrement">Decrease</button>
</div>
</template>
...
前面的代码行添加了两个按钮,单击该按钮时,将调用一个方法来增加或减少计数。 让我们也为 CSS 导入 Bootstrap。 在index.html
中,添加以下代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>vuex-tutorial</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
就是这样。 现在,如果你重新加载浏览器,你应该能够看到以下结果:
当你点击相关按钮时,计数应该增加或减少。 这为您提供了如何在应用中实现 Vuex 的基本概念。
我们介绍了 vuex 的基础知识,以及它在应用中的工作方式和核心概念。 我们介绍了如何创建存储和突变,以及如何使用操作分派它们,还讨论了如何使用 getter 从存储中获取信息。
在前面的章节中,我们为电影列表页面构建了一个应用。 我们将对 Vuex 使用相同的应用。 我们将采取以下行动:
- 我们将定义一个存储所有电影的商店
- 当添加一个新电影时,我们将自动将其显示到电影列表页面,而无需重新加载该页面
让我们打开应用并运行前端和后端服务器:
$ cd movie_rating_app
$ npm run build
$ nodemon server.js
另外,使用以下命令运行mongo
服务器:
$ mongod
电影列表页面应该如下所示:
让我们从安装vuex
开始:
$ npm install --save vuex
检查您的package.json
文件; vuex
应该列在依赖项上:
...
"vue-router": "^3.0.1",
"vue-swal": "0.0.6",
"vue-template-compiler": "^2.5.14",
"vuetify": "^0.17.6",
"vuex": "^3.0.1"
},
...
现在,让我们创建一个文件,在这个文件中,我们可以放置我们将在后面定义的所有getters
、mutations
和actions
。
我们在src
目录下创建一个名为store
的文件夹,在store
目录下创建一个名为store.js
的新文件,并在其中添加以下代码行:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
})
就像我们在前面的示例应用中所做的那样,让我们添加一个state
变量来存储电影列表页面的应用的当前状态。
在store.js
中,添加以下代码行:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
movies: []
},
})
这意味着应用的初始状态将有一个空的电影列表。
现在,我们需要将这个store
导入main.js
,以便在整个组件中都可以访问它。 在src/main.js
中添加以下代码行:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import Vuetify from 'vuetify';
import VueSwal from 'vue-swal';
import App from './App';
import router from './router';
import { store } from './store/store';
Vue.use(BootstrapVue);
Vue.use(Vuetify);
Vue.use(VueSwal);
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>',
});
现在,我们需要在浏览器中打开位置http://localhost:8081/
时获取影片。 以下是我们将要做的:
- 修改
Home.vue
以调用获取影片的动作 - 创建一个将获取所有电影的动作
- 创建一个突变来将获取的影片存储在电影商店中
- 创建一个 getter 方法从要显示在主页上的状态获取影片
让我们通过修改我们的Home.vue
组件开始本节。 用以下代码行更新文件的script
部分:
<script>
export default {
name: 'Movies',
computed: {
movies() {
return this.$store.getters.fetchMovies;
}
},
mounted() {
this.$store.dispatch("fetchMovies");
},
};
</script>
在前面的代码中,在mounted()
方法中,我们分派了一个名为fetchMovies
的操作,我们将在操作中定义该操作。
当影片成功获取后,我们将使用computed
方法,该方法将被映射到movies
变量,我们将在模板中使用该变量:
<template>
<v-layout row wrap>
<v-flex xs4 v-for="movie in movies" :key="movie._id">
<v-card>
<v-card-title primary-title>
...
让我们继续往store.js
文件中添加一个操作:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
movies: []
},
actions: {
fetchMovies: (context, payload) => {
axios({
method: 'get',
url: '/movies',
})
.then((response) => {
context.commit("MOVIES", response.data.movies);
})
.catch(() => {
});
}
}
})
在前面的代码中,我们已经从组件中移动了axios
部分。 当我们得到一个成功的响应时,我们将提交一个名为MOVIES
的突变,该突变随后改变状态中的movies
的值。
让我们继续添加一个突变。 在store.js
中,将内容替换为以下代码:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
movies: []
},
mutations: {
MOVIES: (state, payload) => {
state.movies = payload;
}
},
actions: {
fetchMovies: (context, payload) => {
axios({
method: 'get',
url: '/movies',
})
.then((response) => {
context.commit("MOVIES", response.data.movies);
})
.catch(() => {
});
}
}
})
前面的mutations
改变应用影片的状态。
我们现在有了action
和mutation
。 现在,最后一部分是添加一个getter
方法,该方法从状态中获取movies
的值。
让我们在我们创建的store.js
中添加getter
方法来管理应用的状态:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
movies: []
},
getters: {
fetchMovies: state => state.movies,
},
mutations: {
MOVIES: (state, payload) => {
state.movies = payload;
}
},
actions: {
fetchMovies: (context, payload) => {
axios({
method: 'get',
url: '/movies',
})
.then((response) => {
context.commit("MOVIES", response.data.movies);
})
.catch(() => {
});
}
}
})
就是这样。 当我们导航到http://localhost:8081/movies/add
时,我们应该有一个功能性的 Vuex 实现,它将电影获取到主页。
在向应用添加影片时,让我们继续实现商店。 我们将遵循前面所做的相同过程:
- 修改
AddMovie.vue
来调用创建影片的动作 - 创建一个调用 POST API 来创建电影的
action
- 在
movies
存储区中创建mutation
到store
添加的新影片
将AddMovie.vue
中的script
内容替换为以下代码:
<script>
export default {
data: () => ({
movie: null,
valid: true,
name: '',
description: '',
genre: '',
release_year: '',
nameRules: [
v => !!v || 'Movie name is required',
],
genreRules: [
v => !!v || 'Movie genre year is required',
v => (v && v.length <= 80) || 'Genre must be less than equal to
80 characters.',
],
releaseRules: [
v => !!v || 'Movie release year is required',
],
select: null,
years: [
'2018',
'2017',
'2016',
'2015',
],
}),
methods: {
submit() {
if (this.$refs.form.validate()) {
const movie = {
name: this.name,
description: this.description,
release_year: this.release_year,
genre: this.genre,
}
this.$store.dispatch("addMovie", movie);
this.$refs.form.reset();
this.$router.push({ name: 'Home' });
}
return true;
},
clear() {
this.$refs.form.reset();
},
},
};
</script>
然后将action
和mutations
添加到store.js
文件中:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
movies: []
},
getters: {
fetchMovies: state => state.movies,
},
mutations: {
ADD_MOVIE: (state, payload) => {
state.movies.unshift(payload);
},
MOVIES: (state, payload) => {
state.movies = payload;
}
},
actions: {
addMovie: (context, payload) => {
return axios({
method: 'post',
data: payload,
url: '/movies',
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
context.commit("ADD_MOVIE", response.data)
this.$swal(
'Great!',
'Movie added successfully!',
'success',
);
})
.catch(() => {
this.$swal(
'Oh oo!',
'Could not add the movie!',
'error',
);
});
},
fetchMovies: (context, payload) => {
axios({
method: 'get',
url: '/movies',
})
.then((response) => {
context.commit("MOVIES", response.data.movies);
})
.catch(() => {
});
}
}
})
最后,运行以下命令为 Vue 组件构建静态文件:
$ npm run build
现在,当我们登录并使用 admin 用户添加电影时,应该将电影添加到数据库中,并在主页上列出。
在这样一个小应用中使用 Vuex 有点过分了。 Vuex 的最佳用途是在需要在多个组件之间传输和共享数据的大型应用中。 这让您了解了 Vuex 是如何工作的以及如何实现它。
在本章中,我们讨论了什么是 Vuex——Vuex 状态、getter、突变、动作的核心概念,以及如何在应用中使用它们。 我们讨论了如何构造应用来实现 Vuex,以及当应用变得更大时它所带来的好处。
在下一章中,我们将介绍如何为 Vue.js 和 Node.js 应用编写单元测试和集成。