随着前端应用的日渐复杂,状态和数据管理成为了构建大型应用的关键。受 Redux 等项目的启发,Vue.js 团队也量身定作了状态管理库 Vuex。在这篇教程中,咱们将带你熟悉 Store、Mutation 和 Action 三大关键概念,并升级迷你商城应用的前端代码。javascript
欢迎阅读《从零到部署:用 Vue 和 Express 实现迷你全栈电商应用》系列:css
咱们在第一篇和第三篇中讲解了 Vue 的基础部分,利用这些知识你已经能够实现一些比较简单的应用了。可是针对复杂的应用,好比组件嵌套超过三级,咱们前面讲解的知识处理起来就很费力了,还好 Vue 社区为咱们打造了状态管理容器 Vuex,用来处理大型应用的数据和状态管理。html
首先咱们打开命令行,进入项目目录,执行以下命令安装 Vuex:前端
npm install vuex
复制代码
Vuex 是一个前端状态管理工具,它致力于接管 Vue 的状态,使得 Vue 专心作好渲染页面的事情;它相似在前端创建了一个 “数据库”,而后将全部的前端状态都保存在这个 “数据库” 里面。这个 “数据库” 其实就是一个普通的 JavaScript 对象。vue
好了,讲述了 Vuex 是干什么以后,咱们来看一下如何在 Vue 中运用 Vuex。Vuex 创建的这个 “数据库” 通常用术语 store
来表示,一般咱们创建一个单独的 store
文件夹,用于保存和 store
有关的内容。咱们在 src
文件夹下创建 store
文件夹,而后在里面建立 index.js
文件,代码以下:java
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {
// bought items
cart: [],
// ajax loader
showLoader: false,
// selected product
product: {},
// all products
products: [
{
name: '1',
}
],
// all manufacturers
manufacturers: [],
}
})
复制代码
上面的代码能够分为三部分。webpack
Vue
和 Vuex
Vue.use
方法,告诉 Vue 咱们将使用 Vuex
,这和咱们以前使用 Vue.use(router)
的原理同样Vuex.Store
实例,而且传入了 strict
和 state
参数。这里 strict
参数表示,咱们必须使用 Vuex 的 Mutation 函数来改变 state
,不然就会报错(关于 Mutation 咱们将在 “使用 Vuex 进行状态管理” 一节讲解)。而 state
参数用来存放咱们全局的状态,好比咱们这里定义了 cart
、 showLoader
等属性都是后面咱们完善应用的内容须要的数据。当咱们建立并导出了 Vuex 的 store
实例以后,咱们就可使用它了。打开 src/main.js
文件,在开头导入以前建立的 store
,并将 store
添加到 Vue 初始化的参数列表里,代码以下:ios
// 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 { ValidationProvider } from 'vee-validate';
import App from './App';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
Vue.component('ValidationProvider', ValidationProvider);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>',
});
复制代码
能够看到,在上面的文件中,咱们一开头导入了咱们以前在 src/store/index.js
里定义的 store
实例,接着,在 Vue 实例初始化时,咱们将这个 store
实例使用对象属性简洁写法添加到了参数列表里。git
当咱们将 store
当作参数传给 Vue 进行初始化以后,Vue 就会将 Store 里面的 state
注入到全部的 Vue 组件中,这样全部的 Vue 组件共享同一个全局的 state
,它其实就是一个 JS 对象,应用中全部状态的变化都是对 state
进行操做,而后响应式的触发组件的从新渲染,因此这里的 state
也有 “数据的惟一真相来源” 的称谓。github
这种将状态保存到一个全局的 JavaScript 对象 -- state 中,而后全部的增、删、改、查操做都是对这个 JavaScript 对象进行,使得咱们能够避免组件嵌套层级过深时,组件之间传递属性的复杂性,让属性的定义,获取,修改很是直观,方便开发大型应用和团队协做。
在将 Vuex 和 Vue 整合好以后,咱们立刻来看一下 Vuex 带来的效果,不过在此以前咱们先来说一讲什么是计算属性(computed
)。
首先咱们新增了 script
部分,而后在导出的对象里面增长了一个 computed
属性,这个属性里面的内容用于申明一些可能要在 template
里面使用的复杂表达式。咱们来看一个例子来说解一下 computed
属性:
咱们在模板中可能要获取一个多级嵌套对象里面的某个数据,或者要渲染的数据须要通过复杂的表达式来计算,好比咱们要渲染这样一个数据 obj1.obj2.obj3.a + obj1.obj4.b
,写在模板里就是这样的:
<template>
<div>
{{ obj1.obj2.obj3.a + obj1.obj4.b }}
</div>
</template>
<script> export default { data: { obj1: { obj2: { obj3: { a } }, obj4: { b } } } } </script>
复制代码
能够看到,咱们一眼看上去,这个模板里面有这样一个复杂的表达式,很不容易反应出来它到底要渲染什么,这样代码的可读性就不好,因此 Vue 为咱们提供了计算属性( computed
),用于用简单的变量来表明复杂的表达式结果,进而简化模板中插值的内容,让咱们的模板看起来可读性更好,上面的代码使用计算属性来改进会变成下面这样:
<template>
<div>
{{ addResult }}
</div>
</template>
<script> export default { data: { obj1: { obj2: { obj3: { a } }, obj4: { b } } }, computed: { addResult() { return this.obj1.obj2.obj3.a + this.obj1.obj4.b } } } </script>
复制代码
能够看到,当咱们使用了计算属性 addResult
以后,咱们在模板里面的写法就简化了不少,并且一目了然咱们是渲染了什么。
了解了计算属性以后,咱们打开 src/pages/admin/Products.vue
,对内容做出以下改进以查看 Vuex 和 Vue 整合以后的效果:
<template>
<div>
<div class="title">
<h1>This is Admin</h1>
</div>
<div class="body">
{{ product.name }}
</div>
</div>
</template>
<script> export default { computed: { product() { return this.$store.state.products[0]; } } } </script>
复制代码
能够看到,上面的内容改进主要分为两个部分:
product
计算属性,它里面返回一个从 store
中保存的 state
取到的 products
数组的第一个元素,注意到当咱们在 “将 Vuex 和 Vue 整合” 这一小节中将 store
做为 Vue 初始化实例参数,因此咱们在全部的 Vue 组件中能够经过 this.$store.state
的形式取到 Vuex Store 中保存的 state
。product
,取到了它的 name
属性进行渲染。在这一小节中,咱们学习了如何将 Vuex 整合进 Vue 中:
vuex
依赖src
下面建立了 store
文件夹,用于保存 Vuex 相关的内容,在 store
文件下之下,咱们建立了 index.js
文件,在里面实例化了 Vuex.Store
类,咱们在实例化的过程当中传递了两个参数:strict
和 state
,strict
表示咱们告诉 Vue,只容许 Mutation
方法才能修改 state
,确保修改状态的惟一性;state
是咱们整个应用的状态,整个应用的状态都是从它获取,整个应用状态的改变都是修改它,因此这个 state
也有 “数据的惟一真相来源” 的称谓。main.js
里面导入实例化的 store
,将它加入到初始化 Vue 实例的参数列表中,实现了 Vuex 和 Vue 的整合。this.$store.state
的方式展现了 Vuex 整合以后的效果。好了,咱们已经整合了 Vuex,并在 Vue 组件中获取了保存在 Vuex Store 中的状态(state),接下来咱们来看一下如何修改这个状态。
咱们在上一节中定义了 Vuex Store,并在里面保存了全局的状态 state
,这一节咱们来学习如何修改这一状态。
Vuex 为咱们提供了 Mutation
,它是修改 Vuex Store 中保存状态的惟一手段。
Mutation 是定义在 Vuex Store 的 mutation
属性中的一系列形如 (state, payload) => newState
的函数,用于响应从 Vue 视图层发出来的事件或动做,例如:
ACTION_NAME(state, payload) {
// 对 `state` 进行操做以返回新的 `state`
return newState;
}
复制代码
其中方法名 ACTION_NAME
用于对应从视图层里面发出的事件或动做的名称,这个函数接收两个参数 state
和 payload
,state
就是咱们 Vuex Store 中保存的 state
,payload
是被响应的那个事件或动做携带的参数,而后咱们经过 payload
的参数来操做现有的 state
,返回新的 state
,经过这样的方式,咱们就能够响应修改 Vuex Store 中保存的全局状态。
了解了 Mutation 的概念以后,咱们立刻来看一下如何运用它。
咱们打开 src/store/index.js
文件,修改其中的 state
并加上 mutations
以下:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {
// bought items
cart: [],
// ajax loader
showLoader: false,
// selected product
product: {},
// all products
products: [
{
_id: '1',
name: 'iPhone',
description: 'iPhone是美国苹果公司研发的智能手机系列,搭载苹果公司研发的iOS操做系统',
image: 'https://i.gadgets360cdn.com/large/iPhone11_leak_1567592422045.jpg',
price: 2000,
manufacturer: 'Apple Inc'
},
{
_id: '2',
name: '荣耀20',
description: '李现同款 4800万超广角AI四摄 3200W美颜自拍 麒麟Kirin980全网通版8GB+128GB 蓝水翡翠 全面屏手机',
image: 'https://article-fd.zol-img.com.cn/t_s640x2000/g4/M08/0E/0E/ChMlzF2myueILMN_AAGSPzoz23wAAYJ3QADttsAAZJX090.jpg',
price: 2499,
manufacturer: '华为'
},
{
_id: '3',
name: 'MIX2S',
description: '骁龙845 全面屏NFC 游戏智能拍照手机 白色 全网通 6+128',
image: 'http://himg2.huanqiu.com/attachment2010/2018/0129/08/39/20180129083933823.jpg',
price: 1688,
manufacturer: '小米'
},
{
_id: '4',
name: 'IQOO Pro',
description: '12GB+128GB 竞速黑 高通骁龙855Plus手机 4800万AI三摄 44W超快闪充 5G全网通手机',
image: 'https://www.tabletowo.pl/wp-content/uploads/2019/08/vivo-iqoo-pro-5g-blue-1.jpg',
price: 4098,
manufacturer: 'Vivo'
},
{
_id: '5',
name: 'Reno2',
description: '【12期免息1年碎屏险】4800万变焦四摄8+128G防抖6.5英寸全面屏新 深海夜光(8GB+128GB) 官方标配',
image: 'https://news.maxabout.com/wp-content/uploads/2019/08/OPPO-Reno-2-1.jpg',
price: 2999,
manufacturer: 'OPPO'
}
],
// all manufacturers
manufacturers: [],
},
mutations: {
ADD_TO_CART(state, payload) {
const { product } = payload;
state.cart.push(product)
},
REMOVE_FROM_CART(state, payload) {
const { productId } = payload
state.cart = state.cart.filter(product => product._id !== productId)
}
}
});
复制代码
能够看到上面的代码改进分为两个部分:
state
中的 products
属性,在里面保存一开始咱们的迷你电商平台的初始数据,这里咱们是硬编码到代码中的,在下一节 “使用 Action 获取远程数据”中,咱们将动态获取后端服务器的数据。Vuex.Store
实例化的参数中添加了一个 mutations
属性,在里面定义了两个函数 ADD_TO_CART
和 REMOVE_FROM_CART
,分别表明响应从视图层发起的对应将商品添加至购物车和从购物车移除商品的动做。接着建立 src/components/products/ProductList.vue
文件,它是商品列表组件,用来展现商品的详细信息,代码以下:
<template>
<div>
<div class="products">
<div class="container">
This is ProductList
</div>
<template v-for="product in products">
<div :key="product._id" class="product">
<p class="product__name">产品名称:{{product.name}}</p>
<p class="product__description">介绍:{{product.description}}</p>
<p class="product__price">价格:{{product.price}}</p>
<p class="product.manufacturer">生产厂商:{{product.manufacturer}}</p>
<img :src="product.image" alt="" class="product__image">
<button @click="addToCart(product)">加入购物车</button>
</div>
</template>
</div>
</div>
</template>
<style> .product { border-bottom: 1px solid black; } .product__image { width: 100px; height: 100px; } </style>
<script> export default { name: 'product-list', computed: { // a computed getter products() { return this.$store.state.products; } }, methods: { addToCart(product) { this.$store.commit('ADD_TO_CART', { product }); } } } </script>
复制代码
咱们首先来看该组件的 script
部分:
products
,经过 this.$store.state.products
从本地状态中获取到了 products
数组,并做为计算属性 products
的返回值addToCart
,而且传入了当前处于激活状态的 product
参数。当用户点击“添加购物车”时,触发 addToCart
事件,也就是上面所说的视图层发出的事件。这里是经过 this .$store.commit
将携带当前商品的对象 {product}
做为载荷提交到类型为ADD_TO_CART
的 mutation
中,在 mutation
中进行本地状态修改,咱们会在后面抽离出的 mutations
文件中看到具体的操做。再看该组件的 template
部分,使用 v-for
将从本地获取到的 products
数组进行遍历,每一个 product
对象的详细信息都会显示在模板中。此外,咱们还在每一个 product
对象信息的最后添加了一个“加入购物车”的按钮,容许咱们将指定商品添加到购物车。
Store 和组件都搞定以后,咱们就能够在以前的页面中接入数据了。修改主页 src/pages/Home.vue
,代码以下:
<template>
<div>
<div class="title">
<h1>In Stock</h1>
</div>
<product-list></product-list>
</div>
</template>
<script> import ProductList from '@/components/products/ProductList.vue'; export default { name: 'home', data () { return { msg: 'Welcome to Your Vue.js App' }; }, components: { 'product-list': ProductList } } </script>
复制代码
能够看到,咱们在导入 ProductList
组件后,将其注册到 components
中,而后在模板中使用这个组件。
接着修改购物车页面 src/pages/Cart.vue
文件,将购物车中的商品信息展现出来,添加代码以下:
<template>
<div>
<div class="title">
<h1>{{msg}}</h1>
</div>
<template v-for="product in cart">
<div :key="product._id" class="product">
<p class="product__name">产品名称:{{product.name}}</p>
<p class="product__description">介绍:{{product.description}}</p>
<p class="product__price">价格:{{product.price}}</p>
<p class="product.manufacturer">生产厂商:{{product.manufacturer}}</p>
<img :src="product.image" alt="" class="product__image">
<button @click="removeFromCart(product._id)">从购物车中移除</button>
</div>
</template>
</div>
</template>
<style> .product { border-bottom: 1px solid black; } .product__image { width: 100px; height: 100px; } </style>
<script> export default { name: 'home', data () { return { msg: 'Welcome to the Cart Page' } }, computed: { cart() { return this.$store.state.cart; } }, methods: { removeFromCart(productId) { this.$store.commit('REMOVE_FROM_CART', { productId }); } } } </script>
复制代码
咱们在该组件中主要增长了两部分代码:
首先是 script
部分,咱们增长了一个计算属性和一个点击事件。一样是经过 this.$store.state.cart
的方式从本地状态中获取购物车数组,并做为计算属性 cart
的返回值;当用户点击购物车中的某个商品将其移除购物车时就会触发 removeFromCart
事件,而且将要移除的商品 id 做为参数传入,而后也是经过 this.$store.commit
的方式将包含 productId
的对象做为载荷提交到类型为 REMOVE_FROM_CART
的 mutation
中,在 mutation
中进行本地状态修改,具体修改操做咱们能够在后面抽离出的 mutations
文件中看到。
而后是 template
部分,咱们经过 v-for
遍历了购物车数组,将购物车中的全部商品信息展现在模板中。并在每一个商品信息的最后添加了一个移除购物车的按钮,当用户但愿移除购物车中指定商品时,会触发 removeFromCart
事件。
在项目根目录下运行 npm start
,进入开发服务器查看效果:
能够看到,一开始咱们的购物车是空的,而后随便选了两款手机,点击“加入购物车”,而后就能够在购物车页面看到了!咱们还能够将购物车中的商品移除。
在这一部分中咱们学习了如何发起修改本地状态的“通知”:
Vuex.Store
实例化的参数中添加一个 mutations
属性,在该属性中添加对应的方法,好比 ADD_TO_CART
和 REMOVE_FROM_CART
。this.$store.commit
的方式将须要操做的对象做为载荷提交到对应类型(也就是 ADD_TO_CART
和 REMOVE_FROM_CART
)的 mutation
中,在 mutation
中进行本地状态修改。咱们在上一节中学习了如何在视图层发起本地状态修改的“通知”,这一节咱们来学习如何从后端获取远程数据。请求库咱们采用的是 axios,经过如下命令安装依赖:
npm install axios
复制代码
Vuex 为咱们提供了 Action
,它是用来进行异步操做,咱们能够在这里向后端发起网络数据请求,并将请求到的数据提交到对应的 mutation
中。
Action 是定义在 Vuex Store 的 action
属性中的一系列方法,用于响应从 Vue 视图层分发出来的事件或动做,一个 Action 是形如 (context, payload) => response.data
的函数:
productById(context, payload) {
// 进行异步操做,从后端获取远程数据并返回
return response.data;
}
复制代码
其中:
productById
用于对应从视图层里面分发出的事件或动做的名称context
和 payload
context
指的是 action
的上下文,与 store
实例具备相同的方法和属性,所以咱们能够调用 context.commit
提交一个 mutation
,或者经过 context.state
和 context.getters
来获取 state
和 getters
,可是 context
对象又不是 store
实例自己payload
是分发时携带的参数,而后咱们经过 payload
中的参数来进行异步操做,从而获取后端响应数据并返回。这样咱们就能够根据用户的操做同步更新后端数据,并将后端响应的数据提交给 mutation
,而后利用 mutation
进行本地数据更新。让咱们趁热打铁,实现第一个 Action。再次来到 src/store/index.js
文件,修改代码以下:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
const API_BASE = 'http://localhost:3000/api/v1';
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {
// bought items
cart: [],
// ajax loader
showLoader: false,
// selected product
product: {},
// all products
products: [],
// all manufacturers
manufacturers: [],
},
mutations: {
ADD_TO_CART(state, payload) {
const { product } = payload;
state.cart.push(product)
},
REMOVE_FROM_CART(state, payload) {
const { productId } = payload
state.cart = state.cart.filter(product => product._id !== productId)
},
ALL_PRODUCTS(state) {
state.showLoader = true;
},
ALL_PRODUCTS_SUCCESS(state, payload) {
const { products } = payload;
state.showLoader = false;
state.products = products;
}
},
actions: {
allProducts({ commit }) {
commit('ALL_PRODUCTS')
axios.get(`${API_BASE}/products`).then(response => {
console.log('response', response);
commit('ALL_PRODUCTS_SUCCESS', {
products: response.data,
});
})
}
}
});
复制代码
能够看到,咱们作了如下几件事:
axios
,并定义了 API_BASE
后端接口根路由;store
中去掉了以前硬编码的假数据,使 products
默认值为空数组;mutations
属性中添加了 ALL_PRODUCTS
和 ALL_PRODUCTS_SUCCESS
方法,用来响应 action
中提交的对应类型事件;ALL_PRODUCTS
将 state.showLoader
设为 true
,显示加载状态;ALL_PRODUCTS_SUCCESS
将 action
中提交的数据保存到 state
中,并取消加载状态;actions
属性,在 actions
属性中定义了 allProducts
函数用于响应视图层分发的对应类型的事件;咱们首先提交了类型为 ALL_PRODUCTS
的 mutation
,接着在 axios
请求成功后提交 ALL_PRODUCTS_SUCCESS
,并附带 products
数据体(payload
)提示
咱们能够看到
allProducts
方法中传入了{ commit }
参数,这是采用了解构赋值的方式const { commit } = context
,避免后面使用context.commit
过于繁琐。
再来看 src/components/products/ProductList.vue
文件,咱们对其作了修改,主要添加了生命周期函数 created
,在该组件刚被建立时首先判断本地 products
中是否有商品,若是没有就向后端发起网络请求获取数据。代码以下:
<template>
<div>
<div class="products">
<div class="container">
This is ProductList
</div>
<template v-for="product in products">
<div :key="product._id" class="product">
<!-- 其余字段 -->
<p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
<img :src="product.image" alt="" class="product__image">
<button @click="addToCart(product)">加入购物车</button>
</div>
</template>
</div>
</div>
</template>
<!-- style -->
<script> export default { name: 'product-list', created() { if (this.products.length === 0) { this.$store.dispatch('allProducts') } }, computed: { // ... }, methods: { // ... } } </script>
复制代码
注意到咱们修改了两个地方:
{{product.manufacturer}}
修改成 {{product.manufacturer.name}}
created
生命周期方法,在该组件刚被建立时判断 this.products.length === 0
是 true
仍是 false
,若是是 true
则证实本地中尚未任何商品,须要向后端获取商品数据,因而经过 this.$store.dispatch
的方式触发类型为 allProducts
的 action
中,在 action
中进行异步操做,发起网络请求向后端请求商品数据并返回;若是是 false
则证实本地中存在商品,因此能够直接从本地获取而后进行渲染。最后咱们也一样须要调整一下 src/pages/Cart.vue
中的“生产厂商”字段,修改其模板代码以下:
<template>
<div>
<div class="title">
<h1>{{msg}}</h1>
</div>
<template v-for="product in cart">
<div :key="product._id" class="product">
<!-- 其余字段 -->
<p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
<img :src="product.image" alt="" class="product__image">
<button @click="removeFromCart(product._id)">从购物车中移除</button>
</div>
</template>
</div>
</template>
<!-- style -->
<!-- script -->
复制代码
一样把 {{product.manufacturer}}
修改成 {{product.manufacturer.name}}
。
在测试这一步效果以前,首先确保 MongoDB 和后端 API 服务器已经开启。同时,若是你以前没有在第二篇教程中测试过,颇有可能你的数据库是空的,那么能够下载咱们提供的 MongoDB JSON 数据文件 manufacturers.json 和 products.json,而后运行如下命令:
mongoimport -d test -c manufacturers manufacturers.json
mongoimport -d test -c products products.json
复制代码
而后再进入前端测试,你应该就能够看到从后台获取到的数据,而后一样能够添加到购物车哦!
在这一部分中咱们学习了如何使用 Action
获取远程数据,并将获取的数据提交到对应的 Mutation
中:
axios
和 API_BASE
,因为发起网络请求。store
实例中添加 actions
属性,并在 actions
属性定义对应的方法,用于响应视图层分发的对应类型的事件。mutation
中。在下一篇教程中,咱们将进一步探索 Vue 组件化,从而简化页面逻辑,并抽出 Getters 和 Mutation 数据逻辑。
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。