引子
json-server 为前端带来后端服务
- 全局安装 json-server 工具
yarn global add json-server
- 新建一个 json 文件夹
cd db
{ "cart": [ { "id": 1, "name": "“小金龙”龙年款实战贾莫兰特男子篮球鞋", "price": 899, "count": 14, "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/a43f1f52-6850-4cab-837f-b93ff752f16d/ja-1-ep-%E5%B0%8F%E9%87%91%E9%BE%99%E9%BE%99%E5%B9%B4%E6%AC%BE%E5%AE%9E%E6%88%98%E8%B4%BE%E8%8E%AB%E5%85%B0%E7%89%B9%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-ZLQQx9.png" }, { "id": 4, "name": "LeBron XXI EP 男子篮球鞋", "price": 1099, "count": 1, "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/ba3a7f48-77d9-49aa-ad2b-24d0df830bac/lebron-21-ep-%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-wK6QND.png" }, { "id": 5, "name": "Jordan Nu Retro 1 Low 复刻男子运动鞋", "price": 599, "count": 4, "thumb": "https://static.nike.com.cn/a/images/t_PDP_1280_v1/f_auto,b_rgb:f5f5f5,u_126ab356-44d8-4a06-89b4-fcdcc8df0245,c_scale,fl_relative,w_1.0,h_1.0,fl_layer_apply/17313c9a-52e8-4ade-b899-2e25f4e8e516/jordan-nu-retro-1-low-%E5%A4%8D%E5%88%BB%E7%94%B7%E5%AD%90%E8%BF%90%E5%8A%A8%E9%9E%8B-SsFwr0.png" }, { "id": 6, "name": "Nike SB Force 58 男/女滑板鞋", "price": 399, "count": 1, "thumb": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/30ceab71-d94b-4cef-a768-d41bef344002/sb-force-58-%E7%94%B7-%E5%A5%B3%E6%BB%91%E6%9D%BF%E9%9E%8B-kkk6cJ.png" } ] }
- 进入文件目录,启动后端接口服务
json-server --watch index.json
Demo 功能分析
- 动态渲染购物车,购物车List存放于Vuex进行管理
- 商品项的数字空间控制商品的数量
- 动态计算商品数量及总价
- 移除某一个商品
- 清空购物车
基于脚手架创建项目
使用 VUEX 的一个思路
想象每个组件都分别为家中的成员:爸爸、妈妈、孩子们。但是,作为一个家庭,他们需要共享状态。在这个家庭中,充当看家狗的Vuex就是来帮助我们解决问题的。
当妈妈在超市看到打折的纸巾【理解为前端页面】,她就像是"dispatch"一个"action",也就是发送一个消息说:“我今天会买一大包纸巾。”,把这个消息告诉看家狗(Vuex的store), 看家狗听到了,理解了,然后对这条消息进行核查,“mutation”。核查没问题后,看家狗就会更新家庭购物清单的状态,也就是把纸巾加入购物清单。
然后,爸爸和孩子们,也就是其他的组件,就可以从看家狗那里获取最新的购物清单,来获取纸巾的购买消息,以确保不会重复购买。
不论是小组件还是大组件,只要知道这个购物清单的修改,都可以避免重复购买,从而达到整个大家庭数据共享,而且状态始终跟新,始终一致。这一切,都得益于我们可爱、忠诚、聪明的看家狗——Vuex。
模块化管理 VUEX
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import cart from './modules/cart.js' Vue.use(Vuex) export default new Vuex.Store({ modules: { cart } })
store/modules/carts.js
import axios from 'axios' export default { namespaced: true, state () { return { // 购物车数据的存储结构 [{},{}] list: [] } }, mutations: { updateList (state, newList) { state.list = newList }, updateCount (state, obj) { // 根据 Id 找到对应的对象,更新 count 属性即可 const goods = state.list.find(item => item.id === obj.id) goods.count = obj.newCount }, delClickItem (state, id) { state.list = state.list.filter(item => item.id !== id) }, clear (state) { state.list = [] } }, actions: { // 请求方式 get // 请求地址 http://localhost:3000/cart async getList (context) { const res = await axios.get('http://localhost:3000/cart') context.commit('updateList', res.data) }, // 请求方式 patch // 请求地址 http://localhost:3000/cart/:id // 请求参数: // { // name: 新值 【可选】 // price: 新值 【可选】 // count: 新值 【可选】 // thumb: 新值 【可选】 // } async updateCountAsync (context, obj) { // 将修改更新同步到后台服务器 await axios.patch(`http://localhost:3000/cart/${obj.id}`, { count: obj.newCount }) // 将修改更新同步到 vuex context.commit('updateCount', { id: obj.id, newCount: obj.newCount }) }, async delItem (context, id) { await axios.delete(`http://localhost:3000/cart/${id}`).then((res) => { if (res.status === 200) { context.commit('delClickItem', id) } }) }, async clearAllItem (context) { const deletePromises = context.state.list.map(item => axios.delete(`http://localhost:3000/cart/${item.id}`)) await Promise.all(deletePromises) context.commit('clear') } }, getters: { // 商品总数量 累加count total (state) { return state.list.reduce((sum, item) => sum + item.count, 0) }, // 商品总价格 累加count * price totalPrice (state) { return state.list.reduce((sum, item) => sum + item.count * item.price, 0) } } }
App.vue
- cart-header
- cart-item
- cart-footer
<template> <div class="app-container"> <!-- Header 区域 --> <cart-header></cart-header> <!-- 商品 Item 项组件 --> <cart-item v-for="item in list" :key="item.id" :item="item"></cart-item> <!-- Foote 区域 --> <cart-footer></cart-footer> </div> </template> <script> import CartHeader from '@/components/cart-header.vue' import CartFooter from '@/components/cart-footer.vue' import CartItem from '@/components/cart-item.vue' import { mapState } from 'vuex' export default { name: 'App', created () { this.$store.dispatch('cart/getList') }, computed: { ...mapState('cart', ['list']) }, components: { CartHeader, CartFooter, CartItem } } </script> <style lang="less" scoped> .app-container { padding: 50px 0; font-size: 14px; } </style>
一加载页面发起请求,从服务器拿到购物车的商品信息进行购物车列表的渲染
created () { this.$store.dispatch('cart/getList') },
由于我们使用了 VUEX 来进行管理,所以在
cart-item
<template> <div class="goods-container"> <!-- 左侧图片区域 --> <div class="left"> <img :src="item.thumb" alt="" class="avatar"> <span @click="delItem(item.id)">x</span> </div> <!-- 右侧商品区域 --> <div class="right"> <!-- 标题 --> <div class="title">{{ item.name }}</div> <div class="info"> <!-- 单价 --> <span class="price">{{ item.price }}</span> <div class="btns"> <!-- 按钮区域 --> <button class="btn btn-light" @click="btnClick(-1)">-</button> <span class="count">{{ item.count }}</span> <button class="btn btn-light" @click="btnClick(1)">+</button> </div> </div> </div> </div> </template> <script> export default { name: 'CartItem', methods: { btnClick (step) { const newCount = this.item.count + step const id = this.item.id console.log(id, newCount) if (newCount < 1) return this.$store.dispatch('cart/updateCountAsync', { id, newCount }) }, delItem (id) { try { this.$store.dispatch('cart/delItem', id) } catch (err) { console.log(err) } } }, props: { item: { type: Object, required: true } } } </script> <style lang="less" scoped> .goods-container { display: flex; padding: 10px; + .goods-container { border-top: 1px solid #f8f8f8; } .left { position: relative; &:hover { span { opacity: 1; } } .avatar { width: 100px; height: 100px; } margin-right: 10px; span { opacity: 0; transition: all .5s; position: absolute; top: 5px; right: 5px; width: 20px; height: 20px; border-radius: 50%; text-align: center; line-height: 20px; background-color: rgba(0,0,0,.1); color: rgba(0,0,0,.6); } } .right { display: flex; flex-direction: column; justify-content: space-between; flex: 1; .title { font-weight: bold; } .info { display: flex; justify-content: space-between; align-items: center; .price { color: red; font-weight: bold; } .btns { .count { display: inline-block; width: 30px; text-align: center; } } } } } .custom-control-label::before, .custom-control-label::after { top: 3.6rem; } </style>
cart-footer
<template> <div class="footer-container"> <!-- 中间的合计 --> <div> <span>共 {{ total }} 件商品,合计:</span> <span class="price">{{ totalPrice }}</span> </div> <!-- 右侧结算按钮 --> <button class="btn btn-success btn-settle" @click="clearList">结算</button> </div> </template> <script> import { mapGetters } from 'vuex' export default { name: 'CartFooter', computed: { ...mapGetters('cart', ['total', 'totalPrice']) }, methods: { clearList () { this.$store.dispatch('cart/clearAllItem') } } } </script> <style lang="less" scoped> .footer-container { background-color: white; height: 50px; border-top: 1px solid #f8f8f8; display: flex; justify-content: flex-end; align-items: center; padding: 0 10px; position: fixed; bottom: 0; left: 0; width: 100%; z-index: 999; } .price { color: red; font-size: 13px; font-weight: bold; margin-right: 10px; } .btn-settle { height: 30px; min-width: 80px; margin-right: 20px; border-radius: 20px; background: #000; border: none; color: white; } </style>
cart-header
<template> <div class="header-container">购物车</div> </template> <script> export default { name: 'CartHeader' } </script> <style lang="less" scoped> .header-container { height: 50px; line-height: 50px; font-size: 16px; background-color: #000; text-align: center; color: white; position: fixed; top: 0; left: 0; width: 100%; z-index: 999; } </style>
效果预览
小结
这是一个比较简单的 Demo