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

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

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

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

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

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

git clone -b section-seven https://github.com/tuture-dev/vue-online-shop-frontend.git
cd vue-online-shop-frontend
本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️ 这篇文章点赞+Github仓库加星❤️哦~

代码重构

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

这里咱们简单介绍一下element-ui组件库(若是您了解,您能够跳过这部分):webpack

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

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

安装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文件中导入并注册依赖。git

你能够引入整个 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()注册组件库。github

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

重构导航栏

咱们首先来到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仓库加星❤️哦

相关文章
相关标签/搜索