在一个电商网站中,购物车在很多页面都需要用到,因此非常适合放在 Vuex 的 store 中进行集中管理。在本项目中,采用模块化的方式管理应用中不同的状态。
在项目的 store 目录下新建 modules 文件夹,在该文件下新建 cart.js。如下:
store/modules/cart.js
const state = {
items: []
}
//mutations
const mutations = {
//添加商品到购物车中
pushProductToCart(state, { id, imgUrl, title, price, quantity }) {
if (!quantity)
quantity = 1;
state.items.push({ id, imgUrl, title, price, quantity });
},
//增加商品数量
incrementItemQuantity(state, { id, quantity }) {
let cartItem = state.items.find(item => item.id == id);
cartItem.quantity += quantity;
},
//用于清空购物车
setCartItems(state, { items }) {
state.items = items
},
//删除购物车中的商品
deleteCartItem(state, id) {
let index = state.items.findIndex(item => item.id === id);
if (index > -1)
state.items.splice(index, 1);
}
}
//getters
const getters = {
//计算购物车中所有商品的总价
cartTotalPrice: (state) => {
return state.items.reduce((total, product) => {
return total + product.price * product.quantity
}, 0)
},
//计算购物车中单项商品的价格
cartItemPrice: (state) => (id) => {
if (state.items.length > 0) {
const cartItem = state.items.find(item => item.id === id);
if (cartItem) {
return cartItem.price * cartItem.quantity;
}
}
},
//获取购物车中商品的数量
itemsCount: (state) => {
return state.items.length;
}
}
//actions
const actions = {
//增加任意数量的商品到购物车
addProductToCart({ state, commit },
{ id, imgUrl, title, price, inventory, quantity }) {
if (inventory > 0) {
const cartItem = state.items.find(item => item.id == id);
if (!cartItem) {
commit('pushProductToCart', { id, imgUrl, title, price, quantity })
} else {
commit('incrementItemQuantity', { id, quantity })
}
}
}
}
export default {
namespaced: true,
state,
mutations,
getters,
actions
}
items 数组用于保存购物车中所有商品信息的状态属性。
接下来,编辑 store 目录下的 index.js ,导入 cart 模块。如下:
store/index.js
import { createStore } from 'vuex'
import cart from './modules/cart'
import createPersistedState from "vuex-persistedstate"
export default new Vuex.Store({
modules: {
cart
},
plugins: [createPersistedState()]
})
在刷新浏览器窗口时,store 中存储的状态信息会被重置,这样就会导致加入购物车中的商品信息丢失。所以一般会选择一种浏览器端持久存储方案解决这个问题,比较常见且简单的方案就是 localStorage ,保存在 store 中的状态信息也要同步加入 localStorage ,在刷新浏览器窗口前,或者用用户重新访问网站时,从 localStorage 中读取状态信息保存到 store 中。
在整个应用期间,需要考虑各种情况下 store 与 localStorage 数据同步的问题,这比较麻烦。为此,可以使用一个第三方的插件解决 store 与 localStorage 数据同步的问题,即 vuex-persistedstate 插件。
首先安装 vuex-persistedstate 插件,在 Visual Studio Code 的终端窗口中执行以下命令进行安装。
npm install vuex-persistedstate -S
vuex-persistedstate 插件的使用非常简单,只需要两句代码就可以实现 store 的持久化存储,这会将整个 store 的状态以 vuex 为键名存储到 localStorage 中。
如果只想持久化存储 store 中的部分状态信息,那么可以在调用 createPersistedState() 方法时传递一个选项对象,在该选项对象的 reducer() 函数中返回要存储的数据。例如:
plugins:[createPersistedState({
reducer (data){
return {
// 设置只存储 cart 模块中的状态
cart:data.cart,
// 或者设置只存储 cart 模块中的 items 数据
// products:data.cart.items
}
}
})]
reducer() 函数的 data 参数是完整的 state 对象。
如果想改变底层使用的存储机制,如使用 sessioniStorage,那么可以在选项对象中通过 storage 指定。代码如下:
plugins:[createPersistedState({
reducer (data){
storage:window.sessionStorage,
...
}
})]
配置好 Vuex 的状态管理后,就可以开始编写购物车组件了。
在 views 目录下新建 ShoppingCart。如下:
views/ShoppingCart.vue
商品名称
单价
数量
金额
操作
{{ book.title }}
{{ currency(book.price) }}
{{ book.quantity }}
{{ currency(cartItemPrice(book.id)) }}
总价:{{ currency(cartTotalPrice) }}
ShoppingCart 组件提供了两种方式删除购物车中的某项商品:
(1)单击“删除”按钮,将直接删除购物车中的该商品
(2)用户单击数量下的减号按钮时,如果判断数量减一后为零,则删除该商品
在购物车页面中单击“结算”按钮,则进入结算页面,结算页面再一次列出购物车中的所有商品,不同的是,在结算页面不能再对商品进行修改。
在 views 目录下新建 Checkout.vue。如下:
views/checkout.vue
{{ msg }}
商品结算
商品名称
单价
数量
金额
{{ book.title }}
{{ currency(book.price) }}
{{ book.quantity }}
{{ currency(cartItemPrice(book.id)) }}
总价:{{ currency(cartTotalPrice) }}
在线支付涉及各个支付平台或银联的调用接口,所以本项目的购物车流程到这一步就结束了,当用户单击“付款”按钮时,只是简单地清空购物车,稍后提示用户“付款成功”。
在实际场景中,当用户提交购物订单准备结算时,系统会判断用户是否已经登录,如果没有登录,会提示用户先进行登录,本节实现用户注册和用户登录组件。
用户登录后的状态需要保存,不仅可以用于向用户显示欢迎信息,还可以用于对受保护的资源进行权限验证。同样,用户的状态存储也使用 Vuex 管理。
在 store/modules 目录下新建 user.js 。如下:
store/modules/user.js
const state = {
user: null
}
// mutations
const mutations = {
saveUser(state, { username, id }) {
state.user = { username, id }
},
deleteUser(state) {
state.user = null;
}
}
export default {
namespaced: true,
state,
mutations,
}
对于前端,存储用户名和用户 ID 已经足以,像用户中心等功能的实现,是需要重新向服务端去请求数据的。
编辑 store/index.js 文件,导入 user 模块,并在 modules 选项下进行注册。如下:
store/index.js
import { createStore } from 'vuex'
import cart from './modules/cart'
import user from './modules/user'
import createPersistedState from "vuex-persistedstate"
export default createStore({
modules: {
cart,
user
},
plugins: [createPersistedState()]
})
当用户单击 Header 组件中的 “注册”链接时,将跳转到用户注册页面。
在 components 目录下新建 UserRegister.vue 。如下:
components/UserRegister.vue
红框处在这里实现了一个功能,当用户输入用户名时,实时去服务端检测该用户名是否已经存在,如果存在,则提示用户,这是通过 Vue 的监听器来实现的。
不过由于 v-model 指令内部实现机制的原因(对于文本输入框,默认绑定的是 input 事件),如果用户快速输入或快速用退格键删除用户名时,监听器将触发多次,由此导致频繁地向服务端发起请求。为了解决这个问题,可以利用 axios 的 cancel token 取消重复的请求。
使用 axios 发送请求时,可以传递一个配置对象,在配置对象中使用 cacelToken 选项,通过传递一个 executor() 函数到 CancelToken 的构造函数中创建 cancel token。
将 cancel() 函数保存为组件实例的方法,之后如果要取消请求,调用 this.cancel() 即可。cancel() 函数可以接收一个可选的消息字符串参数,用于给出取消请求的原因。同一个 cancel token 可以取消多个请求。
在发生错误时,可以在 catch() 方法中使用 this.axios.isCancel(error) 判断该错误是否是由取消请求而引发的。
当然,这里也可以通过修改 v-model 的监听事件为 change 解决快速输入和删除导致的重复请求问题,只需要给 v-model 指令添加 .lazy 修饰符即可。
用户名是否已注册的判断,请求的服务端数据接口如下:
http://111.229.37.167/api/user/{用户名}
返回的数据结构如下:
{
"code": 200,
"data": true //如果要注册的用户名存在,则返回 false
}
用户注册请求的服务端数据接口如下:
http://111.229.37.167/api/user/register。
需要采用 Post() 方法向该接口发起请求,提交的数据是一个 JSON 格式的对象,该对象要包含 username、password 和 mobile 三个字段。
返回的数据结构如下:
{
"code":200,
"data":{
"id":18,
"username":"小鱼儿",
"password":"1234",
"mobile":"13222222222"
}
}
实际开发时,服务端不把密码返回给前端,如果前端需要用到密码,则可以采用加密形式传输。
当用户注册成功后,将用户名和 ID 保存到 store 中,并跳转到根目录下,即网站的首页。然后 Header 组件会自动渲染出用户名,显示欢迎信息。
当用户单击 Header 组件中的“登录”链接时,将跳转到用户登录页面。
在 components 目录下新建 UserLogin.vue 。如下:
components/UserLogin.vue
{{ message }}
用户登录组件并不复杂,值得一提的就是在用户登录后需要跳转到进入登录页面前的路由,这会让用户体验更好,实现方式已经在 14.10.1 小节介绍过了,本项目也是利用 beforeEach() 注册的全局前置守卫保存用户登录前的路由路径,可以参看 17.11 节。
用户登录请求的数据接口如下:
http://111.229.37.167/api/user/login
同样是以 Post() 方法发起请求,提交的数据是一个 JSON 格式的对象,该对象要包含 username 和 password 两个字段。
返回的数据格式与用户注册返回的数据格式相同。