Byte Ebi's Logo

Byte Ebi 🍤

每天一小口,蝦米變鯨魚

[Express+Vue 搭建電商網站] 16 抽離 Vuex store 中的邏輯

使用 Express + Vue 搭建一個電商網站 - 抽離 Vuex store 中的邏輯

Ray

隨著我們的迷你電商網站越來越完整,在 Vuex store 中的程式也越來越龐大
不只有 getters、mutation 還有 actions
在章節中先試著將這些複雜的邏輯拆分成個別的檔案,抽出 Getters、Mutations 和 Actions 邏輯

重構 Admin 首頁

打開 src/views/admin/Index.vue 頁面,將選單換成中文。並且加上查看查看製造商的選項

<template>
  <div>
    <div class="admin-new">
      <div class="container">
        <div class="col-lg-3 col-md-3 col-sm-12 col-xs-12">
          <ul class="admin-menu">
            <li>
              <router-link to="/admin">查看商品</router-link>
            </li>
            <li>
              <router-link to="/admin/new">新建商品</router-link>
            </li>
            <li>
              <router-link to="/admin/manufacturers">查看製造商</router-link>
            </li>
          </ul>
        </div>
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

可以預期到的接下來我們會做一個關於製造商的頁面,然後透過後端 API 取得製造商資料

建立 Manufacturers 頁面

新建 src/views/admin/Manufacturers.vue 檔案

<template>
  <div>
    <table class="table">
      <thead>
        <tr>
          <th>製造商</th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="manufacturer in manufacturers" :key="manufacturer._id">
          <td>{{manufacturer.name}}</td>
          <td class="modify">
            <router-link :to="'/admin/manufacturers/edit/' + manufacturer._id">修改</router-link>
          </td>
          <td class="remove">
            <a @click="removeManufacturer(manufacturer._id)" href="#">刪除</a>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style>
table {
  margin: 0 auto;
}

.modify {
  color: blue;
}

.remove a {
  color: red;
}
</style>

<script>
export default {
  created() {
    if (this.manufacturers.length === 0) {
      this.$store.dispatch("allManufacturers");
    }
  },
  computed: {
    manufacturers() {
      return this.$store.getters.allManufacturers;
    }
  },
  methods: {
    removeManufacturer(manufacturerId) {
      const res = confirm("是否刪除此製造商?");
      if (res) {
        this.$store.dispatch("removeManufacturer", {
          manufacturerId
        });
      }
    }
  }
};
</script>

可以看到這邊我們用了一些先前沒用過的方法
例如 computed 中的 manufacturers、生命週期 created() 時候會使用到的 allManufacturers
還有製造商刪除用的 method 中 removeManufacturer,這些會在之後實作出來

重構 Products 組件

接著要動手重構的就是 src/views/admin/Products.vue 組件

<template>
  <div>
    <table class="table">
      <thead>
        <tr>
          <th>名稱</th>
          <th>價錢</th>
          <th>製造商</th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="product in products" :key="product._id">
          <td>{{product.name}}</td>
          <td>{{product.price}}</td>
          <td>{{product.manufacturer.name}}</td>
          <td class="modify">
            <router-link :to="'/admin/edit/' + product._id">修改</router-link>
          </td>
          <td class="remove">
            <a @click="removeProduct(product._id)" href="#">刪除</a>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style>
table {
  margin: 0 auto;
}

.modify {
  color: blue;
}

.remove a {
  color: red;
}
</style>

<script>
export default {
  created() {
    if (this.products.length === 0) {
      this.$store.dispatch("allProducts");
    }
  },
  computed: {
    products() {
      return this.$store.getters.allProducts;
    }
  },
  methods: {
    removeProduct(productId) {
      const res = confirm("是否刪除此商品?");
      if (res) {
        this.$store.dispatch("removeProduct", {
          productId
        });
      }
    }
  }
};
</script>

基本上和使用者畫面中的商品頁差不多,就是將加入購物車換成了修改與刪除商品
也和剛剛操作過的 Manufacturers 組件相似,相關的東西講了許多次了,就交給你來思考

加入路由設定

頁面跟組件都完成了,接著就要讓頁面可以被訪問
打開 vue-router 的設定 src/router/index.js,加入製造商相關的路由參數

引入頁面檔案

import Manufacturers from '@/views/admin/Manufacturers'

在 admin 路由的 children 屬性中加入頁面

{
    path: 'manufacturers',
    name: 'Manufacturers',
    component: Manufacturers,
},

接著開啟專案,可以看到製造商連結已經生效,可以把我們帶到製造商頁面,但是資料還是沒有取得
記得嗎?之前說要從後端 API 取得資料的方法還沒寫,接下來就一邊重構一邊把這個功能完成吧!

分離 Getter 邏輯

首先建立 src/store/getters.js 檔案,用來存放各種不同的 getter

export const productGetters = {
    allProducts(state) {
        return state.products
    },
    productById: (state, getters) => id => {
        if (getters.allProducts.length > 0) {
            return getters.allProducts.filter(product => product._id === id)[0]
        } else {
            return state.product;
        }
    }
}

export const manufacturerGetters = {
    allManufacturers(state) {
        return state.manufacturers;
    }
}

可以看到我們導出了 productGettersmanufacturerGetters 兩個方法
前者包含商品的 getters,後者則是負責製造商的 getter,如此就補上了前面幾段缺少的 manufacturer getters

分離 Mutations 邏輯

就像剛剛分離 Getter 邏輯,接著新建 src/store/mutations.js 檔案作為 store 中 mutation 的程式管理

export const productMutations = {
    ALL_PRODUCTS(state) {
        state.showLoader = true;
    },
    ALL_PRODUCTS_SUCCESS(state, payload) {
        const { products } = payload;

        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;
    },
    REMOVE_PRODUCT(state) {
        state.showLoader = true;
    },
    REMOVE_PRODUCT_SUCCESS(state, payload) {
        state.showLoader = false;

        const { productId } = payload;
        state.products = state.products.filter(product => product._id !== productId);
    }
};

export const cartMutations = {
    ADD_TO_CART(state, payload) {
        const { product } = payload;
        state.cart.push(product)
    },
    REMOVE_FROM_CART(state, payload) {
        const { productId } = payload
        state.cart = state.cart.filter(product => product._id !== productId)
    },
}

export const manufacturerMutations = {
    ALL_MANUFACTURERS(state) {
        state.showLoader = true;
    },
    ALL_MANUFACTURERS_SUCCESS(state, payload) {
        const { manufacturers } = payload;

        state.showLoader = false;
        state.manufacturers = manufacturers;
    },
    REMOVE_MANUFACTURER(state) {
        state.showLoader = true;
    },
    REMOVE_MANUFACTURER_SUCCESS(state, payload) {
        state.showLoader = false;

        const { manufacturerId } = payload;
        state.manufacturers = state.manufacturers.filter(manufacturer => manufacturer._id !== manufacturerId);
    }
}

分別導出了

  • productMutations
  • cartMutations
  • manufacturerMutations

來操作 vuex store 中的不同狀態,這邊也加入了生產商相關的狀態管理 mutations,讓之後的 actions 可以呼叫

重構 Store 物件

既然剛剛都把 Getter 和 Mutations 抽離文件完成了,這邊就要重構 Store 檔案。

要做的事情有兩件:

  1. 移除原有的 getters 和 mutations
  2. 引入新建的 getters 和 mutations

下面就是新的 src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

import { productGetters, manufacturerGetters } from './getters';
import { productMutations, cartMutations, manufacturerMutations } from './mutations';

const API_BASE = 'http://localhost:3000/api/v1';

Vue.use(Vuex);

export default new Vuex.Store({
  strict: true,
  state: {
    // bought items
    cart: [],
    // ajax loader
    showLoader: false,
    // selected product
    product: {},
    // all products
    products: [],
    // all manufacturers
    manufacturers: [],
  },
  mutations: {
    ...productMutations,
    ...cartMutations,
    ...manufacturerMutations,
  },
  getters: {
    ...productGetters,
    ...manufacturerGetters,
  },
  actions: {
    allProducts({ commit }) {
      commit('ALL_PRODUCTS')

      axios.get(`${API_BASE}/products`).then(response => {
        console.log('response', 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,
        });
      })
    },
    removeProduct({ commit }, payload) {
      commit('REMOVE_PRODUCT');

      const { productId } = payload;
      axios.delete(`${API_BASE}/products/${productId}`).then(() => {
        // 傳入 manufacturerId,用來刪除指定商品
        commit('REMOVE_PRODUCT_SUCCESS', {
          productId,
        });
      })
    },
    allManufacturers({ commit }) {
      commit('ALL_MANUFACTURERS');

      axios.get(`${API_BASE}/manufacturers`).then(response => {
        commit('ALL_MANUFACTURERS_SUCCESS', {
          manufacturers: response.data,
        });
      })
    },
    removeManufacturer({ commit }, payload) {
      commit('REMOVE_MANUFACTURER');

      const { manufacturerId } = payload;
      axios.delete(`${API_BASE}/manufacturers/${manufacturerId}`).then(() => {
        // 傳入 manufacturerId,用來刪除指定製造商
        commit('REMOVE_MANUFACTURER_SUCCESS', {
          manufacturerId,
        });
      })
    },
  }
});

移除原有 getters 和 mutations 不難理解,而引入新建的 getters 和 mutations 就值得說明了!

首先藉由

import { productGetters, manufacturerGetters } from './getters';
import { productMutations, cartMutations, manufacturerMutations } from './mutations';

引入剛剛分離出去的檔案
接著使用 ES6 中的:擴展運算符(spread operator) 將剛剛引入的屬性以及包含的方法導入到 store 物件中。

除此之外我們還偷偷在 actions 加入一些 action 屬性,稍後我們也會把它抽離出去
這樣整個 store 看起來就會更簡潔了

分離 Actions 邏輯

上面抽出了 Getters、Mutations 終於輪到 Actions 了

重構 Edit 頁面

打開 src/views/admin/Edit.vue 替換成

<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.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;
    },
    model() {
      const product = this.$store.getters.productById(this.$route.params["id"]);

      // 回傳 product 的備份,是為了在修改 product 的備份之後,在保存之前不修改本地 Vuex store 的 product 屬性
      return { ...product, manufacturer: { ...product.manufacturer } };
    }
  },
  methods: {
    updateProduct(product) {
      this.$store.dispatch("updateProduct", {
        product
      });
    }
  },
  components: {
    "product-form": ProductForm
  }
};
</script>

可以看到我們有兩個 computedmanufacturersmodel,分別回傳製造商和當前商品
之所以要回傳當前的 product 是為了在編輯了 product 的副本之後
在存入資料庫之前先不變更使用者端 Vuex store 中的 product 屬性

當組件被建立時,判斷 model 是否有值,如果沒有代表本機狀態庫中沒有資料
必須透異步 API 取得商品資料,並且使用對應的 mutation 修改狀態庫中的資料

<template> 中使用了子組件 ProductForm 來顯示商品資料
按下表單送出時則會對送出 updateProduct 的異步 action,通知指定 mutation 來更新狀態

重構 New 頁面

src/views/admin/New.vue 負責建立新的商品,邏輯與 Edit 類似
只是一個負責新增商品,一個修改

在這邊我們將組件中原本寫死的資料改為從後端動態取得,並將資料傳入給子組件 ProductForm

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

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

跟 Edit 組件類似,只是這邊的 model 屬性回傳的是空物件,畢竟當前是不存在商品的

拆分 Actions 邏輯

就像之前一樣建立 src/store/actions.js 檔案,用來管理 store 物件中 actions 屬性的內部屬性
就跟上面處理 Getters 和 Manufacturers 時類似做法

import axios from 'axios';

const API_BASE = 'http://localhost:3000/api/v1';

export const productActions = {
    allProducts({ commit }) {
        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,
            });
        })
    },
    removeProduct({ commit }, payload) {
        commit('REMOVE_PRODUCT');

        const { productId } = payload;
        axios.delete(`${API_BASE}/products/${productId}`).then(() => {
            // 回傳 productId,用來刪除對應商品
            commit('REMOVE_PRODUCT_SUCCESS', {
                productId,
            });
        })
    },
    updateProduct({ commit }, payload) {
        commit('UPDATE_PRODUCT');

        const { product } = payload;
        axios.put(`${API_BASE}/products/${product._id}`, product).then(() => {
            commit('UPDATE_PRODUCT_SUCCESS', {
                product,
            });
        })
    },
    addProduct({ commit }, payload) {
        commit('ADD_PRODUCT');

        const { product } = payload;
        axios.post(`${API_BASE}/products`, product).then(response => {
            commit('ADD_PRODUCT_SUCCESS', {
                product: response.data,
            })
        })
    }
};

export const manufacturerActions = {
    allManufacturers({ commit }) {
        commit('ALL_MANUFACTURERS');

        axios.get(`${API_BASE}/manufacturers`).then(response => {
            commit('ALL_MANUFACTURERS_SUCCESS', {
                manufacturers: response.data,
            });
        })
    },
    removeManufacturer({ commit }, payload) {
        commit('REMOVE_MANUFACTURER');

        const { manufacturerId } = payload;
        axios.delete(`${API_BASE}/manufacturers/${manufacturerId}`).then(() => {
            // 回傳 manufacturerId,用來刪除對應的製造商
            commit('REMOVE_MANUFACTURER_SUCCESS', {
                manufacturerId,
            });
        })
    },
}

可以發現我們把 API 的設定與使用都搬到這邊了,所以可以猜到下一步我們要做的就是「重構 Store」

重構 Store

再次回到 src/store/index.js 檔案中,導入 Actions 邏輯相關的設定
並且移除 API 相關的設定,包含引入 axios 和 API 網址的參數設定

import Vue from 'vue';
import Vuex from 'vuex';

import { productGetters, manufacturerGetters } from './getters';
import { productMutations, cartMutations, manufacturerMutations } from './mutations';
import { productActions, manufacturerActions } from './actions';

Vue.use(Vuex);

export default new Vuex.Store({
  strict: true,
  state: {
    // bought items
    cart: [],
    // ajax loader
    showLoader: false,
    // selected product
    product: {},
    // all products
    products: [],
    // all manufacturers
    manufacturers: [],
  },
  mutations: {
    ...productMutations,
    ...cartMutations,
    ...manufacturerMutations,
  },
  getters: {
    ...productGetters,
    ...manufacturerGetters,
  },
  actions: {
    ...productActions,
    ...manufacturerActions,
  }
});

於是我們就完成了 Actions 邏輯的抽換,來賓請掌聲鼓勵!

新增 mutations 屬性

接著我們要在 src/store/mutations.jsproductMutations 下新增一些 mutation 屬性
用來處理使用者不同的操作時更新狀態庫中的內容同步

UPDATE_PRODUCT(state) {
    state.showLoader = true;
},
UPDATE_PRODUCT_SUCCESS(state, payload) {
    state.showLoader = false;

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

        return product;
    })
},
ADD_PRODUCT(state) {
    state.showLoader = true;
},
ADD_PRODUCT_SUCCESS(state, payload) {
    state.showLoader = false;

    const { product } = payload;
    state.products = state.products.concat(product);
}

上面幾個 mutation 屬性分別處理了更新商品以及加入商品的邏輯,如此就完成了第一階段的重構
本來還想把更多的重構寫在一起,但由於此篇篇幅以及資訊量已經很龐大,就在下一篇繼續優化我們的程式吧!


專案範例程式碼 GitHub 網址:ray247k/mini-E-commerce

最新文章

Category

Tag