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