【练习】基于Vue全家桶的仿小米商城系统

网友投稿 282 2022-09-22

【练习】基于Vue全家桶的仿小米商城系统

目录

​​1. 项目概述​​​​2. 项目基础架构​​

​​2.1 跨域及解决​​​​2.2 项目目录结构​​​​2.3 插件的安装​​​​2.4 storage封装​​​​2.5 接口错误拦截​​​​2.6 Mock设置​​

​​3. 商城首页的实现​​​​4. 登录页面的实现​​​​5. Vuex的使用​​​​6. 产品站的实现​​​​7. 退出功能的实现​​​​8. Element UI的使用​​​​9. 订单确认页面的实现​​​​10. 订单支付功能的实现​​

​​11. 订单列表的实现​​

​​11.1 分页器​​​​11.2 按钮加载​​​​11.3 滚动加载​​

​​12. 项目优化​​​​13. 总结​​

前言: 本文是对整个练习过程的记录,记录重点知识以及不太了解的知识。

1. 项目概述

本次练习是基于Vue全家桶的仿小米商城系统,商城的流程如下:

登录 -> 产品首页 -> 产品站 -> 产品详情购物车 -> 订单确认 -> 订单支付 -> 订单列表

总共有上面的八个页面,还有若干个组件。

商城系统整体架构图:

2. 项目基础架构

2.1 跨域及解决

跨域时浏览器为了安全而做出的限制策略,浏览器请求必须遵循同源策略:同域名、同端口、同协议。

这里使用接口代理的方式来解决跨域问题:

接口代理就是通过修改Nginx服务器配置来实现(前端修改,后台不变)

在根目录创建配置文件:​​vue.config.js​​,在里面配置以下内容:

module.exports = { devServer:{ host:'localhost', port:8080, proxy:{ '/api':{ target:'url', changeOrigin:true, pathRewrite:{ '/api':'' } } } }}// 注意:在target里面需要写上接口代理的目标地址,这里就不写了,用url代替

原理: 因为我们需要使用的接口地址可能很多,不可能挨个去进行拦截。所以,这里可以设置一个虚拟的地址​​/api​​​,实际上,是没有这个地址的。当拦截到​​/api​​​时,就将主机的点设置为原点(changeOrigin:true),然后添加路径的转发规则,将​​/api​​​ 置为空,转发时就没有​​/api​​了

2.2 项目目录结构

2.3 插件的安装

需要注意的是:​​axios​​​是一个库,并不是vue中的第三方插件,所以使用时需要在每个页面进行导入操作,这样就很麻烦。我们可以使​​vue-axios​​​将​​axios​​​的作用域对象挂载到vue实例中,这样就可以在需要使用的时候用​​this​​来调用。

Vue.use(VueAxios, axios)

2.4 storage封装

Cookie、localStorage、 sessionStorage三者 区别? 这个问题可以参考之前写的一个总结:​​​链接地址​​

storage本身虽然有API,但是只是简单的key/value形式,storage只能存储字符串,需要手工转化为json对象,并且storage只能一次性的清空,不能进行单个的清空,所有我们需要对storage进行封装。

这里封装的是sessionStorage,实际上就是可以在sessionStorage存储JSON对象,并且可以对这些对象进行一些操作:

// 设置一个keyconst STORAGE_KEY = 'mall';export default{ // 存储值 setItem(key,value,module_name){ if (module_name){ // 如果模块名称存在,就递归找到这个模块,然后给这个模块设置key和value let val = this.getItem(module_name); val[key] = value; // 最后将设置的值存储到整个数据中 this.setItem(module_name, val); }else{ // 如果模块名称不存在,就直接进行设置,并存储在sessionStorage中 let val = this.getStorage(); val[key] = value; window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val)); } }, // 获取某一个模块下面的属性 getItem(key, module_name){ // 如果模块的名称存在,就获取模块的名称,并将返回该模块中某个key的value值 if (module_name){ // 这里是不断往内层遍历,直到寻找到那个模块 let val = this.getItem(module_name); if(val) { return val[key]; } } // 如果模块名称不存在,就直接返回该key的value值 return this.getStorage()[key]; }, // 获取Storage的信息 getStorage(){ // 获取sessionStorage中的整个数据,并将其转化为对象的形式 return JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '{}'); }, // 清空某一个值 clear(key, module_name){ // 首先要获取到整个对象的值 let val = this.getStorage(); // 如果这个模块存在 if (module_name){ // 如果这个模块的值为空,就直接返回 if (!val[module_name])return; // 否则就删除这个模块中的key对应的值 delete val[module_name][key]; }else{ // 如果模块不存在,就说明key就在第一层,直接删除key delete val[key]; } // 删除之后,将删除之后的值设置到sessionStorage中 window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val)); }}

举个例子来解释一下上面的模块的概念:

mall = { 'a': 1, 'b':{ 'c': 2, 'd':{ 'e': 3 } }}

在这个JSON对象中,如果我们想找到e,就需要进行递归,找到d模块中的key(也就是e),然后取出他的值。

2.5 接口错误拦截

对于接口请求,我们要将错误进行统一处理:

统一报错未登录统一拦截请求值、返回值统一处理

// 由于使用的是接口代理的方式进行跨域,所以这里baseURL设置为/api,超时时间设置为8saxios.defaults.baseURL = '/api'axios.defaults.timeout = 8000// 接口错误拦截,根据接口返回状态码,来进行不同的处理(状态码是后台设置的)axios.interceptors.response.use(function(response){ let res = response.data let path = location.hash if(res.status == 0){ return res.data }else if(res.status == 10){ if(path !== '#/index'){ window.location.href = '/#/login' } }else{ alert(res.msg) // 抛出异常,避免返回的错误信息进入成功的结果中 return Promise.reject(res) }})

2.6 Mock设置

在开发阶段,我们可能还不能拿到API文档,所以可以使用Mock模拟数据来进行数据的交互操作。Mock有以下特点:

开发阶段,为了提高效率,需要提前Mock减少代码冗余,灵活插拔较少沟通,减少接口联调时间

使用mock的方法有很多:

本地创建json:在本地创建json文件,然后进行调用easy-mock平台:将baseURL设置为easy-mock的接口地址,调用时和正常调用一样集成Mock API

(1)首先要安装mockjs:​​npm install mockjs --save-dev​​​’ (2)在src中建Mock的API:src/mock/api.js

import Mock from 'mockjs'Mock.mock('/api/user/login', { //接口数据...})

(3)之后在main.js设置一个mock的开关:

const mock = trueif(mock){ require('./mock/api')}

需要注意的是​​require​​​和​​import​​​是不同的,​​import​​是编译的时候就进行加载,而require是执行到这句代码的时候才执行。

3. 商城首页的实现

(1)由于在布局时,很多地方出现了代码的重复,所以可以建一个​​mixin​​文件,来定义一些css函数,再在样式中引用。例如,我们多次使用到了flex布局,多次使用到了背景图片的设置,可以定义一个函数(定义函数时,可以设置一些默认值):

@mixin flex($hov:space-between,$col:center){ display:flex; justify-content:$hov; align-items:$col;}@mixin bgImg($w:0,$h:0,$img:'',$size:contain){ display:inline-block; width:$w; height:$h; background:url($img) no-repeat center; background-size:$size;/*使用定义的函数*/@include bgImg(18px,18px,'/imgs/icon-search.png');@include flex();

注意:

不传参数就意味着使用默认值。使用之前要引入​​mixin.scss​​文件

(2)因为使用到的字体大小、颜色值有很多重复的,所以可以建立一个config.scss文件,来定义一些常用的字体大小和颜色值:

使用:

color:$colorA;font-size: $fontA;

(4)首页轮播图使用的是swiper,但是在编译报错​​"Can’t resolve ‘swiper/dist/css/swiper.css’"​​,经查,是因为swiper版本过高的问题,在安装vue-awesome-swiper的时候,会自动安装一个swiper。默认swiper是最高版本,但是我们此时使用的不是最高版本。最后的解决方法是,重新安装指定的vue-awesome-swiper和swiper,问题就解决了:

npm install swiper vue-awesome-swiper@3.1.3 --save-devnpm install swiper swiper@3.4.2 --save-dev

(4)在首页,总共使用到了四个组件:头部导航栏、底部信息栏、下方服务条、弹窗组件。因为这些组件不仅会在首页使用,还会在其他的页面使用,所以把他们都拆分出来,在需要的时候进行引用。这里说一下弹窗组件。

弹窗组件总共分为三部分:左上角的弹窗标题,中间的弹窗内容,下方的按钮。因为在每个页面中使用的弹窗可能不太一样,所以要把每一部分都定义成活的,便于修改,中间的内容区域定义成插槽。

弹窗结构:

数据传值: 这里是父组件向子组件传值,使用props接收。

export default { name: 'modal', props: { // 弹框类型:小small、中middle、大large、表单form modalType: { type: String, default: 'form' }, // 弹框标题 title: String, // 按钮类型: 1:确定按钮 2:取消按钮 3:确定取消 btnType: String, sureText: { type: String, default: '确定' }, cancelText: { type: String, default: '取消' }, showModal: Boolean } }

首页使用弹窗:

// 新版本的vue插槽必须使用template来包含内容,这里使用的是具名插槽

这里对使用Vue的过渡动画来实现弹窗的过渡效果:

(这里就只写一下过渡效果的代码)

.modal{ @include position(fixed); z-index: 10; transition: all .5s; &.slide-enter-active{ top:0; } &.slide-leave-active{ top:-100%; } &.slide-enter{ top:-100%; }}

需要注意的是,使用动画的部分必须要用​​​​标签进行包裹,在定义动画的时候,在以下类名中定义:

​​v-enter​​:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。​​v-enter-active​​:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。​​v-enter-to​​:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。​​v-leave​​:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。​​v-leave-active​​:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。​​v-leave-to​​:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果使用一个没有名字的 ​​​​​,则 ​​v-​​​ 是这些类名的默认前缀。因为这里定义​​name​​​为​​slide​​​,所以以​​slide-​​开头。

(5)图片懒加载

适用于片的懒加载可以在一定程度上提高网页的性能。在vue中使用图片的懒加载还是比较简单的,来看一些具体的步骤:

安装​​vue-lazyload​​插件在​​mian.js​​​ 中引入:​​import VueLazyLoad from 'vue-lazyload'​​注册并配置插件:

Vue.use(VueLazyLoad, { loading: '/imgs/loading-svg/loading-bars.svg' // 设置了一个加载时的动画效果})

使用​​vue-lazyload​​​:将​​:src​​​换成​​v-lazy​​即可

// 原来

{{title}}

吸顶的实现:

data(){ return { isFixed: false } }, mounted(){ // 监听页面的滚动事件 window.addEventListener('scroll', this.initHeight) }, destroyed(){ // 销毁页面的滚动事件 window.removeEventListener('scroll', this.initHeight) }, methods: { initHeight(){ // 定义事件的监听的内容 let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; this.isFixed = scrollTop > 152; } }

其中, pageYOffset 属性返回文档在窗口垂直方向滚动的像素。如果找不到就找滚动的距离,chrome中使用​​document.documentElement.scrollTop​​​,IE浏览器使用​​document.body.scrollTop​​来定义。由于上面Header组件的高度为152px,所以只要滚动距离大于152,就给组件添加定位属性。

定位实现:

.nav-bar{ &.is_fixed{ position: fixed; top: 0; width: 100%; } }

(2)视频动画实现

视频内容的基本结构:

// 遮罩层
// 视频盒子 // 关闭按钮 // 视频

.video-box{ // 定义进入的动画 @keyframes{ from{ top:-50%; opacity:0; } to{ top:50%; opacity:1; } } // 定义出去的动画 @keyframes{ from{ top:50%; opacity:1; } to{ top:-50%; opacity:0; } } .video{ position:fixed; top:-50%; left:50%; transform:translate(-50%,-50%); z-index:10; width:1000px; height:536px; opacity:1; // 执行完动画之后, top还会变回-50%,所以需要我们手动设置为50% &.slideDown{ animation:slideDown .6s linear; top:50%; } &.slideUp{ animation:slideUp .6s linear; } }

​​animation​​三个参数分别是:动画的名称、动画的执行时间、进入的形式(这里是匀速进入)

有一个小问题就是,当关闭视频之后,视频整个盒子还在,所以要对其进行设置:

还有一个小问题尚未解决,没有找到合适的方法,就是关闭视频实际上是将视频放在了我们看不到的地方,实际上视频依旧在播放着,没有暂停,需要手动设置进行暂停。之后看看有没有什么比较好的解决方案…

7. 退出功能的实现

退出功能的实现,需要考虑以下因素:

退出后要清空顶部的用户名称退出后要清空购物车内商品的数量退出后要清空cookie值接口优化

(1)首显示定义退出功能的结构:

退出

(2)逻辑实现

logout(){ this.axios.post('/user/logout').then(() => { // 清空cookie,将cookie过期时间设置为-1,就是立刻失效 this.$cookie.set('userId', '', {expires: '-1'}) // 将Vuex中的用户名称和购物车商品数量进行初始化(清空) this.$store.dispatch('saveUserName', '') this.$store.dispatch('saveCartCount', '0') Message.success('退出成功') })}

但是这样的话,每次进入主页面都会记进行购物车数量请求,会影响性能,而我们只是想在登录之后才进行请求,所以可以在登录时设置一个参数,若主页面接收到这个参数,说明是从登录页面过来的,就进行数据请求:

getCartCount(){ this.axios.get('/carts/products/sum').then((res=0) => { this.$store.dispatch('saveCartCount', res); }) }// 在 login中设置一个from参数吗,这里使用params传参,这样的话,跳转路径必须使用名称的形式 this.$router.push({ name: 'index', params: { from: 'login' } })// 接收参数,如果参数是login,就进行数据的请求 mounted(){ let params = this.$route.params if(params && params.from == 'login'){ this.getCartCount() } },

(4)优化

在​​APP.vue​​中,我们默认每次打开页面时就进行数据请求,如果没有登录,就会报错,这样实际上也会造成资源的浪费,我们可以判断是否登录,只有登录状态下才进行请求:

mounted(){ if(this.$cookie.get('userId')){ this.getUser() this.getCartCount() } }

在登陆之后,会储存一个后台的会话ID,它的持续时间为Session(也就是当浏览器关闭之后,就自动清空,结束会话),所以我们的cookie过期时间和它的会话持续时间保持一致就可以了:

this.$cookie.set('userId', res.id, { expires: 'Session' })

8. Element UI的使用

在使用Element UI的时候,按照官网的导入方式,遇到报错 ​​Error: Cannot find module 'babel-preset-es2015'​​​问题,需要在 ​​.babelrc​​ 文件中,进行如下修改:

{ "presets": [["@babel/preset-env", { "modules": false}]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ]}

这样问题就解决了。

9. 订单确认页面的实现

submitAddress(){ // 使用解构赋值来来获取data中的数据 let {checkedItem,userAction} = this; let method,url,params={}; if(userAction == 0){ method = 'post',url = '/shippings'; }else if(userAction == 1){ method = 'put',url = `/shippings/${checkedItem.id}`; }else { method = 'delete',url = `/shippings/${checkedItem.id}`; } //表单验证略过... // params中的参数是在表单中解构赋值出来的 params = {receiverName,receiverMobile,receiverProvince,receiverCity,receiverDistrict,receiverAddress,receiverZip} this.axios[method](url,params).then(()=>{ this.closeModal(); // 关闭弹窗 this.getAddressList(); // 重新刷新地址列表 Message.success('操作成功'); }); }

10. 订单支付功能的实现

10.1 支付宝支付

window.open('/#/order/alipay?orderId='+this.orderId,'_blank')

进入该页面后之后,触发提交的请求,后台会返回一个content,这是包含支付的一个表单代码,触发这段代码就会跳转到支付宝的支付页面。这里我们使用​​document.forms[0].submit()​​来触发这个表单的提交:

// 获取订单的IdorderId:this.$route.query.orderId,// 支付请求paySubmit(){ this.axios.post('/pay', { orderId: this.orderId, orderName: '小米商城', amount: 0.01, payType: 1 }).then((res) => { this.content = res.content setTimeout(() =>{ document.forms[0].submit() }, 100) }) }

这里还使用了一个loading组件来作为从支付页面到支付宝的页面的过渡。

这两步完成之后,就可以跳到了支付宝支付页面,然后就可以进行支付操作了。

我们需要将返回的结果显示页面上,所以创建于了一个ScanPayCode的组件,用来显示二维码。

而想要将链接转化为二维码,需要使用一个插件:​​qrcode​​,

安装:​​npm install --save qrcode​​引入:​​import QRCode from 'qrcode'​​使用:

最后就是轮询订单支付的状态,如果支付完成,就清除定时器,跳到订单列表页面:

loopOrderState(){ // 设置定时器 this.T = setInterval(() => { this.axios.get(`/orders/${this.orderId}`).then((res) => { if(res.status == 20) { clearInterval(this.T) // 清除定时器 this.goOrderList() // 跳转到订单列表 } }) }, 1000); }

之前已经做了异常的拦截,但是那个咋请求成功的基础上,对业务请求进行拦截,而没有对状态码进行拦截,所以要对除200以外的状态码进行拦截:

axios.interceptors.response.use((response) => { let res = response.data let path = location.hash if(res.status == 0){ return res.data }else if(res.status == 10){ if(path !== '#/index'){ window.location.href = '/#/login' } return Promise.reject(res) }else{ Message.warning(res.msg) return Promise.reject(res) }}, (error) => { let res = error.response Message.error(res.data.message) return Promise.reject(error)})

实际上,拦截器的第一个参数方法是对业务请求的拦截,第二个参数方法是对状态信息的拦截,只要获取到错误信息,提示用户,并将错误抛出,避免记性res的状态里,就可以了。

11. 订单列表的实现

订单列表页面主要就是订单的加载,这里记录一下订单加载更多的三种方式。

11.1 分页器

这里使用的是element ui的分页器,在页面按需引入,并注册:

import {Pagination} from 'element-ui' // 由于我们引入的是Pagination,而使用时前面有一个el-,所以使用这种方式加载 components:{ [Pagination.name]: Pagination, }

定义结构

数据交互

handleChange(pageNum){ this.pageNum = pageNum // 更改页面 this.getOrderList() // 刷新列表 }

之前也用过​​element ui​​ 的分页器了,还是比较简单的。来看一下之前没有用到过的方法。

11.2 按钮加载

在最底部放一个“加载更多”的按钮,这里也使用element uI的button:

import { Button } from 'element-ui' components:{ [Button.name]: Button, }

定义结构

这里定义一个数据​​showNextPage​​​,默认为​​true​​,就是可以显示下一页

加载更多

数据交互

这里需要注意,我们想要的是加载更多之后与前面加载的数据进行拼接,所以需要对订单的List进行改造:

getOrderList(){ this.loading = true this.axios.get('orders', { params:{ pageNum: this.pageNum } }).then((res) => { this.loading = false; // 将数据与前一页进行拼接 this.list = this.list.concat(res.list) this.total = res.total // 判断是否还有下一页,如果没有就会隐藏加载按钮 this.showNextPage = res.hasNextPage this.busy = false }).catch(() => { this.loading = false }) }loadMore(){ // 页数加一 this.pageNum++ // 重新刷新订单列表 this.getOrderList() }

11.3 滚动加载

滚动加载需要使用到一个插件:​​vue-infinite-scroll​​,首先要安装并引入、注册插件:

// 安装npm install vue-infinite-scroll --save// 引入import infiniteScroll from 'vue-infinite-scroll'// 注册(与data同级)directives:{ infiniteScroll}

定义结构

// 距离底部多少像素的时候,进行加载

数据交互

getList(){ this.loading = true; this.axios.get('/orders',{ params:{ pageSize:10, pageNum:this.pageNum } }).then((res)=>{ this.list = this.list.concat(res.list); this.loading = false; if(res.hasNextPage){ this.busy=false; }else{ this.busy=true; } }); } scrollMore(){ this.busy = true; setTimeout(()=>{ this.pageNum++; this.getList(); },500); },

其中,​​busy​​​代表是否触发加载,如果是​​true​​就加载,反之就不加载。

总之,这三种方法都能对列表加载更多数据,只是方式不同,还要根据需求去使用。

12. 项目优化

懒加载使用import的方式,由于import方式是ES7的语法,所以我们需要引入一个插件,来解析ES7的语法:​​@babel/plugin-syntax-dynamic-import​​

安装:​​npm install --save-dev @babel/plugin-syntax-dynamic-import​​

然后将路由改成按需加载的形式:

{ path: 'confirm', name: 'order-confirm', component: () => import('./pages/orderConfirm.vue') }

这样就实现了路由的懒加载,但是在首页刷新的时候,还是会在空闲时间把所有的​​js​​​代码都加载下来,在network中的​​js​​看不到,只能在other看到。只有在需要加载时,js内容才会出现在script中,js文件中华也就出现了相应的文件内容。

这时,所有的​​js​​​文件都被放在​​​​​中,它告诉浏览器,这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低。也就是说​​prefetch​​​通常用于加速下一次导航,而不是本次的。被标记为​​prefetch​​的资源,将会被浏览器在空闲时间加载。

所以,如果想要真正的做到按需按需加载,就要清除​​prefetch​​​。在​​vue.config.js​​文件中加入以下代码:

chainWebpack:(config)=>{ config.plugins.delete('prefetch'); }

13. 总结

(1)做了什么?

(2)难的是什么?

个人觉得比较难的地方是以下几点:

Vuex状态管理,过程不是很熟悉插件的使用,不是很熟练Bug的解决,有时不知道问题出在哪业务逻辑的实现,有时缺少条件,致使不能实现想要的功能页面布局(个人不是很擅长)项目优化不知从哪里入手项目部署

(3)收获是什么?

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Google IO 2022——CSS 状态
下一篇:隐秘的角落藏着珍宝,艺术家把名画搬到街头巷尾!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~