從零到部署:用 Vue 和 Express 實現迷你全棧電商應用(七)

圖雀社群發表於2020-03-10

從零到部署:用 Vue 和 Express 實現迷你全棧電商應用(七)

本文由圖雀社群成員 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倉庫加星❤️哦

從零到部署:用 Vue 和 Express 實現迷你全棧電商應用(七)

相關文章