2

从零到部署:用 Vue 和 Express 实现迷你全栈电商应用(六)

本文由图雀社区成员 Holy 使用 Tuture 实战教程写作工具 写作而成,欢迎加入图雀社区,一起创作精彩的免费技术实战教程,予力编程行业发展。

前面五篇教程我们已经基本实现了迷你全栈电商应用的界面展示以及功能逻辑,相信大家在这个过程中都收获颇丰,并且迈向了全栈工程师的第一步。但是我们却并不满足于此,我们还需要对我们的项目代码进行优化,使得我们的代码可读性更高,也更好维护。相信细心的你们已经感觉到了项目中的store实例实在是过于臃肿,因此,本篇教程就是带大家一起学习如何抽出 Getters 、 Mutations 和Actions 逻辑实现store的“减重”以及如何干掉 mutation-types 硬编码。

欢迎阅读《从零到部署:用 Vue 和 Express 实现迷你全栈电商应用》系列:

如果你希望直接从这一步开始,请运行以下命令:

git clone -b section-six https://github.com/tuture-dev/vue-online-shop-frontend.git
cd vue-online-shop-frontend
本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦~

抽出 Getters 和 Mutations 逻辑

这一节我们来学习如何抽出在store实例中定义的复杂gettersmutation逻辑。

我们发现之前我们直接把所有的getter属性和方法都定义在了store实例中的getters属性中,所有的mutation属性也都定义在了store实例中的mutations属性中,这样显得store实例特别的累赘,因此我们可以通过对象展开运算符将这些复杂的逻辑抽取到对应的 GettersMutations文件中。

重构 Admin 入口文件

首先我们做一点本土化,把之前的 src/pages/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>

这里我们将有关商品的导航栏修改为中文版,让用户能够秒懂;除此之外我们又添加了有关制造商的导航,这里增加的是查看生产商导航,并添加了对应的导航跳转路径,该路径需要与对应路由参数一致。

创建 Manufacturers 组件

我们创建的src/pages/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) {
      // 使用 JavaScript BOM 的 confirm 方法来询问用户是否删除此制造商
      const res = confirm('是否删除此制造商?');

      // 如果用户同意,那么就删除此制造商
      if (res) {
        this.$store.dispatch('removeManufacturer', {
          manufacturerId,
        })
      }
    }
  }
}
</script>

这里首先定义了一个计算属性manufacturers,通过this.$store.getters.allManufacturers属性访问的形式调用对应的getter属性allManufacturers从本地获取manufacturers,并返回给计算属性manufacturers

然后在该组件刚被创建时判断本地中是否存在manufacturers,如果没有则通过this.$store.dispatch分发到类型为allManufacturersaction中进行异步操作获取所有制造商,并将获取的制造商提交到对应的mutation中,在mutation中修改本地状态,将获取的所有制造商保存到本地。

最后利用v-for在表格中遍历manufacturers,每个制造商的信息在一行展示,除了信息之外还有两个功能(修改和删除制造商),点击修改则会根据'/admin/manufacturers/edit/' + manufacturer._id路由到指定页面;点击删除则会触发removeManufacturer事件,首先询问用户是否同意删除,若用户同意则将选中制造商的id作为载荷分发到类型为removeManufactureraction中,在action中进行异步操作删除后端对应商品,并将对应商品id提交到对应的mutation中,在mutation中进行本地状态修改,删除本地对应的商品。

重构 Products 组件

根据Manufacturers组件的设计原则,我们需要再次进入src/pages/admin/Products.vue文件。按照Manufacturers组件的UI展示以及数据处理,将Products组件进行一下重构。

<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) {
      // 使用 JavaScript BOM 的 confirm 方法来询问用户是否删除此商品
      const res = confirm('是否删除此商品?');

      // 如果用户同意,那么就删除此商品
      if (res) {
        this.$store.dispatch('removeProduct', {
          productId,
        })
      }
    }
  }
}
// ...

这部分代码逻辑与src/pages/admin/Manufacturers.vue文件中的代码逻辑相似,如果您理解了上面的代码逻辑,那么我们相信您对这里的代码也能融会贯通,所以这里就不再赘述了。

添加路由信息

我们已经创建了有关制造商的导航以及查看制造商组件,还需对其配置相应的路由参数才能实现跳转。因此再次进入src/router/index.js文件,这里我们导入了制造商组件并增加了制造商相关路由参数。

// ...
import New from '@/pages/admin/New';
import Products from '@/pages/admin/Products';
import Edit from '@/pages/admin/Edit';
import Manufacturers from '@/pages/admin/Manufacturers';

Vue.use(Router);

// ...
          name: 'Edit',
          component: Edit,
        },
        {
          path: 'manufacturers',
          name: 'Manufacturers',
          component: Manufacturers,
        },
      ]
    },
    {
      // ...

把项目跑起来,点击Admin然后再点击查看生产商,我们可以看到从后端获取的所有生产商: 下面展示的就是从后端获取的制造商,并且我们可以对其进行修改和删除操作。

抽取 Getters 逻辑

我们首先创建了src/store/getters.js文件,用于存放各种不同类型的getter属性和方法。这里我们导出了两个对象分别为productGettersmanufacturerGetters,前者包含了有关商品的getter属性与方法,后者包含了有关制造商的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对象中定义的就是有关商品的getter属性和方法,如allProductsproductById等等;在manufacturerGetters对象中定义的就是有关制造商的getter属性和方法,如allManufacturers等等。

我们可以采用属性调用和方法调用的方式调用这里的getter(根据我们在getter对象中定义的是属性还是方法)

抽取 Mutations 逻辑

同样的我们创建了src/store/mutations.js文件,用于存放从store实例的mutations属性中抽取出来的各种mutation属性,这里我们定义了三个对象分别为productMutationscartMutations以及manufacturerMutations

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对象中定义了有关商品响应Vue视图层以及avtion中提交的事件,比如ALL_PRODUCTSALL_PRODUCTS_SUCCESSPRODUCT_BY_ID以及PRODUCT_BY_ID_SUCCESS等等。

cartMutations对象中定义了有关购物车响应Vue视图层提交的事件,比如ADD_TO_CARTREMOVE_FROM_CART等等。

manufacturerMutations 对象中定义了有关制造商响应Vue视图层以及avtion中提交的事件,比如ALL_MANUFACTURERSALL_MANUFACTURERS_SUCCESSREMOVE_MANUFACTURER以及REMOVE_MANUFACTURER_SUCCESS等等。

重构 Store 实例

我们将store实例中的Getter属性和Mutation属性抽出之后要再进行导入。再回到src/store/index.js文件,这里就是抽离gettersmutations逻辑之后的store实例,看起来是不是轻盈了很多,也增强了代码的可读性。

// ...
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);
 // ...
    manufacturers: [],
  },
  mutations: {
    ...productMutations,
    ...cartMutations,
    ...manufacturerMutations,
  },
  getters: {
    ...productGetters,
    ...manufacturerGetters,
  },
  actions: {
    allProducts({ commit }) {
      // ...
          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,
        });
      })
    },
    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,
        });
      })
    },
  }
});

这里首先导入了gettersmutations文件中导出的所有对象,然后在store实例的gettersmutations属性中通过对象展开运算符的方式将对应的属性和方法导入到store实例中。

对象展开运算符是ES7草案中的新特性,将一个对象当中的对象的一部分取出来成为一个新对象赋值给展开运算符的参数,然后插入到另外一个对象当中。例如:
let shortcuts = {
    attr1: 3,
    attr2: 4
}
let shortcuts2 = {}
shortcuts2 = {...shortcuts}

//上面的这种用法实际上相当于是:
shortcuts2 = {attr1: 3, attr2: 4}

除此之外我们又在actions中添加了一些其他的action属性,因为此时actions还未被抽离,所以可能依然显得有些臃肿,不过在后面我们马上也会将它抽离出来。

小结

这一节我们学习了如何抽出GettersMutations逻辑,减轻store实例中的负载:

  • 首先我们需要分别创建gettersmutationsJS文件,在两个JS文件中分别定义不同类型的gettersmutations对象并导出,然后在gettersmutations对象中定义相应的一些属性和方法。
  • storeindex文件中导入这些gettersmutations对象,并在store实例的gettersmutations属性中通过对象展开运算符混入这些对象。
  • 我们可以使用this.$store.getters.属性this.$store.mutations.属性的方式调用.

抽出 Actions 逻辑

上一节我们学习了如何抽出GettersMutations逻辑,这一节我们以同样的方式抽出Actions逻辑。

重构 Edit 组件

src/pages/admin/Edit.vue是商品编辑组件,当触发'/admin/edit/' + product._id就会路由到指定商品信息编辑页面,然后对商品信息进行修改。

之前我们直接将展示商品信息的代码放在该组件中,但是我们发现展示商品信息这部分功能在新建商品和编辑商品组件中都需要使用,因此我们打算把这部分代码封装为一个展示商品信息的表单组件ProductForm,这样的话我们在新建商品和编辑商品组件中都能复用该组件。

除此之外我们还在该组件中添加了数据处理功能。

<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 stire 的 product 属性
      return { ...product, manufacturer: { ...product.manufacturer } };
    }
  },
  methods: {
    updateProduct(product) {
      this.$store.dispatch('updateProduct', {
        product,
      })
    }
  },
  components: {
    'product-form': ProductForm
  }
}
</script>

我们先来看该组件的script部分,首先定义了两个计算属性modelmanufacturers返回本地商品和制造商。通过方法访问的方式调用指定的getter属性productById,参数为当前处于激活状态的路由对象的id,这里返回product的拷贝,是为了在修改 product的拷贝之后,在保存之前不修改本地 Vuex store 的product属性。计算属性manufacturers通过相同的方式获取本地数据。

当该组件刚被创建时判断计算属性model中是否有值,如果没有则表示本地中没有该商品,将包含该商品id的对象作为载荷分发到类型为productByIdaction中,在action中进行异步操作从后端获取对应商品,并提交到对应类型的mutation中,在mutation中将获取到的商品保存到本地。除此之外判断计算属性manufacturers中是否有值,如果没有则通过相同的方式从后端获取并保存到本地。

template中使用了子组件ProductForm用表单的形式来展示商品信息,当用户提交表单则会向父组件发射save-product事件,父组件监听到之后触发updateProduct事件,并将传入的商品参数作为载荷分发到类型为updateProductaction中,通知后端进行同步更新数据并提交到对应的mutation中进行本地数据更新。

重构 New 组件

src/pages/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.vue组件相似,只是在这里我们定义的计算属性model返回一个空对象作为默认值,因为我们是添加商品,本地中还不存在该商品。

抽取 Actions 逻辑

像之前一样我们创建了src/store/actions.js文件,用于存储从store实例的actions属性中抽取出来的不同类型的action属性。这里我们定义了两个Actions对象:productActionsmanufacturerActions,分别表示有关商品和制造商对视图层分发的事件作出的响应,并导出了这两个对象。

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,
      });
    })
  },
}

在该文件中我们首先导入axios依赖,以及定义了 API_BASE 后端接口根路由;

然后我们定义并导出了两个对象:

  • productActions对象中定义了一些有关商品在视图层分发对应的事件时,action作出的响应,比如allProductsproductByIdremoveProduct以及updateProduct等等。
  • manufacturerActions对象中定义了一些有关制造商在视图层分发对应的事件时,action作出的响应,比如allManufacturersremoveManufacturer等等。

重构 Store 实例

我们再次来到src/store/index.js文件中,添加有关抽取Actions逻辑之后的信息。

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,
  }
});

这里我们首先导入了actions.js文件中导出的一些Action对象,并通过对象展开运算符在store实例的actions属性中混入了不同类型的action属性,实现了Actions逻辑的抽取。

添加 mutations 属性

我们在src/store/mutations.js文件中又添加了一些mutation属性,用于用户进行不同的操作进行本地数据的同步。

// ...

    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);
  }
};

// ...

上述添加的都是有关商品的mutation属性:UPDATE_PRODUCTUPDATE_PRODUCT_SUCCESSADD_PRODUCT以及ADD_PRODUCT_SUCCESS分别表示更新商品信息,更新商品信息成功,添加商品以及添加商品成功。

小结

这一节我们学习了如何抽出Actions逻辑,减轻store实例中的负载:

  • 首先我们需要创建actionsJS文件,在文件中定义不同类型的Actions对象并导出,然后在Actions对象中定义相应的一些属性。
  • storeindex文件中导入这些Actions对象,并在store实例的actions属性中通过对象展开运算符混入这些对象。
  • 我们可以使用this.$store.actions.属性的方式调用。

干掉 mutation-types 硬编码

这一节我们主要是进一步完善我们的项目功能以及去掉一些硬编码。

创建 ManufacturerForm 组件

和商品信息展示功能一样,我们也需要将制造商信息展示部分封装到一个单独的组件ManufacturerForm中,以便我们在新建制造商和编辑制造商组件中都能复用该组件。

因此我们创建了src/components/ManufacturerForm.vue文件,用于展示制造商信息的表单组件。

<template>
  <form @submit.prevent="saveManufacturer">
    <div class="form-group">
      <label>Name</label>
      <input
        type="text"
        placeholder="Name"
        v-model="model.name"
        name="name"
        class="form-control" />
    </div>

    <div class="form-group new-button">
      <button class="button">
        <i class="fa fa-pencil"></i>
        <!-- Conditional rendering for input text -->
        <span v-if="isEditing">Update Manufacturer</span>
        <span v-else>Add Manufacturer</span>
      </button>
    </div>
  </form>
</template>

<script>
export default {
  props: ['model', 'isEditing'],
  methods: {
    saveManufacturer() {
      this.$emit('save-manufacturer', this.model)
    }
  }
}
</script>

该组件通过父子组件传值从父组件获取到了modelisEditing的值,并将model对象的信息展示在表单中。

表单信息中还通过v-if来判断isEditing的值是true还是false,如果是true则创建Update Manufacturer,反之创建Add Manufacturer

当用户提交表单时触发saveManufacturer事件,此时会向父组件发送类型为save-manufacturer的事件通知其保存此次的修改操作。

重构 getters 文件

在创建编辑制造商组件之前,我们需要在getters文件中添加对应的getter属性。

我们在src/store/getters.js文件的manufacturerGetters 对象中又添加了一个manufacturerById方法,用于获取本地中指定的制造商。

// ...
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;
    }
  }
}

manufacturerById方法中的id参数是Vue视图层通过方法调用时传入的id,通过这个id判断本地中是否存在该制造商,如果存在则返回该制造商,如果不存在则返回一个空对象。

创建 EditManufacturers 组件

在创建了展示制造商信息的表单组件ManufacturerForm以及添加了用于获取本地指定制造商数据的getter属性之后,紧接着我们又创建了src/pages/admin/EditManufacturers.vue文件,用于修改制造商信息。

<template>
  <manufacturer-form
    @save-manufacturer="addManufacturer"
    :model="model"
    :isEditing="true"
  >
  </manufacturer-form>
</template>

<script>
import ManufacturerForm from '@/components/ManufacturerForm.vue';
export default {
  created() {
    this.$store.dispatch('manufacturerById', {
      manufacturerId: this.$route.params['id']
    });
  },
  computed: {
    model() {
      const manufacturer = this.$store.getters.manufacturerById(this.$route.params['id']);

      // 这里返回 product 的拷贝,是为了在修改 product 的拷贝之后,在保存之前不修改本地 Vuex stire 的 product 属性
      return { ...manufacturer };
    }
  },
  methods: {
    addManufacturer(model) {
      this.$store.dispatch('updateManufacturer', {
        manufacturer: model,
      })
    },
  },
  components: {
  'manufacturer-form': ManufacturerForm
  }
}
</script>

该组件刚被创建时将当前处于激活状态的路由对象的id参数作为载荷分发到类型为manufacturerByIdaction中,在action中进行异步操作从服务器获取对应制造商,然后将该制造商提交到对应mutation中进行本地状态修改,将获取到的制造商保存到本地。

我们定义了计算属性model返回manufacturer的拷贝,是为了在修改manufacturer的拷贝之后,在保存之前不修改本地 store中的manufacturer属性。这里以方法访问的形式从getters中通过当前激活的路由对象中的id参数获取本地状态中的对应制造商作为manufacturer的拷贝,并返回给计算属性model,然后传给子组件ManufacturerForm

该组件在addManufacturer事件中将子组件传入的新制造商对象作为载荷分发到类型为updateManufactureraction中,在action中进行异步操作修改后端对应的商品信息,然后将新对象提交到对应的mutation中进行本地状态修改,修改本地状态中的manufacturer对象。

创建 NewManufacturers 组件

同样的我们继续创建了src/pages/admin/NewManufacturers.vue文件,用于添加制造商信息。该组件和添加商品信息组件代码逻辑类似。

<template>
  <manufacturer-form
    @save-manufacturer="addManufacturer"
    :model="model"
  >
  </manufacturer-form>
</template>

<script>
import ManufacturerForm from '@/components/ManufacturerForm.vue';
export default {
  computed: {
    model() {
      return {};
    }
  },
  methods: {
    addManufacturer(model) {
      this.$store.dispatch('addManufacturer', {
        manufacturer: model,
      })
    },
  },
  components: {
  'manufacturer-form': ManufacturerForm
  }
}
</script>

该组件逻辑代码与New.vue组件类似,一个是添加商品组件,一个是添加制造商组件,您可以对比着来看。

重构 Admin 入口文件

之前我们在该入口文件中增加了查看生产商导航,这里我们又增加了添加生产商导航。

// ...
            <li>
              <router-link to="/admin/manufacturers">查看生产商</router-link>
            </li>
            <li>
              <router-link to="/admin/manufacturers/new">添加生产商</router-link>
            </li>
          </ul>
        </div>
        <router-view></router-view>
      // ...

添加路由信息

我们已经创建了添加和修改制造商组件以及添加了对应的入口导航,接着我们需要在该文件中对其进行路由参数配置。

再次进入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,
        },
      ]
    },
    {
      // ...

这里添加制造商的路由配置就是静态路由的配置方式;修改制造商的路由配置采用了动态传参的方式,这里使用的是$route.params对象的Post方式传参。

配置好添加制造商和修改制造商的路由参数之后,我们又可以进行验收啦,运行项目,点击Admin然后再点击添加制造商,我们可以看到添加制造商的表单: 然后再点击查看制造商,我们又可以看到一系列的制造商,并且每个制造商都对应的有修改和删除操作,这里我们随便选择一个制造商进行修改,就会看到该制造商的表单信息:

创建 mutation-types 文件

这一节我们将对我们的项目代码进行优化,干掉一些硬编码。

我们都知道在actions文件和mutations文件中有一部分的事件类型是需要保持一致的,比如在我们在视图层分发一个添加商品事件ADD_PRODUCT,在actions文件中就需要有对应事件类型的action接收,然后向后端发起请求并将请求结果提交到对应类型的mutation中,这就要求了这几个文件中的对应事件类型都要保持一致。可是我们在开发过程中难免会出错,比如漏掉一个字母就会导致两个文件中的对应事件无法接收,尴尬的是控制台也没有报错,这就造成了我们很难查错。

因此,我们采用了字符串常量的形式定义actions文件和mutations文件中的事件类型,只要我们写错一个单词都会导致字符串常量不一致,关键的是这个时候会报错,利于我们查错。

进而我们创建了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,
      })
    })
  }
}

这里我们首先导入了mutation-types文件中定义的一些字符串常量,替换掉了对应的事件类型。

重构 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);
  }
}

这里我们首先导入了mutation-types文件中定义的一些字符串常量,替换掉了对应的事件类型。

小结

这一节我们主要做了以下工作:

  • 创建了制造商相关组件并配置了相应路由参数,实现了新建和修改制造商信息;
  • 利用字符串常量代替actions文件和mutations文件中的事件类型;
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦


一只图雀
863 声望1.2k 粉丝

我们图雀社区是一个供大家分享用 Tuture 写作工具撰写教程的一个平台。在这里,读者们可以尽情享受高质量的实战教程,并且与作者和其他读者互动和讨论;而作者们也可以借此传播他们的技术知识,宣传他们的开源项目。