0%

从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(五)

@pftom

查看代码

使用 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 方法遍历数组,判断当前商品是否在购物车中,如果不在则 isAddingtrue,创建加入购物车按钮;如果在则 isAddingfalse,创建从购物车移除按钮。

对应的两个按钮添加了两个点击事件:addToCartremoveFromCart

  • 当点击加入购物车按钮时触发 addToCart,我们通过 this.$store.commit 的方式将包含当前商品的对象作为载荷直接提交到类型为 ADD_TO_CARTmutation 中,将该商品添加到本地购物车中。
  • 当点击从购物车移除按钮时触发removeFromCart,我们也是通过this.$store.commit的方式将包含当前商品id的对象作为载荷直接提交到类型为REMOVE_FROM_CARTmutation中,将该商品从本地购物车中移除。

实现 ProductItem 组件

src/components/products/ProductItem.vue文件为商品信息组件,用来展示商品详细信息,并且注册了上面讲的按钮组件,改变商品在购物车中的状态,除此之外我们还使用了之前创建好的ProductButton组件,实现对商品在购物车中的状态进行修改。

  • 首先通过import ProductButton from './ProductButton'导入创建好的ProductButton组件。
  • 然后在components中注册组件。
  • 最后在模板中使用该组件。

代码如下:

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">
[tuture-del] <div :key="product._id" class="product">
[tuture-del] <p class="product__name">产品名称:{{product.name}}</p>
[tuture-del] <p class="product__description">介绍:{{product.description}}</p>
[tuture-del] <p class="product__price">价格:{{product.price}}</p>
[tuture-del] <p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
[tuture-del] <img :src="product.image" alt="" class="product__image">
[tuture-del] <button @click="addToCart(product)">加入购物车</button>
[tuture-del] </div>
[tuture-add] <product-item :product="product" :key="product._id"></product-item>
</template>
</div>
</div>
[tuture-omit]
</style>

<script>
[tuture-add]import ProductItem from './ProductItem.vue';
export default {
name: 'product-list',
created() {
[tuture-omit]
return this.$store.state.products;
}
},
[tuture-del] methods: {
[tuture-del] addToCart(product) {
[tuture-del] this.$store.commit('ADD_TO_CART', {
[tuture-del] product
[tuture-del] });
[tuture-del] }
[tuture-add] components: {
[tuture-add] '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">
[tuture-del] <div :key="product._id" class="product">
[tuture-del] <p class="product__name">产品名称:{{product.name}}</p>
[tuture-del] <p class="product__description">介绍:{{product.description}}</p>
[tuture-del] <p class="product__price">价格:{{product.price}}</p>
[tuture-del] <p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
[tuture-del] <img :src="product.image" alt="" class="product__image">
[tuture-del] <button @click="removeFromCart(product._id)">从购物车中移除</button>
[tuture-del] </div>
[tuture-add] <product-item :product="product" :key="product._id"></product-item>
</template>
</div>
</template>
[tuture-omit]
</style>

<script>
[tuture-add]import ProductItem from '@/components/products/ProductItem.vue';
export default {
name: 'home',
data () {
[tuture-omit]
return this.$store.state.cart;
}
},
[tuture-del] methods: {
[tuture-del] removeFromCart(productId) {
[tuture-del] this.$store.commit('REMOVE_FROM_CART', {
[tuture-del] productId
[tuture-del] });
[tuture-del] }
[tuture-add] components: {
[tuture-add] 'product-item': ProductItem
}
}
</script>

这里也是首先导入并注册子组件ProductItem,然后在模板中挂载子组件。通过this.$store.state.cart的方式从本地获取购物车数组,并返回给计算属性cart。在模板中通过v-for遍历购物车数组,并将购物车中每个商品对象传给对应的子组件ProductItem,通过子组件来展示对应的商品信息。

把项目开起来,查看商品列表,可以看到每个商品下面都增加了“添加到购物车”按钮:

购物车中,也有了“移出购物车”按钮:

尽情地买买买吧!

小结

这一节我们学习了如何使用 Vue 组件来简化页面逻辑:

  • 首先我们需要通过import的方式导入子组件。
  • 然后在components中注册子组件。
  • 最后将子组件挂载到模板中,并将需要子组件展示的数据传给子组件。

使用 Vuex Getters 复用本地数据获取逻辑

在这一节中,我们将实现这个电商应用的商品详情页面。商品详情和之前商品列表在数据获取上的逻辑是非常一致的,能不能不写重复的代码呢?答案是肯定的。之前我们使用 Vuex 进行状态管理是通过 this.$store.state 的方式获取本地数据,而在这一节我们使用 Vuex Getters来复用本地数据的获取逻辑。

Vuex允许我们在 store 中定义“getter”(可以认为是 store的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter也是定义在 Vuex Store 的 getter 属性中的一系列方法,用于获取本地状态中的数据。我们可以通过两种方式访问 getter,一个是通过属性访问,另一个是通过方法访问:

  • 属性访问的方式为this.$store.getter.allProducts,对应的getter如下:
allProducts(state) {
// 返回本地中的数据
return state.products;
}
  • 方法访问的方式为this.$store.getter.productById(id),对应的getter如下:
productById: (state, getters) => id => {
//通过传入的id参数进行一系列操作并返回本地数据
return state.product;
}

我们可以看到Getter可以接受两个参数:stategettersstate就表示本地数据源;我们可以通过第二个参数getters获取到不同的getter属性。

定义 Vuex Getters

光说不练假把式,我们来手撸几个 getters。打开 src/store/index.js 文件,我们添加了一些需要用到的 action 属性、mutation 属性以及这一节的主角—— getters。代码如下:

src/store/index.js查看完整代码

state.showLoader = false;
state.products = products;
[tuture-add] },
[tuture-add] PRODUCT_BY_ID(state) {
[tuture-add] state.showLoader = true;
[tuture-add] },
[tuture-add] PRODUCT_BY_ID_SUCCESS(state, payload) {
[tuture-add] state.showLoader = false;
[tuture-add]
[tuture-add] const { product } = payload;
[tuture-add] state.product = product;
[tuture-add] }
[tuture-add] },
[tuture-add] getters: {
[tuture-add] allProducts(state) {
[tuture-add] return state.products;
[tuture-add] },
[tuture-add] productById: (state, getters) => id => {
[tuture-add] if (getters.allProducts.length > 0) {
[tuture-add] return getters.allProducts.filter(p => p._id == id)[0];
[tuture-add] } else {
[tuture-add] return state.product;
[tuture-add] }
}
},
actions: {
[tuture-omit]
commit('ALL_PRODUCTS')

axios.get(`${API_BASE}/products`).then(response => {
[tuture-del] console.log('response', response);
commit('ALL_PRODUCTS_SUCCESS', {
products: response.data,
});
})
[tuture-add] },
[tuture-add] productById({ commit }, payload) {
[tuture-add] commit('PRODUCT_BY_ID');
[tuture-add]
[tuture-add] const { productId } = payload;
[tuture-add] axios.get(`${API_BASE}/products/${productId}`).then(response => {
[tuture-add] commit('PRODUCT_BY_ID_SUCCESS', {
[tuture-add] product: response.data,
[tuture-add] });
[tuture-add] })
}
}
});

这里主要添加了三部分内容:

  • actions中添加了productById属性,当视图层通过指定id分发到类型为PRODUCT_BY_IDaction中,这里会进行异步操作从后端获取指定商品,并将该商品提交到对应类型的mutation中,就来到了下一步。
  • mutations中添加了PRODUCT_BY_IDPRODUCT_BY_ID_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() {
[tuture-del] return this.$store.state.products[0];
[tuture-add] 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">
[tuture-del] <p class="product__name">产品名称:{{product.name}}</p>
[tuture-del] <p class="product__description">介绍:{{product.description}}</p>
[tuture-del] <p class="product__price">价格:{{product.price}}</p>
[tuture-del] <p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
[tuture-del] <img :src="product.image" alt="" class="product__image">
[tuture-add] <router-link :to="'/detail/' + product._id" class="product-link">
[tuture-add] <p class="product__name">产品名称:{{product.name}}</p>
[tuture-add] <p class="product__description">介绍:{{product.description}}</p>
[tuture-add] <p class="product__price">价格:{{product.price}}</p>
[tuture-add] <p class="product.manufacturer">生产厂商:{{product.manufacturer.name}}</p>
[tuture-add] <img :src="product.image" alt="" class="product__image">
[tuture-add] </router-link>
<product-button :product="product"></product-button>
</div>
</div>
</template>

[tuture-add]<style>
[tuture-add].product {
[tuture-add] border-bottom: 1px solid black;
[tuture-add]}
[tuture-add]
[tuture-add].product__image {
[tuture-add] width: 100px;
[tuture-add] height: 100px;
[tuture-add]}
[tuture-add]</style>
[tuture-add]
<script>
import ProductButton from './ProductButton';
export default {

该组件经过修改之后实现了点击商品的任何一条信息,都会触发路由跳转到商品详情页,并将该商品id通过动态路由的方式传递到详情页。

在 ProductList 中使用 Getters

修改商品列表组件 src/components/products/ProductList.vue 文件,使用了 Vuex Getters 复用了本地数据获取逻辑,代码如下:

src/components/products/ProductList.vue查看完整代码
  </div>
</template>

[tuture-del]<style>
[tuture-del].product {
[tuture-del] border-bottom: 1px solid black;
[tuture-del]}
[tuture-del]
[tuture-del].product__image {
[tuture-del] width: 100px;
[tuture-del] height: 100px;
[tuture-del]}
[tuture-del]</style>
[tuture-del]
<script>
import ProductItem from './ProductItem.vue';
export default {
[tuture-omit]
computed: {
// a computed getter
products() {
[tuture-del] return this.$store.state.products;
[tuture-add] 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的对象作为载荷分发到类型为productByIdaction中,在action中进行异步操作从后端获取指定商品,然后提交到对应的mutation中进行本地状态修改,这已经使我们习惯的思路了。

配置 Detail 页面的路由

最后我们打开路由配置 src/router/index.js 文件,导入了 Detail 组件,并添加了对应的路由参数,代码如下:

src/router/index.js查看完整代码

import Home from '@/pages/Home';
import Cart from '@/pages/Cart';
[tuture-add]import Detail from '@/pages/Detail';

// Admin Components
import Index from '@/pages/admin/Index';
[tuture-omit]
name: 'Cart',
component: Cart,
},
[tuture-add] {
[tuture-add] path: '/detail/:id',
[tuture-add] name: 'Detail',
[tuture-add] component: Detail,
[tuture-add] }
],
});

又到了验收的环节,运行项目,点击单个商品,可以进入到商品详情页面,并且数据是完全一致的:

小结

这一节中我们学会了如何使用Vuex Getters来复用本地数据的获取逻辑:

  • 我们需要先在store实例中添加getters属性,并在getters属性中定义不同的属性或者方法。
  • 在这些不同类型的getter中,我们可以获取本地数据。
  • 我们可以通过属性访问和方法访问的方式来调用我们的getter
图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2019/10/17/6f96d15/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!