从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(六)
前面五篇教程我们已经基本实现了迷你全栈电商应用的界面展示以及功能逻辑,相信大家在这个过程中都收获颇丰,并且迈向了全栈工程师的第一步。但是我们却并不满足于此,我们还需要对我们的项目代码进行优化,使得我们的代码可读性更高,也更好维护。相信细心的你们已经感觉到了项目中的store实例实在是过于臃肿,因此,本篇教程就是带大家一起学习如何抽出 Getters 、 Mutations 和Actions 逻辑实现store的“减重”以及如何干掉 mutation-types 硬编码。
抽出 Getters 和 Mutations 逻辑
这一节我们来学习如何抽出在实例中定义的复杂和逻辑。
我们发现之前我们直接把所有的属性和方法都定义在了实例中的属性中,所有的属性也都定义在了实例中的属性中,这样显得实例特别的累赘,因此我们可以通过对象展开运算符将这些复杂的逻辑抽取到对应的 和 文件中。
重构 Admin 入口文件
首先我们做一点本土化,把之前的 中的英文导航改成中文版,方便查看;并且我们增加了查看生产商导航。
这里我们将有关商品的导航栏修改为中文版,让用户能够秒懂;除此之外我们又添加了有关制造商的导航,这里增加的是查看生产商导航,并添加了对应的导航跳转路径,该路径需要与对应路由参数一致。
创建 Manufacturers 组件
我们创建的文件是本地制造商组件,用于展示制造商的信息。
制造商
{{manufacturer.name}}
修改
删除
这里首先定义了一个计算属性,通过属性访问的形式调用对应的属性从本地获取,并返回给计算属性。
然后在该组件刚被创建时判断本地中是否存在,如果没有则通过分发到类型为的中进行异步操作获取所有制造商,并将获取的制造商提交到对应的中,在中修改本地状态,将获取的所有制造商保存到本地。
最后利用在表格中遍历,每个制造商的信息在一行展示,除了信息之外还有两个功能(修改和删除制造商),点击修改则会根据路由到指定页面;点击删除则会触发事件,首先询问用户是否同意删除,若用户同意则将选中制造商的id作为载荷分发到类型为的中,在中进行异步操作删除后端对应商品,并将对应商品id提交到对应的中,在中进行本地状态修改,删除本地对应的商品。
重构 Products 组件
根据组件的设计原则,我们需要再次进入文件。按照组件的UI展示以及数据处理,将组件进行一下重构。
名称
价格
制造商
{{product.name}}
{{product.price}}
{{product.manufacturer.name}}
修改
删除
我们先来看该组件的部分,首先定义了两个计算属性和返回本地商品和制造商。通过方法访问的方式调用指定的属性,参数为当前处于激活状态的路由对象的id,这里返回的拷贝,是为了在修改 的拷贝之后,在保存之前不修改本地 Vuex store 的属性。计算属性通过相同的方式获取本地数据。
当该组件刚被创建时判断计算属性中是否有值,如果没有则表示本地中没有该商品,将包含该商品id的对象作为载荷分发到类型为的中,在中进行异步操作从后端获取对应商品,并提交到对应类型的中,在中将获取到的商品保存到本地。除此之外判断计算属性中是否有值,如果没有则通过相同的方式从后端获取并保存到本地。
在中使用了子组件用表单的形式来展示商品信息,当用户提交表单则会向父组件发射事件,父组件监听到之后触发事件,并将传入的商品参数作为载荷分发到类型为的中,通知后端进行同步更新数据并提交到对应的中进行本地数据更新。
重构 New 组件
是添加商品组件,与组件的代码逻辑相似,只是一个是修改商品信息,一个是添加商品信息。
我们将该组件中原先写死的数据改成了从后端动态获取, 并将获取的数据传递给子组件
该组件代码逻辑和组件相似,只是在这里我们定义的计算属性返回一个空对象作为默认值,因为我们是添加商品,本地中还不存在该商品。
抽取 Actions 逻辑
像之前一样我们创建了文件,用于存储从实例的属性中抽取出来的不同类型的属性。这里我们定义了两个对象:和,分别表示有关商品和制造商对视图层分发的事件作出的响应,并导出了这两个对象。
// src/store/actions.js
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,
});
})
},
}
在该文件中我们首先导入依赖,以及定义了 后端接口根路由;
然后我们定义并导出了两个对象:
在对象中定义了一些有关商品在视图层分发对应的事件时,作出的响应,比如,,以及等等。
在对象中定义了一些有关制造商在视图层分发对应的事件时,作出的响应,比如,等等。
重构 Store 实例
我们再次来到文件中,添加有关抽取逻辑之后的信息。
// src/store/index.js
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);
// ...
actions: {
...productActions,
...manufacturerActions,
}
});
这里我们首先导入了文件中导出的一些Action对象,并通过对象展开运算符在实例的属性中混入了不同类型的属性,实现了Actions逻辑的抽取。
添加 mutations 属性
我们在文件中又添加了一些属性,用于用户进行不同的操作进行本地数据的同步。
// src/store/mutations.js
// ...
const { productId } = payload;
state.products = state.products.filter(product => product._id !== productId);
},
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);
}
};
// ...
上述添加的都是有关商品的属性:,,以及分别表示更新商品信息,更新商品信息成功,添加商品以及添加商品成功。
小结
这一节我们学习了如何抽出逻辑,减轻实例中的负载:
首先我们需要创建JS文件,在文件中定义不同类型的对象并导出,然后在对象中定义相应的一些属性。
在的文件中导入这些对象,并在实例的属性中通过对象展开运算符混入这些对象。
我们可以使用的方式调用。
干掉 mutation-types 硬编码
这一节我们主要是进一步完善我们的项目功能以及去掉一些硬编码。
创建 ManufacturerForm 组件
和商品信息展示功能一样,我们也需要将制造商信息展示部分封装到一个单独的组件中,以便我们在新建制造商和编辑制造商组件中都能复用该组件。
因此我们创建了文件,用于展示制造商信息的表单组件。
该组件通过父子组件传值从父组件获取到了和的值,并将对象的信息展示在表单中。
表单信息中还通过来判断的值是还是,如果是则创建,反之创建。
当用户提交表单时触发事件,此时会向父组件发送类型为的事件通知其保存此次的修改操作。
重构 getters 文件
在创建编辑制造商组件之前,我们需要在getters文件中添加对应的getter属性。
我们在文件的对象中又添加了一个方法,用于获取本地中指定的制造商。
// src/store/getters.js
// ...
export const manufacturerGetters = {
allManufacturers(state) {
return state.manufacturers;
},
manufacturerById: (state, getters) => id => {
if (getters.allManufacturers.length > 0) {
return getters.allManufacturers.filter(manufacturer => manufacturer._id === id)[0]
} else {
return state.manufacturer;
}
}
}
方法中的id参数是Vue视图层通过方法调用时传入的id,通过这个id判断本地中是否存在该制造商,如果存在则返回该制造商,如果不存在则返回一个空对象。
创建 EditManufacturers 组件
在创建了展示制造商信息的表单组件以及添加了用于获取本地指定制造商数据的getter属性之后,紧接着我们又创建了文件,用于修改制造商信息。
该组件刚被创建时将当前处于激活状态的路由对象的id参数作为载荷分发到类型为的中,在中进行异步操作从服务器获取对应制造商,然后将该制造商提交到对应中进行本地状态修改,将获取到的制造商保存到本地。
我们定义了计算属性返回的拷贝,是为了在修改的拷贝之后,在保存之前不修改本地 中的属性。这里以方法访问的形式从中通过当前激活的路由对象中的id参数获取本地状态中的对应制造商作为的拷贝,并返回给计算属性,然后传给子组件。
该组件在事件中将子组件传入的新制造商对象作为载荷分发到类型为的中,在中进行异步操作修改后端对应的商品信息,然后将新对象提交到对应的中进行本地状态修改,修改本地状态中的对象。
创建 NewManufacturers 组件
同样的我们继续创建了文件,用于添加制造商信息。该组件和添加商品信息组件代码逻辑类似。
该组件逻辑代码与组件类似,一个是添加商品组件,一个是添加制造商组件,您可以对比着来看。
重构 Admin 入口文件
之前我们在该入口文件中增加了查看生产商导航,这里我们又增加了添加生产商导航。
查看生产商
添加生产商
添加路由信息
我们已经创建了添加和修改制造商组件以及添加了对应的入口导航,接着我们需要在该文件中对其进行路由参数配置。
再次进入文件,我们导入了添加制造商和修改制造商的组件并配置了相关路由参数。
// src/router/index.js
// ...
import Products from '@/pages/admin/Products';
import Edit from '@/pages/admin/Edit';
import Manufacturers from '@/pages/admin/Manufacturers';
import NewManufacturers from '@/pages/admin/NewManufacturers';
import EditManufacturers from '@/pages/admin/EditManufacturers';
Vue.use(Router);
// ...
name: 'Manufacturers',
component: Manufacturers,
},
{
path: 'manufacturers/new',
name: 'NewManufacturers',
component: NewManufacturers,
},
{
path: 'manufacturers/edit/:id',
name: 'EditManufacturers',
component: EditManufacturers,
},
]
},
{
// ...
这里添加制造商的路由配置就是静态路由的配置方式;修改制造商的路由配置采用了动态传参的方式,这里使用的是对象的方式传参。
创建 mutation-types 文件
这一节我们将对我们的项目代码进行优化,干掉一些硬编码。
我们都知道在文件和文件中有一部分的事件类型是需要保持一致的,比如在我们在视图层分发一个添加商品事件,在文件中就需要有对应事件类型的接收,然后向后端发起请求并将请求结果提交到对应类型的中,这就要求了这几个文件中的对应事件类型都要保持一致。可是我们在开发过程中难免会出错,比如漏掉一个字母就会导致两个文件中的对应事件无法接收,尴尬的是控制台也没有报错,这就造成了我们很难查错。
因此,我们采用了字符串常量的形式定义文件和文件中的事件类型,只要我们写错一个单词都会导致字符串常量不一致,关键的是这个时候会报错,利于我们查错。
进而我们创建了文件,用于定义一些字符串常量来表示各种事件类型,并导出这些字符串常量。
// src/store/mutation-types.js
export const ALL_PRODUCTS = 'ALL_PRODUCTS';
export const ALL_PRODUCTS_SUCCESS = 'ALL_PRODUCTS_SUCCESS';
export const PRODUCT_BY_ID = 'PRODUCT_BY_ID';
export const PRODUCT_BY_ID_SUCCESS = 'PRODUCT_BY_ID_SUCCESS';
export const ADD_PRODUCT = 'ADD_PRODUCT';
export const ADD_PRODUCT_SUCCESS = 'ADD_PRODUCT_SUCCESS';
export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
export const UPDATE_PRODUCT_SUCCESS = 'UPDATE_PRODUCT_SUCCESS';
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT';
export const REMOVE_PRODUCT_SUCCESS = 'REMOVE_PRODUCT_SUCCESS';
export const ADD_TO_CART = 'ADD_TO_CART';
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART';
export const ALL_MANUFACTURERS = 'ALL_MANUFACTURER';
export const ALL_MANUFACTURERS_SUCCESS = 'ALL_MANUFACTURER_S';
export const MANUFACTURER_BY_ID = 'MANUFACTURER_BY_ID';
export const MANUFACTURER_BY_ID_SUCCESS = 'MANUFACTURER_BY_ID_SUCCESS';
export const ADD_MANUFACTURER = 'ADD_MANUFACTURER';
export const ADD_MANUFACTURER_SUCCESS = 'ADD_MANUFACTURER_SUCCESS';
export const UPDATE_MANUFACTURER = 'UPDATE_MANUFACTURER';
export const UPDATE_MANUFACTURER_SUCCESS = 'UPDATE_MANUFACTURER_SUCCESS';
export const REMOVE_MANUFACTURER = 'REMOVE_MANUFACTURER';
export const REMOVE_MANUFACTURER_SUCCESS = 'REMOVE_MANUFACTURER_SUCCESS';
重构 actions 文件
我们再次来到文件中,将所有的事件类型用字符串常量表示。
// src/store/actions.js
import axios from 'axios';
import {
ADD_PRODUCT,
ADD_PRODUCT_SUCCESS,
PRODUCT_BY_ID,
PRODUCT_BY_ID_SUCCESS,
UPDATE_PRODUCT,
UPDATE_PRODUCT_SUCCESS,
REMOVE_PRODUCT,
REMOVE_PRODUCT_SUCCESS,
ALL_PRODUCTS,
ALL_PRODUCTS_SUCCESS,
ALL_MANUFACTURERS,
ALL_MANUFACTURERS_SUCCESS,
MANUFACTURER_BY_ID,
MANUFACTURER_BY_ID_SUCCESS,
ADD_MANUFACTURER,
ADD_MANUFACTURER_SUCCESS,
UPDATE_MANUFACTURER,
UPDATE_MANUFACTURER_SUCCESS,
REMOVE_MANUFACTURER,
REMOVE_MANUFACTURER_SUCCESS,
} from './mutation-types';
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,
});
})
},
manufacturerById({ commit }, payload) {
commit(MANUFACTURER_BY_ID);
const { manufacturerId } = payload;
axios.get(`${API_BASE}/manufacturers/${manufacturerId}`).then(response => {
commit(MANUFACTURER_BY_ID_SUCCESS, {
manufacturer: response.data,
});
})
},
removeManufacturer({ commit }, payload) {
commit(REMOVE_MANUFACTURER);
const { manufacturerId } = payload;
axios.delete(`${API_BASE}/manufacturers/${manufacturerId}`).then(() => {
// 返回 manufacturerId,用于删除本地对应的制造商
commit(REMOVE_MANUFACTURER_SUCCESS, {
manufacturerId,
});
})
},
updateManufacturer({ commit }, payload) {
commit(UPDATE_MANUFACTURER);
const { manufacturer } = payload;
axios.put(`${API_BASE}/manufacturers/${manufacturer._id}`, manufacturer).then(() => {
commit(UPDATE_MANUFACTURER_SUCCESS, {
manufacturer,
});
})
},
addManufacturer({ commit }, payload) {
commit(ADD_MANUFACTURER);
const { manufacturer } = payload;
axios.post(`${API_BASE}/manufacturers`, manufacturer).then(response => {
commit(ADD_MANUFACTURER_SUCCESS, {
manufacturer: response.data,
})
})
}
}
这里我们首先导入了文件中定义的一些字符串常量,替换掉了对应的事件类型。
重构 mutations 文件
同actions文件一样,我们再次进入文件,将文件中的各种事件类型用字符串常量替代。
// src/store/mutations.js
import {
ADD_PRODUCT,
ADD_PRODUCT_SUCCESS,
PRODUCT_BY_ID,
PRODUCT_BY_ID_SUCCESS,
UPDATE_PRODUCT,
UPDATE_PRODUCT_SUCCESS,
REMOVE_PRODUCT,
REMOVE_PRODUCT_SUCCESS,
ADD_TO_CART,
REMOVE_FROM_CART,
ALL_PRODUCTS,
ALL_PRODUCTS_SUCCESS,
ALL_MANUFACTURERS,
ALL_MANUFACTURERS_SUCCESS,
MANUFACTURER_BY_ID,
MANUFACTURER_BY_ID_SUCCESS,
ADD_MANUFACTURER,
ADD_MANUFACTURER_SUCCESS,
UPDATE_MANUFACTURER,
UPDATE_MANUFACTURER_SUCCESS,
REMOVE_MANUFACTURER,
REMOVE_MANUFACTURER_SUCCESS,
} from './mutation-types';
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);
},
[UPDATE_PRODUCT](state) {
state.showLoader = true;
},
[UPDATE_PRODUCT_SUCCESS](state, payload) {
state.showLoader = false;
const { product: newProduct } = payload;
// ...
return product;
})
},
[ADD_PRODUCT](state) {
state.showLoader = true;
},
[ADD_PRODUCT_SUCCESS](state, payload) {
state.showLoader = false;
const { product } = payload;
// ...
};
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;
},
[MANUFACTURER_BY_ID](state) {
state.showLoader = true;
},
[MANUFACTURER_BY_ID_SUCCESS](state, payload) {
state.showLoader = false;
const { manufacturer } = payload;
state.manufacturer = manufacturer;
},
[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);
},
[UPDATE_MANUFACTURER](state) {
state.showLoader = true;
},
[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;
})
},
[ADD_MANUFACTURER](state) {
state.showLoader = true;
},
[ADD_MANUFACTURER_SUCCESS](state, payload) {
state.showLoader = false;
const { manufacturer } = payload;
state.manufacturers = state.manufacturers.concat(manufacturer);
}
}
这里我们首先导入了文件中定义的一些字符串常量,替换掉了对应的事件类型。
小结
这一节我们主要做了以下工作:
创建了制造商相关组件并配置了相应路由参数,实现了新建和修改制造商信息;
利用字符串常量代替actions文件和mutations文件中的事件类型;