[Express+Vue 搭建電商網站] 16 抽離 Vuex store 中的邏輯
使用 Express + Vue 搭建一個電商網站 - 抽離 Vuex store 中的邏輯
隨著我們的迷你電商網站越來越完整,在 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;
}
}
可以看到我們導出了 productGetters
和 manufacturerGetters
兩個方法
前者包含商品的 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 檔案。
要做的事情有兩件:
- 移除原有的 getters 和 mutations
- 引入新建的 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>
可以看到我們有兩個 computed
:manufacturers
和 model
,分別回傳製造商和當前商品
之所以要回傳當前的 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.js
的 productMutations
下新增一些 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