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

16 抽離 Vuex store 中的邏輯

抽離 Vuex store 中的邏輯

隨著我們的迷你電商網站越來越完整,在 Vuex store 中的程式也越來越龐大,不只有 gettersmutation 還有 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,後者則是負責製造商的 getters,如此就補上了前面幾段缺少的 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);
 }
}

分別導出了 productMutationscartMutationsmanufacturerMutations 來操作 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 屬性分別處理了更新商品以及加入商品的邏輯,如此就完成了第一階段的重構。本來還想把更多的重構寫在一起,但由於此篇篇幅以及資訊量已經很龐大,就在下一篇繼續優化我們的程式吧!

留言