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

本文由图雀社区成员 Holy 使用 Tuture 实战教程写做工具 写做而成,欢迎加入图雀社区,一块儿创做精彩的免费技术实战教程,予力编程行业发展。javascript

组件化和逻辑复用能帮助写出简洁易懂的代码,随着应用越写越复杂,咱们有必要把视图层中重复的逻辑抽成组件,以求在多个页面中复用;同时对于 Vuex 端,Store 中的逻辑也会愈来愈臃肿,咱们有必要使用 Vuex 提供的 Getters 来复用本地数据获取逻辑。在这篇教程中,咱们将带领你抽出 Vue 组件简化页面逻辑,使用 Vuex Getters 复用本地数据获取逻辑。css

欢迎阅读《从零到部署:用 Vue 和 Express 实现迷你全栈电商应用》系列:html

若是你但愿直接从这一步开始,请运行如下命令:vue

git clone -b section-five https://github.com/tuture-dev/vue-online-shop-frontend.git
cd vue-online-shop-frontend
复制代码

本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~java

使用 Vue 组件简化页面逻辑

在前面的教程中,咱们已经学习了如何使用 Vuex 进行状态管理,如何使用 Action 获取远程数据以及如何使用 Mutation 修改本地状态,实现了用户修改客户端数据的同时,同步更新后端数据,而后更新本地数据,最后进行从新渲染。ios

这一节咱们将进一步经过 Vue 组件化的思想简化复杂的页面逻辑。git

实现 ProductButton 组件

咱们打开 src/components/products/ProductButton.vue 文件,它是用于操做商品在购物车中状态的按钮组件,代码以下:github

<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,建立从购物车移除按钮。编程

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

  • 当点击加入购物车按钮时触发 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中注册组件。
  • 最后在模板中使用该组件。

代码以下:

<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,代码以下:

<template>
  <div>
    <div class="products">
      <div class="container">
        This is ProductList
      </div>
      <template v-for="product in products">
        <product-item :product="product" :key="product._id"></product-item>
      </template>
    </div>
  </div>
</template>

<script> import ProductItem from './ProductItem.vue'; export default { name: 'product-list', created() { // ... }, computed: { // ... }, components: { 'product-item': ProductItem } } </script>
复制代码

这部分代码是将以前展现商品信息的逻辑代码封装到了子组件ProductItem中,而后导入并注册子组件ProductItem,再将子组件挂载到模板中。

能够看到,咱们经过this.$store.state.products从本地获取products数组,并返回给计算属性products。而后在模板中利用v-for遍历products数组,并将每一个product对象传给每一个子组件ProductItem,在每一个子组件中展现对应的商品信息。

重构 Cart 组件

最后,咱们重构一波购物车组件 src/pages/Cart.vue,也使用了子组件ProductItem简化了页面逻辑,修改代码以下:

<template>
  <div>
    <div class="title">
      <h1>{{msg}}</h1>
    </div>
    <template v-for="product in cart">
      <product-item :product="product" :key="product._id"></product-item>
    </template>
  </div>
</template>

<script> import ProductItem from '@/components/products/ProductItem.vue'; export default { name: 'home', data () { return { msg: 'Welcome to the Cart Page' } }, computed: { cart() { return this.$store.state.cart; } }, components: { '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。代码以下:

// ...

export default new Vuex.Store({
  strict: true,
  state: {
    // ...
  },
  mutations: {
    // ...
    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: {
    // ...
    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分发到类型为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 属性,从而获取本地商品,代码以下:

export default {
  computed: {
    product() {
      return this.$store.getters.allProducts[0];
    }
  }
}
复制代码

咱们经过this.$store.getters.allProducts属性访问的方式调用对应getter中的allProducts属性,并返回本地商品数组中的第一个商品。

建立 ProductDetail 组件

接着开始实现商品详情组件 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 包裹起来,实现商品信息可点击查看详情。代码以下:

<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>
复制代码

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

在 ProductList 中使用 Getters

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

// ...

<script> import ProductItem from './ProductItem.vue'; export default { name: 'product-list', created() { // ... }, computed: { // a computed getter products() { return this.$store.getters.allProducts; } }, components: { 'product-item': ProductItem } } </script>
复制代码

咱们在计算属性products中使用this.$store.getters.allProducts属性访问的方式调用getters中的allProducts属性,咱们也知道在对应的getter中获取到了本地中的products数组。

建立 Detail 页面组件

实现了 ProductDetail 子组件以后,咱们即可以搭建商品详情我页面组件 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 组件,并添加了对应的路由参数,代码以下:

// ...
import Detail from '@/pages/Detail';

export default new Router({
  routes: [
    // ...
    {
      path: '/detail/:id',
      name: 'Detail',
      component: Detail,
    }
  ],
});
复制代码

又到了验收的环节,运行项目,点击单个商品,能够进入到商品详情页面,而且数据是彻底一致的:

小结

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

  • 咱们须要先在store实例中添加getters属性,并在getters属性中定义不一样的属性或者方法。
  • 在这些不一样类型的getter中,咱们能够获取本地数据。
  • 咱们能够经过属性访问和方法访问的方式来调用咱们的getter

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦

相关文章
相关标签/搜索