引子
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