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

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

元件化和邏輯複用能幫助寫出簡潔易懂的程式碼,隨著應用越寫越複雜,我們有必要把檢視層中重複的邏輯抽成元件,以求在多個頁面中複用;同時對於 Vuex 端,Store 中的邏輯也會越來越臃腫,我們有必要使用 Vuex 提供的 Getters 來複用本地資料獲取邏輯。在這篇教程中,我們將帶領你抽出 Vue 元件簡化頁面邏輯,使用 Vuex Getters 複用本地資料獲取邏輯。

歡迎閱讀《從零到部署:用 Vue 和 Express 實現迷你全棧電商應用》系列:

如果您覺得我們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵我們寫出更好的教程?

使用 Vue 元件簡化頁面邏輯

在前面的教程中,我們已經學習瞭如何使用 Vuex 進行狀態管理,如何使用 Action 獲取遠端資料以及如何使用 Mutation 修改本地狀態,實現了使用者修改客戶端資料的同時,同步更新後端資料,然後更新本地資料,最後進行重新渲染。

這一節我們將進一步通過 Vue 元件化的思想簡化複雜的頁面邏輯。

實現 ProductButton 元件

我們開啟 src/components/products/ProductButton.vue 檔案,它是用於操作商品在購物車中狀態的按鈕元件,程式碼如下:

<template>
  <div>
    <button v-if="isAdding" class="button" @click="addToCart">加入購物車</button>
    <button v-else class="button" @click="removeFromCart(product._id)">從購物車移除</button>
  </div>
</template>

<script>
export default {
  props: ['product'],
  computed: {
    isAdding() {
      let isAdding = true;
      this.cart.map(product => {
        if (product._id === this.product._id) {
          isAdding = false;
        }
      });

      return isAdding;
    },
    cart() {
      return this.$store.state.cart;
    }
  },
  methods: {
    addToCart() {
      this.$store.commit('ADD_TO_CART', {
        product: this.product,
      })
    },
    removeFromCart(productId) {
      this.$store.commit('REMOVE_FROM_CART', {
        productId,
      })
    }
  }
}
</script>
複製程式碼

該元件通過 v-if 判斷 isAdding 是否為 true 來決定建立加入購物車按鈕還是從購物車移除按鈕。cart 陣列是通過 this.$store.state.cart 從本地獲取的。在 isAdding 中我們先令其為 true,然後通過 cart 陣列的 map 方法遍歷陣列,判斷當前商品是否在購物車中,如果不在則 isAddingtrue,建立加入購物車按鈕;如果在則 isAddingfalse,建立從購物車移除按鈕。

對應的兩個按鈕新增了兩個點選事件:addToCartremoveFromCart

  • 當點選加入購物車按鈕時觸發 addToCart,我們通過 this.$store.commit 的方式將包含當前商品的物件作為載荷直接提交到型別為 ADD_TO_CARTmutation 中,將該商品新增到本地購物車中。
  • 當點選從購物車移除按鈕時觸發removeFromCart,我們也是通過this.$store.commit的方式將包含當前商品id的物件作為載荷直接提交到型別為REMOVE_FROM_CARTmutation中,將該商品從本地購物車中移除。

實現 ProductItem 元件

src/components/products/ProductItem.vue檔案為商品資訊元件,用來展示商品詳細資訊,並且註冊了上面講的按鈕元件,改變商品在購物車中的狀態,除此之外我們還使用了之前建立好的ProductButton元件,實現對商品在購物車中的狀態進行修改。

  • 首先通過import ProductButton from './ProductButton'匯入建立好的ProductButton元件。
  • 然後在components中註冊元件。
  • 最後在模板中使用該元件。

程式碼如下:

<template>
  <div>
    <div class="product">
      <p class="product__name">產品名稱:{{product.name}}</p>
      <p class="product__description">介紹:{{product.description}}</p>
      <p class="product__price">價格:{{product.price}}</p>
      <p class="product.manufacturer">生產廠商:{{product.manufacturer.name}}</p>
      <img :src="product.image" alt="" class="product__image">
      <product-button :product="product"></product-button>
    </div>
  </div>
</template>

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

可以看到,我們將父元件傳入的product物件展示到模板中,並將該product物件傳到子元件ProductButton中。

重構 ProductList 元件

有了 ProductButton 和 ProductItem,我們便可以來重構之前略顯臃腫的 ProductList 元件了,修改 src/components/products/ProductList.vue,程式碼如下:

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

<script>
import ProductItem from './ProductItem.vue';
export default {
  name: 'product-list',
  created() {
    // ...
      return this.$store.state.products;
    }
  },
  components: {
    'product-item': ProductItem
  }
}
</script>
複製程式碼

這部分程式碼是將之前展示商品資訊的邏輯程式碼封裝到了子元件ProductItem中,然後匯入並註冊子元件ProductItem,再將子元件掛載到模板中。

可以看到,我們通過this.$store.state.products從本地獲取products陣列,並返回給計算屬性products。然後在模板中利用v-for遍歷products陣列,並將每個product物件傳給每個子元件ProductItem,在每個子元件中展示對應的商品資訊。

重構 Cart 元件

最後,我們重構一波購物車元件 src/pages/Cart.vue,也使用了子元件ProductItem簡化了頁面邏輯,修改程式碼如下:

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

<script>
import ProductItem from '@/components/products/ProductItem.vue';
  export default {
    name: 'home',
    data () {
      // ...
        return this.$store.state.cart;
      }
    },
    components: {
      'product-item': ProductItem
    }
  }
</script>
複製程式碼

這裡也是首先匯入並註冊子元件ProductItem,然後在模板中掛載子元件。通過this.$store.state.cart的方式從本地獲取購物車陣列,並返回給計算屬性cart。在模板中通過v-for遍歷購物車陣列,並將購物車中每個商品物件傳給對應的子元件ProductItem,通過子元件來展示對應的商品資訊。

把專案開起來,檢視商品列表,可以看到每個商品下面都增加了“新增到購物車”按鈕:

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

購物車中,也有了“移出購物車”按鈕:

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

盡情地買買買吧!

小結

這一節我們學習瞭如何使用 Vue 元件來簡化頁面邏輯:

  • 首先我們需要通過import的方式匯入子元件。
  • 然後在components中註冊子元件。
  • 最後將子元件掛載到模板中,並將需要子元件展示的資料傳給子元件。

使用 Vuex Getters 複用本地資料獲取邏輯

在這一節中,我們將實現這個電商應用的商品詳情頁面。商品詳情和之前商品列表在資料獲取上的邏輯是非常一致的,能不能不寫重複的程式碼呢?答案是肯定的。之前我們使用 Vuex 進行狀態管理是通過 this.$store.state 的方式獲取本地資料,而在這一節我們使用 Vuex Getters來複用本地資料的獲取邏輯。

Vuex允許我們在 store 中定義“getter”(可以認為是 store的計算屬性)。就像計算屬性一樣,getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。

Getter也是定義在 Vuex Store 的 getter 屬性中的一系列方法,用於獲取本地狀態中的資料。我們可以通過兩種方式訪問 getter,一個是通過屬性訪問,另一個是通過方法訪問:

  • 屬性訪問的方式為this.$store.getter.allProducts,對應的getter如下:
allProducts(state) {
    // 返回本地中的資料
    return state.products;
}
複製程式碼
  • 方法訪問的方式為this.$store.getter.productById(id),對應的getter如下:
productById: (state, getters) => id => {
      //通過傳入的id引數進行一系列操作並返回本地資料
      return state.product;
  }
複製程式碼

我們可以看到Getter可以接受兩個引數:stategettersstate就表示本地資料來源;我們可以通過第二個引數getters獲取到不同的getter屬性。

定義 Vuex Getters

光說不練假把式,我們來手擼幾個 getters。開啟 src/store/index.js 檔案,我們新增了一些需要用到的 action 屬性、mutation 屬性以及這一節的主角—— getters。程式碼如下:

// ...

      state.showLoader = false;
      state.products = products;
    },
    PRODUCT_BY_ID(state) {
      state.showLoader = true;
    },
    PRODUCT_BY_ID_SUCCESS(state, payload) {
      state.showLoader = false;

      const { product } = payload;
      state.product = product;
    }
  },
  getters: {
    allProducts(state) {
      return state.products;
    },
    productById: (state, getters) => id => {
      if (getters.allProducts.length > 0) {
        return getters.allProducts.filter(p => p._id == id)[0];
      } else {
        return state.product;
      }
    }
  },
  actions: {
    // ...
      commit('ALL_PRODUCTS')

      axios.get(`${API_BASE}/products`).then(response => {
        commit('ALL_PRODUCTS_SUCCESS', {
          products: response.data,
        });
      })
    },
    productById({ commit }, payload) {
      commit('PRODUCT_BY_ID');

      const { productId } = payload;
      axios.get(`${API_BASE}/products/${productId}`).then(response => {
        commit('PRODUCT_BY_ID_SUCCESS', {
          product: response.data,
        });
      })
    }
  }
});
複製程式碼

這裡主要新增了三部分內容:

  • actions中新增了productById屬性,當檢視層通過指定id分發到型別為PRODUCT_BY_IDaction中,這裡會進行非同步操作從後端獲取指定商品,並將該商品提交到對應型別的mutation中,就來到了下一步。
  • mutations中新增了PRODUCT_BY_IDPRODUCT_BY_ID_SUCCESS屬性,響應指定型別提交的事件,將提交過來的商品儲存到本地。
  • 新增了getters並在getters中新增了allProducts屬性和productById方法,用於獲取本地資料。在allProducts中獲取本地中所有的商品;在productById通過傳入的id查詢本地商品中是否存在該商品,如果存在則返回該商品,如果不存在則返回空物件。

在後臺 Products 元件中使用 Getters

我們先通過一個簡單的例子演示如果使用 Vuex Getters。開啟後臺商品元件,src/pages/admin/Products.vue,我們通過屬性訪問的方式呼叫對應的 getter 屬性,從而獲取本地商品,程式碼如下:

// ...
export default {
  computed: {
    product() {
      return this.$store.getters.allProducts[0];
    }
  }
}
// ...
複製程式碼

我們通過this.$store.getters.allProducts屬性訪問的方式呼叫對應getter中的allProducts屬性,並返回本地商品陣列中的第一個商品。

建立 ProductDetail 元件

接著開始實現商品詳情元件 src/components/products/ProductDetail.vue,程式碼如下:

<template>
  <div class="product-details">
    <div class="product-details__image">
      <img :src="product.image" alt="" class="image">
    </div>
    <div class="product-details__info">
      <div class="product-details__description">
        <small>{{product.manufacturer.name}}</small>
        <h3>{{product.name}}</h3>
        <p>
          {{product.description}}
        </p>
      </div>
      <div class="product-details__price-cart">
        <p>{{product.price}}</p>
        <product-button :product="product"></product-button>
      </div>
    </div>
  </div>
</template>

<style>
  .product-details__image .image {
    width: 100px;
    height: 100px;
  }
</style>

<script>
import ProductButton from './ProductButton';
export default {
  props: ['product'],
  components: {
    'product-button': ProductButton
  }
}
</script>
複製程式碼

該元件將父元件傳入的product物件展示在了模板中,並複用了ProductButton元件。

在 ProductItem 元件中新增連結

有了商品詳情,我們還需要進入詳情的連結。再次進入 src/components/products/ProductItem.vue 檔案中,我們對其進行了修改,將模板中的商品資訊用 Vue 原生元件 router-link 包裹起來,實現商品資訊可點選檢視詳情。程式碼如下:

<template>
  <div>
    <div class="product">
      <router-link :to="'/detail/' + product._id" class="product-link">
        <p class="product__name">產品名稱:{{product.name}}</p>
        <p class="product__description">介紹:{{product.description}}</p>
        <p class="product__price">價格:{{product.price}}</p>
        <p class="product.manufacturer">生產廠商:{{product.manufacturer.name}}</p>
        <img :src="product.image" alt="" class="product__image">
      </router-link>
      <product-button :product="product"></product-button>
    </div>
  </div>
</template>

<style>
.product {
  border-bottom: 1px solid black;
}

.product__image {
  width: 100px;
  height: 100px;
}
</style>

<script>
import ProductButton from './ProductButton';
export default {
  // ...
複製程式碼

該元件經過修改之後實現了點選商品的任何一條資訊,都會觸發路由跳轉到商品詳情頁,並將該商品id通過動態路由的方式傳遞到詳情頁。

在 ProductList 中使用 Getters

修改商品列表元件 src/components/products/ProductList.vue 檔案,使用了 Vuex Getters 複用了本地資料獲取邏輯,程式碼如下:

// ...
  </div>
</template>

<script>
import ProductItem from './ProductItem.vue';
export default {
  // ...
  computed: {
    // a computed getter
    products() {
      return this.$store.getters.allProducts;
    }
  },
  components: {
    // ...
複製程式碼

我們在計算屬性products中使用this.$store.getters.allProducts屬性訪問的方式呼叫getters中的allProducts屬性,我們也知道在對應的getter中獲取到了本地中的products陣列。

建立 Detail 頁面元件

實現了 ProductDetail 子元件之後,我們便可以搭建商品詳情我頁面元件 src/pages/Detail.vue,程式碼如下:

<template>
  <div>
    <product-detail :product="product"></product-detail>
  </div>
</template>

<script>
import ProductDetail from '@/components/products/ProductDetail.vue';
export default {
  created() {
    // 跳轉到詳情時,如果本地狀態裡面不存在此商品,從後端獲取此商品詳情
    const { name } = this.product;
    if (!name) {
      this.$store.dispatch('productById', {
        productId: this.$route.params['id']
      });
    }
  },
  computed: {
    product() {
      return this.$store.getters.productById(this.$route.params['id']);
    }
  },
  components: {
    'product-detail': ProductDetail,
  }
}
</script>
複製程式碼

該元件中定義了一個計算屬性product,用於返回本地狀態中指定的商品。這裡我們使用了this.$store.getters.productById(id)方法訪問的方式獲取本地中指定的商品,這裡的id引數通過this.$route.params['id']從當前處於啟用狀態的路由物件中獲取,並傳入對應的getter中,進而從本地中獲取指定商品。

在該元件剛被建立時判斷當前本地中是否有該商品,如果沒有則通過this.$store.dispatch的方式將包含當前商品id的物件作為載荷分發到型別為productByIdaction中,在action中進行非同步操作從後端獲取指定商品,然後提交到對應的mutation中進行本地狀態修改,這已經使我們習慣的思路了。

配置 Detail 頁面的路由

最後我們開啟路由配置 src/router/index.js 檔案,匯入了 Detail 元件,並新增了對應的路由引數,程式碼如下:

// ...

import Home from '@/pages/Home';
import Cart from '@/pages/Cart';
import Detail from '@/pages/Detail';

// Admin Components
import Index from '@/pages/admin/Index';
// ...
      name: 'Cart',
      component: Cart,
    },
    {
      path: '/detail/:id',
      name: 'Detail',
      component: Detail,
    }
  ],
});
複製程式碼

又到了驗收的環節,執行專案,點選單個商品,可以進入到商品詳情頁面,並且資料是完全一致的:

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

小結

這一節中我們學會了如何使用Vuex Getters來複用本地資料的獲取邏輯:

  • 我們需要先在store例項中新增getters屬性,並在getters屬性中定義不同的屬性或者方法。
  • 在這些不同型別的getter中,我們可以獲取本地資料。
  • 我們可以通過屬性訪問和方法訪問的方式來呼叫我們的getter

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

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

相關文章