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

6,813 阅读23分钟

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

在之前的六篇教程中我们已经基本实现了迷你全栈电商应用,相信大家对于一个全栈应用的开发已经有了一个全面的认知。但是一个追求完美的工程师是不会吝啬他的艺术创造,仅仅实现应用的功能还不能满足用户的高需求,应用的界面效果也是提高用户体验的关键因素。因此本篇教程将基于element-ui组件库重构项目的前端代码,改善迷你电商应用的界面效果,提高用户的体验感。虽然我们可以轻松地引入现成的组件库,但是与之对应的数据处理也值得我们注意,那我会在引入组件库的同时带大家一起踩一踩element-ui给我们挖的坑,毕竟踩坑才能成长嘛。

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

如果你希望直接从这一步开始,请运行以下命令:

git clone -b section-seven https://github.com/tuture-dev/vue-online-shop-frontend.git
cd vue-online-shop-frontend

本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦~

代码重构

这一部分我们主要利用element-ui组件库重构之前的项目代码,实现极具美感的迷你电商应用。

这里我们简单介绍一下element-ui组件库(如果您了解,您可以跳过这部分):

Element UI 是一套采用 Vue 2.0 作为基础框架实现的组件库,一套为开发者、设计师和产品经理准备的基于 Vue 2.0的组件库,提供了配套设计资源,帮助网站快速成型。

Element UI文档提供了很多实例代码,一般情况下我们直接拷下示例代码稍微看看改改数据之类的就OK了。但是在某些场景下,我们可能又需要使用到一些特殊的功能和属性,而这些功能属性一般在官方提供的组件中都已经内置了,所以我们可以直接先从文档中寻找查看是否有属性或者方法等能够满足我们的需求,从而避免重复造轮子。

安装element-ui依赖

  1. npm 安装推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用。
npm i element-ui -S
  1. CDN引入目前可以通过 unpkg.com/element-ui 获取到最新版本的资源,在页面上引入 js 和 css 文件即可开始使用。
<!-- 引入样式 --><link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><!-- 引入组件库 --><script src="https://unpkg.com/element-ui/lib/index.js"></script>

我们建议使用 CDN 引入 Element 的用户在链接地址上锁定版本,以免将来 Element升级时受到非兼容性更新的影响。锁定版本的方法请查看 unpkg.com

导入依赖

依赖安装完成之后,我们需要在项目的main.js文件中导入并注册依赖。

你可以引入整个 Element,或是根据需要仅引入部分组件,这里我们引入了完整的 Element。

// 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';
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.config.productionTip = false;
Vue.component('ValidationProvider', ValidationProvider);
Vue.use(ElementUI);

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>',
});

main.js文件中我们首先导入了element-ui组件库,需要注意的是我们要单独引入样式文件;除此之外还要使用Vue.use()注册组件库。

至此,一个基于 Vue 和 Element 的开发环境已经搭建完毕,现在就可以愉快地使用组件库进行代码重构了。

重构导航栏

我们首先来到App组件,这里之前是采用普通的nav标签展示首页导航,显得甚是简陋,现在我们可以使用element-ui组件库提供的el-menu导航菜单组件重构导航栏,绝对酷炫。

<template>
  <div id="app">
    <el-menu
      class="menu"
      :default-active="activeIndex2"
      mode="horizontal"
      @select="handleSelect"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b">
      <el-menu-item index="1"><router-link to="/" tag="div">Home</router-link></el-menu-item>
      <el-submenu index="2">
        <template slot="title">Admin</template>
        <el-menu-item index="2-1"><router-link to="/admin" tag="div">查看商品</router-link></el-menu-item>
        <el-menu-item index="2-2"><router-link to="/admin/new" tag="div">添加商品</router-link></el-menu-item>
        <el-menu-item index="2-3"><router-link to="/admin/manufacturers" tag="div">查看生产商</router-link></el-menu-item>
        <el-menu-item index="2-4"><router-link to="/admin/manufacturers/new" tag="div">添加生产商</router-link></el-menu-item>
      </el-submenu>  
      <el-menu-item index="3"><router-link to="/cart" tag="div">Cart</router-link></el-menu-item>
    </el-menu>
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      activeIndex: '1',
      activeIndex2: '1'
    };
  },
  methods: {
    handleSelect(key, keyPath) {
        console.log(key, keyPath);
    }
  }

};
</script>

// ...

这里导航栏组件的使用相信大家都能看懂,这里我们只讲一下比较特殊的地方。我们不需要在意 data 属性以及 handleSelect方法,我们暂时用不到。这里一个特殊的地方就是 el-menu-item标签中的 tag 属性,我们将其值设置为 "div" 表示将该标签渲染为 "div" 盒子,如果不设置该属性则该标签默认渲染为 "a" 标签,导致标签包裹的内容带有下划线,因此这里 tag 属性的设置是为了去除下划线。

重构商品列表

重新修改 ProductList 组件,由于该组件中的子组件 ProductItem 进行了重构,因此这里也需要做一定的修改,看到后面 ProductItem 组件的重构您就会明白我们这里修改的用意。

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

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

这里之前是将从本地获取的 products 数组利用 v-forproduct 对象遍历到每个 ProductItem 组件中分别进行展示,但是我们这里取消了 v-for 遍历 products 数组,选择直接将 products 数组传入 ProductItem 组件中。请允许我先在这里卖个关子,继续往下看。

重新进入 ProductItem 组件进行修改,这里我们使用了 element-ui 组件库提供的 el-table 表格组件取代了原始标签来展示商品信息列表。

<template>
  <div class="products">
    <el-table
    class="table"
    :data="products"
    max-height="250">
      <el-table-column
        prop="name"
        label="产品名称"
        width="180">
      </el-table-column>
      <el-table-column
        prop="description"
        label="介绍"
        width="180">
      </el-table-column>
      <el-table-column
        prop="price"
        label="价格"
        width="180">
      </el-table-column>
      <el-table-column
        prop="manufacturer.name"
        label="生产厂商"
        width="180">
      </el-table-column>
      <!-- <el-table-column
        label="图片"
        width="200">
        <img :src="image" alt="" class="product__image">
      </el-table-column> -->
      <el-table-column
        label="操作"
        width="180">
        <template slot-scope="scope">
          <product-button :id="scope.row._id"></product-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
  // ...
</template>

// ...

<script>
import ProductButton from './ProductButton';
export default {
  name: 'product-item',
  props: ['products'],
  components: {
    'product-button': ProductButton,
  }
}
</script>

其实这里的修改相信大家都能看懂,我们就简单的做一下介绍。您可能还记得我们在上面卖的一个关子,为什么我们直接向该组件中传入了 products 数组而不是遍历的 product 对象?相信大家看了该组件的重构也能豁然开朗,那就是因为我们使用的 el-table 表格组件需要传入一个数组作为 data 属性,并将每个元素对象作为 prop 传入表格,按照对应的列名展示出来。

除此之外,相信大家也发现了最后一个 el-table-column 标签中并没有定义 prop 属性,这是因为最后一列单元格中放置的是按钮而不是商品信息,该按钮是用于对指定行对象进行指定操作,这里我们使用 scope.row 获取指定行对象并将其id传递给了 ProductButton 按钮组件。

通过 slot-scope 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据

再次进入 ProductButton 组件进行修改,这里我们使用了 element-ui 组件库提供的 el-button 按钮组件替代之前普通的 button 标签并修改了对应的数据处理。

<template>
  <div>
    <el-button
          v-if="isAdding"
          @click="addToCart"
          type="text"
          size="small">
          加入购物车
    </el-button>
    <el-button
          v-else
          @click="removeFromCart(id)"
          type="text"
          size="small">
          从购物车移除
    </el-button>
  </div>
</template>

<script>
export default {
  props: ['id'],
  computed: {
    product() {
      let product = this.$store.getters.allProducts.find(product => product._id === this.id)
      return product;
    },
    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>

这里我们首先简单地使用了 el-button 按钮组件,然后将从父组件获取的 product 对象修改为了 id,因为我们在 ProductItem 组件中传入的是指定对象的 id,因此我们在按钮组件中定义了计算属性 product,从本地获取指定 idproduct 对象。

我们已经迫不及待把项目跑起来了,看看我们的首页导航以及商品信息列表发生了怎样不可思议的改变:

在这里插入图片描述

重构商品信息功能

这部分内容主要是有关商品信息功能的重构,包括商品信息列表的展示、修改指定商品信息以及添加新商品,我们都使用了 element-ui 组件库提供的组件进行重构,提高用户操作商品信息时的交互体验。

首先我们进入 Products 组件,同样使用了 element-ui 组件库提供的 el-table 组件替换了之前普通表格来展示商品信息列表。

<template>
  <div class="products">
    <el-table
    class="table"
    :data="products">
      <el-table-column
        prop="name"
        label="名称"
        width="180">
      </el-table-column>
      <el-table-column
        prop="price"
        label="价格"
        width="180">
      </el-table-column>
      <el-table-column
        prop="manufacturer.name"
        label="制造商"
        width="180">
      </el-table-column>
      <el-table-column
        label="操作"
        width="200">
        <template slot-scope="scope">
          <el-button class="modify" type="text" size="small"><router-link :to="'/admin/edit/' + scope.row._id">修改</router-link></el-button>
          <el-button class="remove" @click="removeProduct(scope.row._id), deleteRow(scope.$index, products)" type="text" size="small">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    // ...
  </div>
</template>

// ...

细心的大家肯定已经发现了这里的表格有点似曾相识,没错,这里的表格与 ProductItem 组件中的表格非常相似,都是用来展示本地商品信息,但是两者的区别是对商品对象的操作,ProductItem 组件中的按钮组件是用于将商品添加或移出购物车,而该组件中的按钮组件是用于修改或删除商品对象。

这是我们重构之后的商品信息列表:

在这里插入图片描述

然后我们先对修改功能进行重构,再次进入 Edit 组件,我们在这里做了数据处理修改,目的是尝试解决商品信息表单无法编辑问题。

<template>
  <div>
    <div class="title">
      <h1>This is Admin/Edit</h1>
    </div>
    <product-form
      @save-product="updateProduct"
      :model="model"
      :manufacturers="manufacturers"
      :isEditing="true"
      ></product-form>
  </div>
</template>

<script>
import ProductForm from '@/components/products/ProductForm.vue';
export default {
  data: {
    model() {
      const product = this.$store.getters.productById(this.$route.params['id']);
      // 这里返回 product 的拷贝,是为了在修改 product 的拷贝之后,在保存之前不修改本地 Vuex stire 的 product 属性
      return { ...product, manufacturer: { ...product.manufacturer } };
    }
  },
  created() {
    const { name } = this.model;
    if (!name) {
      this.$store.dispatch('productById', {
        productId: this.$route.params['id']
      });
    }

    if (this.manufacturers.length === 0) {
      this.$store.dispatch('allManufacturers');
    }
  },
  computed: {
    manufacturers() {
      return this.$store.getters.allManufacturers;
    }
  },
  methods: {
    updateProduct(product) {
      this.$store.dispatch('updateProduct', {
        product,
      })
    }
  },
  components: {
    'product-form': ProductForm
  }
}
</script>

这里我们把定义的计算属性 model 修改为 data 属性,因为我们发现如果商品对象 model 作为计算属性传给子组件 ProductForm 进行信息展示时,无法进行表单编辑,大家可以运行起来尝试一下是否可以进行编辑。我们初始猜想是 el-form 表单组件中的表单数据对象 model 不能来自计算属性,否则无法进行编辑,因此我们首度尝试将该组件中的计算属性 model 放到 data 属性中。

再次进入 ProductForm 组件进行重构,这里我们使用了 element-ui 组件库提供的 el-form 表单组件替换之前的普通表单展示商品信息。

<template>
  <div class="productInfo">
    <el-form class="form" ref="form" :model="model" label-width="180px">
      <el-form-item label="Name">
        <el-input v-model="model.name"></el-input>
      </el-form-item>
      <el-form-item label="Price">
        <el-input v-model="model.price"></el-input>
      </el-form-item>
      <el-form-item label="Manufacturer ">
        <el-select v-model="model.manufacturer.name" clearable placeholder="请选择制造商">
          <el-option
            v-for="manufacturer in manufacturers"
            :key="manufacturer._id"
            :label="manufacturer.name"
            :value="manufacturer.name">
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Image ">
        <el-input v-model="model.image"></el-input>
      </el-form-item>
      <el-form-item label="Description ">
        <el-input type="textarea" v-model="model.description"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" @click="onSubmit">Update Product</el-button>
        <el-button v-else @click="onSubmit">Add Product</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>

<script>
export default {
  props: ['model', 'manufacturers', 'isEditing'],
  created() {
    console.log(this.model)
  },
  methods: {
    onSubmit() {
      this.$emit('save-product', this.model)
    }
  }
}
</script>
<style>
.productInfo {
  padding-top: 10px;
}
.form {
  margin: 0 auto;
  width: 500px;
}
.el-input__inner {
  height: 60px;
}
</style>

相信大家也能轻松的看懂 el-form 表单组件的使用,这里的 model 属性表示表单数据对象,我们可以使用 v-model 将表单数据对象中的信息双向绑定到相应的表单内组件上。特别提醒一下商品对象 model 中的 manufacturer 是一个制造商对象,包含制造商 idname 属性。

现在我们再进入 New 组件进行重构,当我们发现 Edit 组件中的问题之后,我们同样尝试将该组件中的计算属性 model 定义到 data 属性中。

<template>
  <product-form
    @save-product="addProduct"
    :model="model"
    :manufacturers="manufacturers"
  >
  </product-form>
</template>

<script>
import ProductForm from '@/components/products/ProductForm.vue';
export default {
  data() {
    return {
      model: {manufacturer:{name: ''}}
    }
  },
  created() {
    if (this.manufacturers.length === 0) {
      this.$store.dispatch('allManufacturers');
    }
  },
  computed: {
    manufacturers() {
      return this.$store.getters.allManufacturers;
    }
  },
  methods: {
    addProduct(model) {
      this.$store.dispatch('addProduct', {
        product: model,
      })
    },
  },
  components: {
  'product-form': ProductForm
  }
}
</script>

因为该组件是新建商品组件,因此我们定义的是一个空对象 model,但是我们需要给其一个默认初始形式 model: {manufacturer: {name: ' '}},防止在子组件表单中无法访问 name 属性导致报错。

现在我们添加或者修改商品信息的表单界面变成了这样:

在这里插入图片描述

重构制造商信息功能

制造商信息功能包括制造商信息展示,添加制造商以及修改制造商信息,同重构商品信息功能一样,我们也使用了 element-ui 组件库提供的组件进行重构,提高用户操作制造商信息时的交互体验。

首先我们进入 Manufacturers 组件进行重构,同 Products 组件类似,我们使用了 element-ui 组件库提供的 el-table 表格组件替换了之前普通的表格展示制造商信息列表。

<template>
  <div class="manufacturers">
    <el-table
    class="table"
    :data="manufacturers">
      <el-table-column
        prop="name"
        label="制造商"
        width="180">
      </el-table-column>
      <el-table-column
        label="操作"
        width="200">
        <template slot-scope="scope">
          <el-button class="modify" type="text" size="small"><router-link :to="'/admin/manufacturers/edit/' + scope.row._id">修改</router-link></el-button>
          <el-button class="remove" @click="removeManufacturer(scope.row._id), deleteRow(scope.$index, products)" type="text" size="small">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    // ...
  </div>
</template>

// ...
<script>
export default {
  created() {
    this.$store.dispatch('allManufacturers');
  },
  computed: {
    manufacturers() {
      return this.$store.getters.allManufacturers
    }
  },
  methods: {
    removeManufacturer(manufacturerId) {
      // 使用 JavaScript BOM 的 confirm 方法来询问用户是否删除此制造商
      const res = confirm('是否删除此制造商?');

      // 如果用户同意,那么就删除此制造商
      if (res) {
        this.$store.dispatch('removeManufacturer', {
          manufacturerId,
        })
      }
    }
  }
}
</script>

这是我们重构后的制造商信息列表:

在这里插入图片描述

再次进入 NewManufacturers 组件进行重构,同样的将定义的计算属性 model 放到 data 属性中。

<template>
  <manufacturer-form
    @save-manufacturer="addManufacturer"
    :model="model"
  >
  </manufacturer-form>
</template>

<script>
import ManufacturerForm from '@/components/ManufacturerForm.vue';
export default {
  data() {
    return {
      model: {}
    }
  },
  methods: {
    addManufacturer(model) {
      this.$store.dispatch('addManufacturer', {
        manufacturer: model,
      })
    },
  },
  components: {
  'manufacturer-form': ManufacturerForm
  }
}
</script>

然后进入子组件 ManufacturerForm 中进行重构,同 ProductForm 组件类似,使用 element-ui 组件库提供的 el-form 表单组件替换了之前普通的表单展示制造商信息。

<template>
  <div class="manufacturerInfo">
    <el-form class="form" ref="form" :model="model" label-width="180px">
      <el-form-item label="Name">
        <el-input v-model="model.name"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" @click="onSubmit">Update Manufacturer</el-button>
        <el-button v-else @click="onSubmit">Add Manufacturer</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>

<script>
export default {
  props: ['model', 'isEditing'],
  methods: {
    onSubmit() {
      this.$emit('save-manufacturer', this.model)
    }
  }
}
</script>
// ...

这是我们重构后用户添加或者修改制造商信息时的表单界面:

在这里插入图片描述

最后我们进入 Cart 组件进行重构,我们会发现该组件与 ProductList 组件极其相似,因为两者都复用了子组件 ProductItem,该组件是为了展示购物车商品信息列表。

<template>
  <div>
    <div class="title">
      <h1>{{msg}}</h1>
    </div>
    <product-item :products="cart"></product-item>
  </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>

这是重构后的购物车界面:

在这里插入图片描述

小结

这一节我们主要就是使用 element-ui 组件库进行项目代码的重构,实现了首页导航栏、商品信息功能、制造商信息功能以及购物车的页面升级,提高了用户的交互体验。但是这也造成了部分功能逻辑的瘫痪,我们在下一节会带大家一起去解决问题。

修复element-ui表单双向绑定问题

上一节我们使用了 element-ui 组件库完成项目代码重构,可是当我们把项目跑起来之后发现表单信息仍然无法编辑,说明我们之前的尝试失败。不过我们并没有灰心,而是选择继续尝试,这一节我们又尝试新方法来修复 element-ui 表单双向绑定问题。

大家遇到的问题应该是这样子:

在这里插入图片描述

重构 Edit 组件

我们首先进入 Edit 组件进行修复,这里我们主要恢复了原先的数据定义。

<template>
  <div>
    <div class="title">
      <h1>This is Admin/Edit</h1>
    </div>
    <product-form
      @save-product="updateProduct"
      :model="model"
      :manufacturers="manufacturers"
      :isEditing="true"
    ></product-form>
  </div>
</template>

<script>
import ProductForm from "@/components/products/ProductForm.vue";
export default {
  created() {
    const { name = "" } = this.modelData || {};

    if (!name) {
      this.$store.dispatch("productById", {
        productId: this.$route.params["id"]
      });
    }

    if (this.manufacturers.length === 0) {
      this.$store.dispatch("allManufacturers");
    }
  },
  computed: {
    manufacturers() {
      return this.$store.getters.allManufacturers;
    },
    model() {
      const product = this.$store.getters.productById(this.$route.params["id"]);
      const res = { ...product, manufacturer: { ...product.manufacturer } };

      return res;
    }
  },
  methods: {
    updateProduct(product) {
      this.$store.dispatch("updateProduct", {
        product
      });
    }
  },
  components: {
    "product-form": ProductForm
  }
};
</script>

我们又将替换到 data 属性中的 model 对象恢复到了计算属性中,用于缓存 model 对象信息,提高性能。我们打算在下面的 ProductForm 组件中进行修复表单无法编辑的问题。

重构 ProductForm 组件

再次进入 ProductForm 组件中,我们尝试另一种方法来修复表单无法编辑的问题。

<template>
  <div class="productInfo">
    <el-form class="form" ref="form" label-width="180px">
      <el-form-item label="Name">
        <el-input v-model="model.name"></el-input>
      </el-form-item>
      <el-form-item label="Price">
        <el-input v-model="model.price"></el-input>
      </el-form-item>
      <el-form-item label="Manufacturer ">
        <el-select
          v-model="modelData.manufacturer.name"
          clearable
          placeholder="请选择制造商"
        >
          <el-option
            v-for="manufacturer in manufacturers"
            :key="manufacturer._id"
            :label="manufacturer.name"
            :value="manufacturer.name"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Image ">
        <el-input v-model="model.image"></el-input>
      </el-form-item>
      <el-form-item label="Description ">
        <el-input type="textarea" v-model="model.description"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" @click="onSubmit"
          >Update Product</el-button
        >
        <el-button v-else @click="onSubmit">Add Product</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>
 // ...
<script>
export default {
  data() {
    return {
      modelData: { manufacturer: { name: "" } }
    };
  },
  props: ["model", "manufacturers", "isEditing"],
  created() {
    const product = this.model;

    this.modelData = { ...product, manufacturer: { ...product.manufacturer } };
  },
  watch: {
    model(val, oldVal) {
      this.modelData = val;
    }
  },
  methods: {
    onSubmit() {
      this.$emit("save-product", this.modelData);
    }
  }
};
</script>
// ...

这里我们没有直接使用从父组件获取的 model 对象作为表单数据对象,而是在该组件中自定义一个 modelData 对象,并使用默认初始形式。然后在组件刚被创建时,先将从父组件获取的 model 对象赋值给一个临时变量 product,然后将 product 浅拷贝到 modelData 对象中,这样就避免了表单数据对象使用计算属性。但是这仅仅完成了一半的工作,因为我们需要实现双向绑定的效果,因此我们需要监测表单组件的变化,通过使用 watch 方法监测用户的输入,然后将新数据储存到 modelData 对象中,这样就成功实现了双向绑定,而且表单也能随意进行编辑。

但是这里我们仅仅在下拉菜单中使用了 modelData 对象进行尝试,因此后面我们会在整个表单内组件使用该对象。

小结

这一节我们主要带大家修复了 element-ui 表单双向绑定问题,通过自定义 modelData 对象以及 watch 方法监测表单数据的改变实现了表单数据的双向绑定,并且解决了表单无法编辑的问题。但是仅仅在下拉菜单中进行尝试,后面我们会重构整个商品信息表单组件。

完善表单双向绑定问题

重构 ProductForm 组件

再次进入 ProductForm 组件,我们需要完善上一节遗留的问题,也就是仅仅对商品信息表单中的下拉菜单进行了尝试,并且尝试成功,因此这一节我们需要将 modelData 对象导入所有表单内组件中,解决其他表单内组件无法编辑的问题。

<template>
  <div class="productInfo">
    <el-form class="form" ref="form" label-width="180px">
      <el-form-item label="Name">
        <el-input v-model="modelData.name"></el-input>
      </el-form-item>
      <el-form-item label="Price">
        <el-input v-model="modelData.price"></el-input>
      </el-form-item>
      <el-form-item label="Manufacturer ">
        <el-select
          v-model="modelData.manufacturer.name"
          clearable
          placeholder="请选择制造商"
        >
          <el-option
            v-for="manufacturer in manufacturers"
            :key="manufacturer._id"
            :label="manufacturer.name"
            :value="manufacturer.name"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Image ">
        <el-input v-model="modelData.image"></el-input>
      </el-form-item>
      <el-form-item label="Description ">
        <el-input type="textarea" v-model="modelData.description"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" @click="onSubmit"
          >Update Product</el-button
        >
        <el-button v-else @click="onSubmit">Add Product</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>
 // ...
<script>
export default {
  data() {
    return {
      modelData: { manufacturer: { name: "" } }
    };
  },
  props: ["model", "manufacturers", "isEditing"],
  created() {
    const product = this.model;
 // ...
    this.modelData = { ...product, manufacturer: { ...product.manufacturer } };
  },
  watch: {
    model(val, oldVal) {
      this.modelData = val;
    }
  },
  methods: {
    onSubmit() {
      this.$emit("save-product", this.modelData);
    }
  }
};
</script>
// ...

小结

这一节我们带大家补充了上一节遗留的问题,也就是复制下拉菜单中的尝试到其他表单内组件中,保证整个表单组件都能够顺利地实现编辑功能。

解决操作商品信息表单报错问题

重构 ProductForm 组件

相信大家在对商品信息表单进行添加或者修改操作时,控制台会出现 id 属性未定义的错误,我们首先应该进入报错的组件中进行调试,大家应该都看到了报错信息出现在 ProductForm 组件中,因此我们需要进入 ProductForm 组件进行调试。

<template>
  <div class="productInfo">
    <el-form class="form" ref="form" label-width="180px">
      <el-form-item label="Name">
        <el-input v-model="modelData.name"></el-input>
      </el-form-item>
      <el-form-item label="Price">
        <el-input v-model="modelData.price"></el-input>
      </el-form-item>
      <el-form-item label="Manufacturer ">
        <el-select
          v-model="modelData.manufacturer.name"
          clearable
          placeholder="请选择制造商"
        >
          <el-option
            v-for="manufacturer in manufacturers"
            :key="manufacturer._id"
            :label="manufacturer.name"
            :value="manufacturer.name"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Image ">
        <el-input v-model="modelData.image"></el-input>
      </el-form-item>
      <el-form-item label="Description ">
        <el-input type="textarea" v-model="modelData.description"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" @click="onSubmit"
          >Update Product</el-button
        >
        <el-button v-else @click="onSubmit">Add Product</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>
 // ...
<script>
export default {
  data() {
    return {
      modelData: { manufacturer: { name: "" } }
    };
  },
  props: ["model", "manufacturers", "isEditing"],
  created() {
    const product = this.model;
 // ...
    this.modelData = { ...product, manufacturer: { ...product.manufacturer } };
  },
  watch: {
    model(val, oldVal) {
      this.modelData = val;
    }
  },
  methods: {
    onSubmit() {
      const manufacturer = this.manufacturers.find(item => item.name === this.modelData.manufacturer.name);
      this.modelData.manufacturer = manufacturer;
      this.$emit("save-product", this.modelData);
    }
  }
};
</script>
// ...

首先大家应该清楚商品对象中还包含了相应的制造商对象,并且制造商对象中包含了 id 属性和 name 属性。但是我们应该可以发现商品信息表单中的下拉菜单双向绑定的是商品对象中的制造商对象的 name 属性,因此在 watch 方法中存储到 modelData 对象中的制造商对象也只有 name 属性,但是后端数据库要求制造商对象必须也要有 id 属性,这就是我们点击更新商品信息出现报错的原因。

这里我们使用了本地制造商数组的 find 方法,检索到了对应 name 的制造商对象并将其覆盖掉 modelData 对象中的制造商对象,这样我们的 modelData 对象中的制造商对象就是一个符合后端数据库要求的对象了。

小结

这一节我们带大家分析并尝试解决了操作商品信息表单出现 id 属性未定义的问题。

添加动态效果及消息提示

我们注意到了当用户进行添加或修改商品或者制造商信息时,难免会遇到更新延迟的问题,这个时候如果页面毫无反馈会显得些许尴尬,因此我们认为只要用户进行添加或者修改操作,在后端数据同步完成之前我们为页面添加一个动态加载的效果,给用户一个反馈表示数据正在处理中,请耐心等待;并且在后端同步完成之后为页面添加一个消息提示,给用户一个反馈表示数据处理成功,这样就避免了尴尬的场面,提高了用户的交互体验。

实现loading动态加载效果

再次进入 ManufactureForm 组件,实现用户在添加或者修改制造商信息时且当后端数据同步完成之前,页面出现 loading动态加载效果。

<template>
  <div class="manufacturerInfo">
    <el-form 
    class="form" 
    ref="form" 
    label-width="180px"
    v-loading="loading"
    element-loading-text="拼命加载中"
    element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.8)">
      <el-form-item label="Name">
        <el-input v-model="manufacturerData.name"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" native-type="submit" @click="onSubmit">Update Manufacturer</el-button>
        <el-button v-else @click="onSubmit">Add Manufacturer</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>
 // ...
<script>
export default {
  props: ['model', 'isEditing'],
  data() {
    return {
      manufacturerData: {name: ''}
    }
  },
  created() {
    this.manufacturerData = this.model
  },
  watch: {
    model(val, oldVal) {
      this.manufacturerData = val;
    }
  },
  computed: {
    loading() {
      return this.$store.state.showLoader
    }
  },
  methods: {
    onSubmit() {
      this.$emit('save-manufacturer', this.manufacturerData);
    }
  }
}
</script>
// ...

首先我们在该组件中使用了 element-ui 组件库提供的自定义指令 v-loading,通过判断 loading 为true还是false来决定是否实现动态加载效果。这里我们通过获取本地状态中的 showLoader 属性作为 loading 属性值,因为在用户刚进行添加或修改操作时,向后端发起数据请求,此时本地状态中的 showLoader 属性值为true,当成功获取到了数据响应之后,也就是后端数据同步完成,此时 showLoader 属性值为false,这样就实现了在指定时间显示动态加载效果;除此之外,我们还按照 ProductForm 组件补充修改了数据处理,解决制造商表单组件编辑问题。

同样进入 ProductForm 组件进行修改,实现用户在添加或修改商品信息时,且当后端数据同步完成之前,页面出现 loading 动态加载效果。

<template>
  <div class="productInfo">
    <el-form 
    class="form" 
    ref="form" 
    label-width="180px"
    v-loading="loading"
    element-loading-text="拼命加载中"
    element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.8)">
      <el-form-item label="Name">
        <el-input v-model="modelData.name"></el-input>
      </el-form-item>
      <el-form-item label="Price">
        <el-input v-model="modelData.price"></el-input>
      </el-form-item>
      <el-form-item label="Manufacturer ">
        <el-select
          v-model="modelData.manufacturer.name"
          clearable
          placeholder="请选择制造商"
        >
          <el-option
            v-for="manufacturer in manufacturers"
            :key="manufacturer._id"
            :label="manufacturer.name"
            :value="manufacturer.name"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="Image ">
        <el-input v-model="modelData.image"></el-input>
      </el-form-item>
      <el-form-item label="Description ">
        <el-input type="textarea" v-model="modelData.description"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button v-if="isEditing" type="primary" native-type="submit" @click="onSubmit"
          >Update Product</el-button
        >
        <el-button v-else @click="onSubmit">Add Product</el-button>
      </el-form-item>
    </el-form>
  </div>
  // ...
</template>
 // ...
<script>
export default {
  data() {
    return {
      modelData: { manufacturer: { name: "" } }
    };
  },
  props: ["model", "manufacturers", "isEditing"],
  created() {
    const product = this.model;
    this.modelData = { ...product, manufacturer: { ...product.manufacturer } };
  },
  watch: {
    model(val, oldVal) {
      this.modelData = val;
    }
  },
  computed: {
    loading() {
      return this.$store.state.showLoader
    }
  },
  methods: {
    onSubmit() {
      // 由于表单中只绑定了modelData.manufacturer.name,
      // 缺少manufacturer._id,但是后端需要manufacturer整个对象,
      // 所以需要将manufacturers中对应的manufacturer找出并覆盖到modelData中
      const manufacturer = this.manufacturers.find(item => item.name === this.modelData.manufacturer.name);
      this.modelData.manufacturer = manufacturer;

      this.$emit("save-product", this.modelData);
    }
  }
};
</script>
// ...

实现消息提示功能

首先进入 actions.js 文件进行修改,由于发送网络请求数据的操作在该文件中执行,因此我们可以将消息提示功能添加到这里。

import axios from 'axios';

import {
  // ...
} from './mutation-types';
import { Message } from 'element-ui';
 // ...
const API_BASE = 'http://localhost:3000/api/v1';
 // ...
export const productActions = {
  // ...
  removeProduct({ commit }, payload) {
    commit(REMOVE_PRODUCT);

    const { productId } = payload;
    axios.delete(`${API_BASE}/products/${productId}`)
    .then(() => {
      // 返回 productId,用于删除本地对应的商品
      commit(REMOVE_PRODUCT_SUCCESS, {
        productId,
      });
      Message({
        message: '恭喜你,商品删除成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,商品删除失败!');
    })
  },
  updateProduct({ commit }, payload) {
    commit(UPDATE_PRODUCT);

    const { product } = payload;
    axios.put(`${API_BASE}/products/${product._id}`, product)
    .then(response => {
      commit(UPDATE_PRODUCT_SUCCESS, {
        product: response.data,
      });
      Message({
        message: '恭喜你,商品更新成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,商品更新失败!');
    })
  },
  addProduct({ commit }, payload) {
    commit(ADD_PRODUCT);

    const { product } = payload;
    axios.post(`${API_BASE}/products`, product)
    .then(response => {
      commit(ADD_PRODUCT_SUCCESS, {
        product: response.data,
      })
      Message({
        message: '恭喜你,商品添加成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,商品添加失败!');
    })
  }
};
 // ...
export const manufacturerActions = {
  // ...
  removeManufacturer({ commit }, payload) {
    commit(REMOVE_MANUFACTURER);

    const { manufacturerId } = payload;
    axios.delete(`${API_BASE}/manufacturers/${manufacturerId}`)
    .then(() => {
      // 返回 manufacturerId,用于删除本地对应的制造商
      commit(REMOVE_MANUFACTURER_SUCCESS, {
        manufacturerId,
      });
      Message({
        message: '恭喜你,制造商删除成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,制造商删除失败!');
    })
  },
  updateManufacturer({ commit }, payload) {
    commit(UPDATE_MANUFACTURER);

    const { manufacturer } = payload;
    axios.put(`${API_BASE}/manufacturers/${manufacturer._id}`, manufacturer)
    .then(response => {
      commit(UPDATE_MANUFACTURER_SUCCESS, {
        manufacturer: response.data,
      });
      Message({
        message: '恭喜你,制造商更新成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,制造商更新失败!');
    })
  },
  addManufacturer({ commit }, payload) {
    commit(ADD_MANUFACTURER);
    const { manufacturer } = payload;
    axios.post(`${API_BASE}/manufacturers`, manufacturer)
    .then(response => {
      commit(ADD_MANUFACTURER_SUCCESS, {
        manufacturer: response.data,
      });
      Message({
        message: '恭喜你,制造商添加成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,制造商添加失败!');
    })
  }
}

这里我们首先导入了 element-ui 组件库提供的 Message 消息提示组件,并在网络请求成功之后添加成功消息提醒,在请求失败之后添加失败消息提醒。

然后进入 mutations.js 文件进行修改,这里的修改是为本地购物车数据处理添加消息提示。

import {
  // ...
} from './mutation-types';
import { Message } from 'element-ui';
 // ...
export const cartMutations = {
  [ADD_TO_CART](state, payload) {
    const { product } = payload;
    state.cart.push(product);
    Message({
      message: '恭喜你,成功加入购物车!',
      type: 'success'
    })
  },
  [REMOVE_FROM_CART](state, payload) {
    const { productId } = payload
    state.cart = state.cart.filter(product => product._id !== productId)
    Message({
      message: '恭喜你,成功移除购物车!',
      type: 'success'
    })
  },
};
 // ...

同样的我们首先需要导入 element-ui 组件库提供的 Message 消息提示组件,当用户进行添加或者移除购物车操作时,执行操作成功消息提醒。

我们在进行添加、删除、修改以及加入或移除购物车操作时都会得到这样的反馈:

在这里插入图片描述

小结

这一节我们主要做的几点工作:

  • 为表单组件添加 element-ui 组件库提供的 v-loading 指令,实现动态加载效果;
  • 添加了 element-ui 组件库提供的 Message 消息提示组件,实现用户操作表单信息后得到的反馈消息提示。

解决表单信息修改后无法显示最新

重构到这里相信有些朋友已经迫不及待地将项目跑起来了,但是总是事与愿违,但是大家丝毫不用方,只要您跟着我们一步一步脚踏实地地去分析问题,那么什么问题都会迎刃而解了。现在的问题就是当用户对商品或者制造商进行信息修改时,点击更新之后表单却又显示了旧信息。

大家遇到的状况应该是这样:

在这里插入图片描述
在这里插入图片描述

数据出现问题我们应该根据 vue 的单向数据流原则进行调试,当用户对表单信息进行更新时,应该首先向后端发起网络请求,然后将最新数据同步到本地状态中进行展示,因此我们来到 actions.js 文件中进行调试。

提交最新数据

再次进入 actions.js 文件进行调试,我们可以大胆的猜测网络请求成功之后提交到 mutations.js 文件中的数据对象不是用户修改的最新数据。

import axios from 'axios';
 // ...
import {
  // ...
} from './mutation-types';
import { Message } from 'element-ui';
 // ...
const API_BASE = 'http://localhost:3000/api/v1';
 // ...
export const productActions = {
  // ...
  updateProduct({ commit }, payload) {
    commit(UPDATE_PRODUCT);
 // ...
    const { product } = payload;
    axios.put(`${API_BASE}/products/${product._id}`, product)
    .then(response => {
      commit(UPDATE_PRODUCT_SUCCESS, {
        product: product,
      });
      Message({
        message: '恭喜你,商品更新成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,商品更新失败!');
    })
  },
  // ...
};
 // ...
export const manufacturerActions = {
  // ...
  updateManufacturer({ commit }, payload) {
    commit(UPDATE_MANUFACTURER);
 // ...
    const { manufacturer } = payload;
    axios.put(`${API_BASE}/manufacturers/${manufacturer._id}`, manufacturer)
    .then(response => {
      commit(UPDATE_MANUFACTURER_SUCCESS, {
        manufacturer: manufacturer,
      });
      Message({
        message: '恭喜你,制造商更新成功!',
        type: 'success'
      })
    })
    .catch(() => {
      Message.error('不好意思,制造商更新失败!');
    })
  },
  // ...
}

我们在这里将网络请求成功时提交的载荷修改为了最新数据对象,然后提交到对应类型的 mutation 中进行本地数据的更新。

将最新数据同步到本地

紧接着我们需要进入 mutations.js 文件,将其获取到的最新数据同步到本地状态中。

import {
  // ...
} from './mutation-types';
import { Message } from 'element-ui';
 // ...
export const productMutations = {
  // ...
  [UPDATE_PRODUCT_SUCCESS](state, payload) {
    state.showLoader = false;

    const { product: newProduct } = payload;
    // ...
    state.products = state.products.map( product => {
      if (product._id === newProduct._id) {
        return newProduct;
      }
      return product;
    });

    state.product = newProduct;
  },
  // ...
  [UPDATE_MANUFACTURER_SUCCESS](state, payload) {
    state.showLoader = false;
 // ...
    const { manufacturer: newManufacturer } = payload;
    state.manufacturers = state.manufacturers.map(manufacturer => {
      if (manufacturer._id === newManufacturer._id) {
        return newManufacturer;
      }
      return manufacturer;
    });

    state.manufacturer = newManufacturer;
  },
  // ...
}

小结

这一节我们主要带大家分析并尝试解决了表单信息修改后无法显示最新信息的问题。

本篇教程为大家呈现了在实际开发过程中,使用element-ui组件库对电商应用前端代码进行重构所遇到的一些问题,并且我们一步一步地带大家去分析及尝试解决问题。希望这篇教程让大家对element-ui组件库的使用需要注意的问题有一个大致的了解,重要的是分析和尝试解决问题的能力。好了,到这里我们的项目基本上可以愉快地跑起来了,用户的交互体验感明显得到了改善。

如果大家在项目运行中遇到了其他问题,希望大家不要吝啬自己的质疑,多多和我们沟通哦!

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

本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦