组件化和逻辑复用能帮助写出简洁易懂的代码,随着应用越写越复杂,我们有必要把视图层中重复的逻辑抽成组件,以求在多个页面中复用;同时对于 Vuex 端,Store 中的逻辑也会越来越臃肿,我们有必要使用 Vuex 提供的 Getters 来复用本地数据获取逻辑。在这篇教程中,我们将带领你抽出 Vue 组件简化页面逻辑,使用 Vuex Getters 复用本地数据获取逻辑。
使用 Vue 组件简化页面逻辑
在前面的教程中,我们已经学习了如何使用 Vuex 进行状态管理,如何使用 Action 获取远程数据以及如何使用 Mutation 修改本地状态,实现了用户修改客户端数据的同时,同步更新后端数据,然后更新本地数据,最后进行重新渲染。
这一节我们将进一步通过 Vue 组件化的思想简化复杂的页面逻辑。
实现 ProductButton 组件
我们打开 src/components/products/ProductButton.vue
文件,它是用于操作商品在购物车中状态的按钮组件,代码如下:
<!-- src/components/products/ProductButton.vue -->
<template>
<div>
<button v-if="isAdding" class="button" @click="addToCart">加入购物车</button>
<button v-else class="button" @click="removeFromCart(product._id)">从购物车移除</button>
</div>
</template>
<script>
export default {
props: ['product'],
computed: {
isAdding() {
let isAdding = true;
this.cart.map(product => {
if (product._id === this.product._id) {
isAdding = false;
}
});
return isAdding;
},
cart() {
return this.$store.state.cart;
}
},
methods: {
addToCart() {
this.$store.commit('ADD_TO_CART', {
product: this.product,
})
},
removeFromCart(productId) {
this.$store.commit('REMOVE_FROM_CART', {
productId,
})
}
}
}
</script>
复制代码
该组件通过 v-if
判断 isAdding
是否为 true
来决定创建加入购物车按钮还是从购物车移除按钮。cart
数组是通过 this.$store.state.cart
从本地获取的。在 isAdding
中我们先令其为 true
,然后通过 cart
数组的 map
方法遍历数组,判断当前商品是否在购物车中,如果不在则 isAdding
为 true
,创建加入购物车按钮;如果在则 isAdding
为 false
,创建从购物车移除按钮。
对应的两个按钮添加了两个点击事件:addToCart
和removeFromCart
当点击加入购物车按钮时触发 addToCart
,我们通过 this.$store.commit
的方式将包含当前商品的对象作为载荷直接提交到类型为 ADD_TO_CART
的 mutation
中,将该商品添加到本地购物车中。
当点击从购物车移除按钮时触发removeFromCart
,我们也是通过this.$store.commit
的方式将包含当前商品 id 的对象作为载荷直接提交到类型为REMOVE_FROM_CART
的mutation
中,将该商品从本地购物车中移除。
实现 ProductItem 组件
src/components/products/ProductItem.vue
文件为商品信息组件,用来展示商品详细信息,并且注册了上面讲的按钮组件,改变商品在购物车中的状态,除此之外我们还使用了之前创建好的ProductButton
组件,实现对商品在购物车中的状态进行修改。
代码如下:
<!-- src/components/products/ProductItem.vue -->
<template>
<div>
<div 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.name}}</p>
<img :src="product.image" alt="" class="product__image">
<product-button :product="product"></product-button>
</div>
</div>
</template>
<script>
import ProductButton from './ProductButton';
export default {
name: 'product-item',
props: ['product'],
components: {
'product-button': ProductButton,
}
}
</script>
复制代码
可以看到,我们将父组件传入的product
对象展示到模板中,并将该product
对象传到子组件ProductButton
中。
重构 ProductList 组件
有了 ProductButton 和 ProductItem,我们便可以来重构之前略显臃肿的 ProductList 组件了,修改 src/components/products/ProductList.vue
,代码如下:
<!-- src/components/products/ProductList.vue -->
<!-- ... -->
This is ProductList
</div>
<template v-for="product in products">
<product-item :product="product" :key="product._id"></product-item>
</template>
</div>
</div>
<!-- ... -->
</style>
<script>
import ProductItem from './ProductItem.vue';
export default {
name: 'product-list',
created() {
<!-- ... -->
return this.$store.state.products;
}
},
components: {
'product-item': ProductItem
}
}
</script>
复制代码
这部分代码是将之前展示商品信息的逻辑代码封装到了子组件ProductItem
中,然后导入并注册子组件ProductItem
,再将子组件挂载到模板中。
可以看到,我们通过this.$store.state.products
从本地获取products
数组,并返回给计算属性products
。然后在模板中利用v-for
遍历products
数组,并将每个product
对象传给每个子组件ProductItem
,在每个子组件中展示对应的商品信息。
重构 Cart 组件
最后,我们重构一波购物车组件 src/pages/Cart.vue
,也使用了子组件ProductItem
简化了页面逻辑,修改代码如下:
<!-- src/pages/Cart.vue -->
<!-- ... -->
<h1>{{msg}}</h1>
</div>
<template v-for="product in cart">
<product-item :product="product" :key="product._id"></product-item>
</template>
</div>
</template>
<!-- ... -->
</style>
<script>
import ProductItem from '@/components/products/ProductItem.vue';
export default {
name: 'home',
data () {
<!-- ... -->
return this.$store.state.cart;
}
},
components: {
'product-item': ProductItem
}
}
</script>
复制代码
这里也是首先导入并注册子组件ProductItem
,然后在模板中挂载子组件。通过this.$store.state.cart
的方式从本地获取购物车数组,并返回给计算属性cart
。在模板中通过v-for
遍历购物车数组,并将购物车中每个商品对象传给对应的子组件ProductItem
,通过子组件来展示对应的商品信息。
把项目开起来,查看商品列表,可以看到每个商品下面都增加了“添加到购物车”按钮:
购物车中,也有了“移出购物车”按钮:
尽情地买买买吧!
小结
这一节我们学习了如何使用 Vue 组件来简化页面逻辑:
使用 Vuex Getters 复用本地数据获取逻辑
在这一节中,我们将实现这个电商应用的商品详情页面。商品详情和之前商品列表在数据获取上的逻辑是非常一致的,能不能不写重复的代码呢?答案是肯定的。之前我们使用 Vuex 进行状态管理是通过 this.$store.state
的方式获取本地数据,而在这一节我们使用 Vuex Getters
来复用本地数据的获取逻辑。
Vuex
允许我们在 store
中定义“getter”(可以认为是 store
的计算属性)。就像计算属性一样,getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter
也是定义在 Vuex Store 的 getter
属性中的一系列方法,用于获取本地状态中的数据。我们可以通过两种方式访问 getter
,一个是通过属性访问,另一个是通过方法访问:
allProducts(state) {
// 返回本地中的数据
return state.products;
}
复制代码
productById: (state, getters) => id => {
//通过传入的id参数进行一系列操作并返回本地数据
return state.product;
}
复制代码
我们可以看到Getter
可以接受两个参数:state
和getters
,state
就表示本地数据源;我们可以通过第二个参数getters
获取到不同的getter
属性。
定义 Vuex Getters
光说不练假把式,我们来手撸几个 getters。打开 src/store/index.js
文件,我们添加了一些需要用到的 action
属性、mutation
属性以及这一节的主角—— getters
。代码如下:
// src/store/index.js
// ...
state.showLoader = false;
state.products = products;
},
PRODUCT_BY_ID(state) {
state.showLoader = true;
},
PRODUCT_BY_ID_SUCCESS(state, payload) {
state.showLoader = false;
const { product } = payload;
state.product = product;
}
},
getters: {
allProducts(state) {
return state.products;
},
productById: (state, getters) => id => {
if (getters.allProducts.length > 0) {
return getters.allProducts.filter(p => p._id == id)[0];
} else {
return state.product;
}
}
},
actions: {
// ...
commit('ALL_PRODUCTS')
axios.get(`${API_BASE}/products`).then(response => {
commit('ALL_PRODUCTS_SUCCESS', {
products: response.data,
});
})
},
productById({ commit }, payload) {
commit('PRODUCT_BY_ID');
const { productId } = payload;
axios.get(`${API_BASE}/products/${productId}`).then(response => {
commit('PRODUCT_BY_ID_SUCCESS', {
product: response.data,
});
})
}
}
});
复制代码
这里主要添加了三部分内容:
在actions
中添加了productById
属性,当视图层通过指定 id 分发到类型为PRODUCTBYID
的action
中,这里会进行异步操作从后端获取指定商品,并将该商品提交到对应类型的mutation
中,就来到了下一步。
在mutations
中添加了PRODUCT_BY_ID
和PRODUCTBYID_SUCCESS
属性,响应指定类型提交的事件,将提交过来的商品保存到本地。
添加了getters
并在getters
中添加了allProducts
属性和productById
方法,用于获取本地数据。在allProducts
中获取本地中所有的商品;在productById
通过传入的 id 查找本地商品中是否存在该商品,如果存在则返回该商品,如果不存在则返回空对象。
在后台 Products 组件中使用 Getters
我们先通过一个简单的例子演示如果使用 Vuex Getters。打开后台商品组件,src/pages/admin/Products.vue
,我们通过属性访问的方式调用对应的 getter
属性,从而获取本地商品,代码如下:
<!-- src/pages/admin/Products.vue -->
<!-- ... -->
export default {
computed: {
product() {
return this.$store.getters.allProducts[0];
}
}
}
<!-- ... -->
复制代码
我们通过this.$store.getters.allProducts
属性访问的方式调用对应getter
中的allProducts
属性,并返回本地商品数组中的第一个商品。
创建 ProductDetail 组件
接着开始实现商品详情组件 src/components/products/ProductDetail.vue
,代码如下:
<!-- src/components/products/ProductDetail.vue -->
<template>
<div class="product-details">
<div class="product-details__image">
<img :src="product.image" alt="" class="image">
</div>
<div class="product-details__info">
<div class="product-details__description">
<small>{{product.manufacturer.name}}</small>
<h3>{{product.name}}</h3>
<p>
{{product.description}}
</p>
</div>
<div class="product-details__price-cart">
<p>{{product.price}}</p>
<product-button :product="product"></product-button>
</div>
</div>
</div>
</template>
<style>
.product-details__image .image {
width: 100px;
height: 100px;
}
</style>
<script>
import ProductButton from './ProductButton';
export default {
props: ['product'],
components: {
'product-button': ProductButton
}
}
</script>
复制代码
该组件将父组件传入的product
对象展示在了模板中,并复用了ProductButton
组件。
在 ProductItem 组件中添加链接
有了商品详情,我们还需要进入详情的链接。再次进入 src/components/products/ProductItem.vue
文件中,我们对其进行了修改,将模板中的商品信息用 Vue 原生组件 router-link
包裹起来,实现商品信息可点击查看详情。代码如下:
<!-- src/components/products/ProductItem.vue -->
<template>
<div>
<div class="product">
<router-link :to="'/detail/' + product._id" class="product-link">
<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.name}}</p>
<img :src="product.image" alt="" class="product__image">
</router-link>
<product-button :product="product"></product-button>
</div>
</div>
</template>
<style>
.product {
border-bottom: 1px solid black;
}
.product__image {
width: 100px;
height: 100px;
}
</style>
<script>
import ProductButton from './ProductButton';
export default {
<!-- ... -->
复制代码
该组件经过修改之后实现了点击商品的任何一条信息,都会触发路由跳转到商品详情页,并将该商品 id 通过动态路由的方式传递到详情页。
在 ProductList 中使用 Getters
修改商品列表组件 src/components/products/ProductList.vue
文件,使用了 Vuex Getters 复用了本地数据获取逻辑,代码如下:
<!-- src/components/products/ProductList.vue -->
<!-- ... -->
</div>
</template>
<script>
import ProductItem from './ProductItem.vue';
export default {
<!-- ... -->
computed: {
// a computed getter
products() {
return this.$store.getters.allProducts;
}
},
components: {
<!-- ... -->
复制代码
我们在计算属性products
中使用this.$store.getters.allProducts
属性访问的方式调用getters
中的allProducts
属性,我们也知道在对应的getter
中获取到了本地中的products
数组。
创建 Detail 页面组件
实现了 ProductDetail 子组件之后,我们便可以搭建商品详情我页面组件 src/pages/Detail.vue
,代码如下:
<!-- src/pages/Detail.vue -->
<template>
<div>
<product-detail :product="product"></product-detail>
</div>
</template>
<script>
import ProductDetail from '@/components/products/ProductDetail.vue';
export default {
created() {
// 跳转到详情时,如果本地状态里面不存在此商品,从后端获取此商品详情
const { name } = this.product;
if (!name) {
this.$store.dispatch('productById', {
productId: this.$route.params['id']
});
}
},
computed: {
product() {
return this.$store.getters.productById(this.$route.params['id']);
}
},
components: {
'product-detail': ProductDetail,
}
}
</script>
复制代码
该组件中定义了一个计算属性product
,用于返回本地状态中指定的商品。这里我们使用了this.$store.getters.productById(id)
方法访问的方式获取本地中指定的商品,这里的 id 参数通过this.$route.params['id']
从当前处于激活状态的路由对象中获取,并传入对应的getter
中,进而从本地中获取指定商品。
在该组件刚被创建时判断当前本地中是否有该商品,如果没有则通过this.$store.dispatch
的方式将包含当前商品 id 的对象作为载荷分发到类型为productById
的action
中,在action
中进行异步操作从后端获取指定商品,然后提交到对应的mutation
中进行本地状态修改,这已经使我们习惯的思路了。
配置 Detail 页面的路由
最后我们打开路由配置 src/router/index.js
文件,导入了 Detail
组件,并添加了对应的路由参数,代码如下:
// src/router/index.js
// ...
import Home from '@/pages/Home';
import Cart from '@/pages/Cart';
import Detail from '@/pages/Detail';
// Admin Components
import Index from '@/pages/admin/Index';
// ...
name: 'Cart',
component: Cart,
},
{
path: '/detail/:id',
name: 'Detail',
component: Detail,
}
],
});
复制代码
又到了验收的环节,运行项目,点击单个商品,可以进入到商品详情页面,并且数据是完全一致的:
小结
这一节中我们学会了如何使用Vuex Getters
来复用本地数据的获取逻辑:
我们需要先在store
实例中添加getters
属性,并在getters
属性中定义不同的属性或者方法。
在这些不同类型的getter
中,我们可以获取本地数据。
我们可以通过属性访问和方法访问的方式来调用我们的getter
。
评论