写点什么

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

用户头像
图雀社区
关注
发布于: 2020 年 06 月 04 日
从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(五)

组件化和逻辑复用能帮助写出简洁易懂的代码,随着应用越写越复杂,我们有必要把视图层中重复的逻辑抽成组件,以求在多个页面中复用;同时对于 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 方法遍历数组,判断当前商品是否在购物车中,如果不在则 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">        <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 组件来简化页面逻辑:


  • 首先我们需要通过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; }, 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 分发到类型为PRODUCTBYIDaction中,这里会进行异步操作从后端获取指定商品,并将该商品提交到对应类型的mutation中,就来到了下一步。

  • mutations中添加了PRODUCT_BY_IDPRODUCTBYID_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 的对象作为载荷分发到类型为productByIdaction中,在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 Componentsimport Index from '@/pages/admin/Index';// ... name: 'Cart', component: Cart, }, { path: '/detail/:id', name: 'Detail', component: Detail, } ],});
复制代码


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



小结


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


  • 我们需要先在store实例中添加getters属性,并在getters属性中定义不同的属性或者方法。

  • 在这些不同类型的getter中,我们可以获取本地数据。

  • 我们可以通过属性访问和方法访问的方式来调用我们的getter


发布于: 2020 年 06 月 04 日阅读数: 56
用户头像

图雀社区

关注

一只图雀 @ 公众号「图雀社区」 2019.12.14 加入

汇集精彩的免费实战教程,主站 https://tuture.co

评论

发布
暂无评论
从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(五)