这个是我的第一个vue项目,历时了两个多星期完成吧,通过这个项目了解了vue框架的基本语法以及生命周期等特性,并且了解了vue-loader、vue-cli、vue-router、vuex的基本使用方法,以及使用到axios,swiper,mint-ui,Volecity库,收获颇深,因此想在此简单地叙述一下重构有赞商城的基本思路以及当中的一些重要操作,另外当作对自己项目的回顾以及相关vue知识的总结。
以下是本次项目的代码链接和预览链接:
代码链接:https://github.com/Leonardo-zyh/Vue-youzanStore
预览链接:https://leonardo-zyh.github.io/Vue-youzanStore/dist1/
首先这次重构有赞商城使用的是一个多页面应用的重构思路,因此在进行重构之前要对项目文件进行一些配置和调整,具体的操作的话可以点击以下这个链接进行查看:基于vue-cli搭建一个多页面应用
在完成了多页面应用的基础结构的搭建之后,会出现项目根目录下有一个src文件夹,src文件里有components、modules、pages三个文件夹的情况,而components文件夹是用来放置一些共用的vue组件的,而modules文件夹里是放置一些共用的css、js模块,至于最后的pages文件夹则是用来放置有赞商城的不同页面的文件,每个页面都会在pages内呈一个单独的文件夹,里面会放置关于这个页面的独有的所有文件。
在这里先说明一下,重构过程中所有获取到的数据,都是通过使用在easymock上编写对应的接口(原在数据在rap2上,但是接口数据不稳定且无法搭建在github上),然后通过axios发送异步请求来获取到的模拟的数据,这是模仿真实的开发环境下的操作,具体的实现过程的话可以参考easymock以及我在github上面的源码文件。
以下是重构有赞商城的所需的页面列表,一共有六个页面,分别为:
1.首页
2.目录分类页
3.商品搜索列表页
4.商品详情页
5.购物车页面
6.个人中心地址管理页面
接下来我们会逐个页面来说说他们的重构思路
然后我们会用到的一些第三方插件,分别为:
- axios
- swiper
- mint-ui
- Volecity
- qs库
一、首页
首页的整体结构是,顶部一个无缝轮播组件,中间是三个推荐的商店链接,然后再下面是一个“最热商品推荐”的商品列表,然后最底下是一个底部导航栏组件一共四部分,对于中间三个推荐商店链接我这边暂不做处理,原因是关于并没有重构商店页面的计划。
1、无缝轮播组件
那我们首先来说一下轮播组件,首先我们需要在src目录下的compnents文件夹里新建一个轮播组件文件,轮播的话我们会直接选择使用swiper插件提供的轮播组件库,我们只需把它封装到一个组件文件中即可,具体的操作在这里我就不详细说明了,这里只强调两个需要注意的问题:
1.应不应该在轮播组件放入图片数据呢?
回答:不应该,原因是为了使得轮播组件独立出来,在不同的组件中得以复用,并且使其可以适应不同规格不同数量的图片,因此我们的轮播组件只负责展示数据,不负责拿数据,数据应该通过props从父组件中获取。
<Swipe :lists="bannerLists" name="swpie.vue" v-if="bannerLists"></Swipe>
new Swiper('.swiper-container',{ loop:true, pagination: '.swiper-pagination', autoplay: 2000 }) getBanner(){//获取轮播数据 this.$http.get(url.banner).then(res=>{ this.bannerLists = res.data.lists })
2.关于swiper的配置应将其写在轮播组件的生命周期的哪一部分呢?
回答:首先我们需要了解的是swiper是对DOM节点进行操作的,所以swiper的配置应该写在组件的mounted生命周期钩子里,因为在这个阶段已经在页面上生成了该组件对应的DOM节点;另一方面,swiper组件里的数据是swiper的父组件异步获取后传递给swiper的,因此应该等swiper拿到了传递的数据之后再对这个组件进行渲染,因此需要给这个组件添加一个v-if="bannerLists"
的判断,判断swiper组件是否获取到数据,只有获取到了数据才生成这个DOM节点。
2、“最热商品推荐”的商品列表
关于这个“最热商品推荐”的商品列表的重构也非常简单,只需通过axios发送你想获取的商品列表的页数和每页的展示商品的个数的请求到对应的接口中,就可以获取到对应的商品列表的数据,然后再通过v-for
把每个商品的图片、名称和价格渲染到页面中即可。
同样的,这里有两个值得注意的问题:
1.获取到的价格的格式并不统一,如何来使得这些价格的格式统一起来?
回答:这里需要用到vue实例里的一个自带属性filters
来对数据进行过滤,在vue1.0的时候,filters里面会有自带的过滤器,不过在vue2.0时被移除了,因此需要我们来自己写所需的过滤器的过滤方式:
filters:{ currency(num){ num=num+'' let arr=num.split('.') if (arr.length===1){ return num+'.00' } else { if (arr[1].length===1){ return num+'0' } else return num } } }
只有在渲染页面时,只要对你想进行的数据后加上该过滤器即可:
<div class="price">¥{{list.price | currency}}</div>
2.如何做到下来商品列表就发送对应的请求来更新一页新的商品列表?
回答:这里我们使用到了mint-ui,一个移动端分页效果库,然后我们使用它文档上面对应的infinite scroll的api来达到我们想要的效果,具体代码如下:
<ul class="js-list js-lazy" data-src="" v-infinite-scroll="getList" infinite-scroll-disabled="loading" infinite-scroll-distance="50" > <li v-for="list in lists" :key="list.id"> <div class="goods-item"> <a :rel="nofollow noopener noreferrer" href="'/goods.html?id='+list.id" rel="external nofollow" > <div class="thumb img-box"> <img class="fadeIn" v-bind:src="list.img"> </div> <div class="detail"> <div class="title">{{list.name}}</div> <div class="price">¥{{list.price | currency}}</div> </div> </a> </div> </li> </ul>
上述代码中,v-infinite-scroll="getList"
表示每当下拉到一定距离时就触发methods里面的getList方法;getList方法的具体代码如下所示:
getList(){ if (this.allLoad) return this.loading=true axios.post(url.hostLists,{ pageNum:this.pageNum, pageSize:this.pageSize }).then((response)=>{ let currentList=response.data.lists if (currentList.length<this.pageSize) this.allLoad=true if (this.lists) { this.lists=this.lists.concat(currentList) } else { this.lists=currentList } this.pageNum +=1 this.loading=false }) }
infinite-scroll-disabled="loading"
表示效果触发的条件,若loading为false则表示可以触发,若loading为true则表示不能触发,因此当loading为true时我们可以给底部添加一个加载效果,当数据获取完毕,loading变为false时,我们可以通过v-show="loading"
来让加载效果消失;infinite-scroll-distance="50"
表示下拉的触发距离,设置的数值越大,表示滚动条离底部的触发距离越大,越容易触发。
3.底部导航栏组件
底部导航栏和轮播组件一样,由于可以在其他地方进行复用,因此会把该组件放于components文件夹中,这里值得一提的是,底部导航栏组件由于点击不同的图标,它会跳转到不同的页面,因此会导致导航栏状态的重新加载,因此,若想要在不同的页面让导航栏呈现不同的状态,我们需要在跳转的时候传入对应的查询参数,然后在跳转到不同的页面时读取这个参数来呈现对应的不同的状态,具体的代码片段如下:
let {index}=qs.parse(window.location.search.substring(1)) export default { data(){ return { navConfig, curIndex:parseInt(index,10) || 0 } }, methods:{ changeNav(index,list){ location.rel="nofollow noopener noreferrer" href=`${list.href}?index=${index}` } } }
值得一提的是,在这里我们使用到了一个qs库,这个库可以方便我们提取出当前url后面的查询参数。
最后,由于在其他页面中,filters属性和底部导航栏组件都可以进行复用,所以这里我们利用mixins属性,来对filters属性和底部导航栏组件的注入进行打包,打包在一个js文件夹下的mixin.js文件中:
import Footnav from 'components/FootNav.vue' let mixin={ filters:{ currency(num){ num=num+'' let arr=num.split('.') if (arr.length===1){ return num+'.00' } else { if (arr[1].length===1){ return num+'0' } else return num } } }, components:{ Footnav } } export default mixin
当你的页面需要使用到该过滤器,或者底部导航栏时,只要对这个模块进行引入,并在mixins属性中添加它即可:
new Vue({ ... mixins:[mixin] })
二、目录分类页
目录分类页并无新的操作,和首页的部分操作类似,就是利用axios从接口中获取数据并渲染到页面中,并对页面中的一些焦点状态进行v-show的处理,以及一些类名和焦点的处理,我们可以从目录分类页中通过点击热销商品进入商品详情页,通过点击热门品牌进入商品搜索列表页,在进行这些页面的跳转时,把一些关键的数据传入查询参数中以便跳转页面获取即可。
let { index } = qs.parse(location.search.substr(1)); changeNav(list, index) { //this.curIndex = index; location.href = `${list.href}?index=${index}`; //页面跳转 event.preventDefault(); }
三、商品搜索列表页
商品搜索列表页同样的基本操作与首页和目录分类页类似,这里唯一不同的是,商品搜索列表页在用户下拉一定距离后,会出现一个回到顶部的图标,点击图标,用户就可以直接回到顶部,在这里,我们使用了一个叫作Velocity.js的动画库,它是把css中的一些动画效果进行封装,进而可以通过一些api轻松实现一些简单的动画效果,例如上面所说的回到顶部,在项目中的代码片段如下所示:
//引入Velocity import Velocity from 'velocity-animate/velocity.js' //在methods中加入对应方法 methods:{ scrollMove(){ if (window.scrollY>=290){ this.isShow=true } else { this.isShow=false } }, goToTop(){ Velocity(document.body, 'scroll', { duration: 500, easing: "easeOutQuart" }); this.isShow=false //回到顶部图标消失 } }
四、商品详情页
在商品详情页中,除了有对数据的获取和页面的渲染外,这里主要涉及到了三个新的操作:
- sku算法的应用
- 页面的载入和消失的动画效果
- 页面展示时的穿透滚动问题的解决
首先是sku算法,由于此次商品详情页的选择并不需要使用到它,因为商品的可选属性只有一个,但是在实际情况下,由于很多商品的可选属性不止一个,因此是需要使用到sku算法的。SKU=Stock Keeping Unit(库存量单位),同一型号的产品,或者说是同一个产品项目(产品条形码是针对企业的产品)。
然后如何制作sku页面载入和消失时的动画效果呢?这里我们使用到了vue提供的自带transition的封装组件,可以通过这个组件来给任何元素和组件添加进入或者离开时的过渡。这个组件提供了八个JavaScript钩子函数以及六个过渡类名的切换,利用这些钩子函数以及类名的切换就可以完成组件的过渡动画了,这里列举一个vue文档上的典型例子给大家参考一下吧:
transition过渡:
<div id="demo"> <button v-on:click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition> </div>
new Vue({ el: '#demo', data: { show: true } })
.fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { opacity: 0; }
最后在遮罩层和弹出层出现之后(即sku页面),对页面进行上下拖动时,背后的内容层也会跟着一起拖动,这是典型的滚动穿透问题,在这里我的解决办法是监听遮罩层和弹出层的出现,当它们出现之后,我们更改内容层的样式,使其样式变为:position:fixed;width:100%;
这样内容层就不会再滚动了,之后我们再通过设置:
scrollTop = document.scrollingElement.scrollTop document.body.style.top = -scrollTop + 'px'
使得内容层的滚动高度与遮罩层和弹出层出现前相同,并且,调整html元素的样式:height:100%;overflow:hidden;
,在关闭遮罩层和弹出层后,还原这些修改样式,即可使得滚动穿透的问题得以解决。需要注意的是,还原这些样式之后,原本内容层滚动的高度就会丢失,因此我们要通过之前记录下来内容层滚动的高度,在还原样式时将滚动高度也一并还原。
document.scrollingElement.scrollTop = scrollTop
这样滚动穿透的问题就算是彻底解决了,下面是全部的这部分的全部代码片段:
chooseSku(type) {//显示购买菜单 this.skuType = type this.showModal = true }, changeSku(num) {//增减数量 if (num < 0 && this.skuNum === 1) return this.skuNum += num }, addCart() {//加入购物车 $.ajax($.url.cartAdd, { id, skuNum: this.skuNum }).then((data) => { if (data.status === 200) { this.showModal = false this.showAddMsg = true //添加成功的信息 this.isAddedCart = true //显示购物车图标 setTimeout(() => this.showAddMsg = false, 1200) } }) } },
watch:{ showSku(val,oldVal){ if (val){ scrollTop = document.scrollingElement.scrollTop document.body.style.top = -scrollTop + 'px' } document.body.style.position=val?'fixed':'static' // document.body.style.margin=val?`0 0 ${window.scrollY}px 0`:'0px' document.querySelector('html').style.overflow=val?'hidden':'auto' document.body.style.width=val?'100%':'auto' document.querySelector('html').style.height=val?'100%':'auto' if (!val){ document.scrollingElement.scrollTop = scrollTop } } }
WeUI上面的层级规范:用于规范WeUI页面元素所属层级、层级顺序及组合规范。
五、购物车页面
购物车页面里面比较多的逻辑关系,在此就不一一枚举了,大概说一下它的重构思路:
商品的获取渲染以及增加是否被选中属性
获取后台数据加载处理或动态响应式处理
商品选中店铺选中全选,影响价格三级联动。
编辑状态,其余不可切换。对数量操作,加减更改。删除,单商品删除,选中(多个)删除,商品删除店铺删除。
原生事件,滑动删除页面,Volecity。
删除多个商品进行过滤处理
fetch层封装,
同一个场景下思维层封装
问题呈现,左滑删除样式继承。[0].style.left='0px' this.$refs[`goods-${shopIndex}-${goodIndex}`][0].style.left='0px'
ref 是非响应式的,不建议在模板中进行数据绑定,即使用唯一标识绑定
v-for 模式使用“就地复用”策略,简单理解就是会复用原有的dom结构,尽量减少dom重排来提高性能 ( 解决方案:还原dom样式 )
key 为每个节点提供身份标识,数据改变时会重排,最好绑定唯一标识,如果用index标识可能得不到想要的效果(绑定唯一识别key)
网页性能管理详解
首先获取数据,渲染到页面这些是基本的操作
获取到数据之后,由于有一些属性数据中没有,并且我们想要它在页面中是呈响应式存在的,因此从接口获取到数据之后不应该直接赋值给data里,而是应该先给数据增添属性,再把增添后的数据赋值到data处,具体代码如下:
getLists(){ cart.getCartLists().then((response)=>{ let list=response.data.cartList list.forEach(shop=>{ shop.checked=true shop.editingStatus=false shop.editingMsg='编辑' shop.removeChecked=false shop.goodsList.forEach(good=>{ good.checked=true good.removeChecked=false good.touchDelete=false }) }) this.cartLists=list }) }
每次选择了商店下的商品时,都利用数组的every方法来遍历数组看是否全部商品都被选中了,若选中则商店也被选中,同理,若选择了商店,则遍历商店下的商品,把商店下的商品全部选中。取消选中亦是同理。
全选与否则利用计算属性来处理,利用计算属性的getter来获取此时购物车的状态来判断是否被全选,利用计算属性的setter来处理点击全选时商店及商店下商品的状态的改变。
同样的,利用计算属性来计算正常状态下选中商品的总价,并返回选中商品的列表。同理,利用计算属性来计算编辑状态下的选中的商品的列表,并以数组的形式返回。
在编辑状态下,我利用了计算属性来对商品的数量的数据进行了监测,若判断出数量中存在非数字或者负数的情况,则会自动把数量的数据变成1。
利用touchstart
和touchend
两个事件来实现商品左拉删除的功能,这两个事件分别绑定start
和end
的方法,方法的具体代码如下所示:
start(e,good){ good.startX=e.changedTouches[0].clientX }, end(e,good,goodIndex,shopIndex,shop){ let endX=e.changedTouches[0].clientX let left='0px' if (good.startX-endX>100){ good.touchDelete=true left='-60px' Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`],{left}) shop.goodsList.forEach((otherGood,index)=>{ if (otherGood.touchDelete && index!==goodIndex) { otherGood.touchDelete=false Velocity(this.$refs[`goods-${shopIndex}-${index}`],{left:'0px'}) } }) } else if (endX-good.startX>100) { good.touchDelete=false left='0px' Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`],{left}) } }
当添加了左拉删除的功能之后,页面会出现一个BUG,就是左拉之后,点击该商品对应的商店下的编辑按钮,删除的按钮会继续被左拉,呈现一个比其他删除按钮长的BUG状态。
处理办法:通过给每个商品的一个具有唯一识别性的ref,然后在点击编辑时,找到已左拉的商品的对应的具有唯一识别性的ref,把它的左拉状态还原即可,具体代码如下所示:
shop.editingStatus=!shop.editingStatus if (shop.editingStatus){ shop.goodsList.forEach((good,index)=>{ if (good.touchDelete){ good.touchDelete=false this.$refs[`goods-${shopIndex}-${index}`][0].style.left='0px' } }) }
六、个人中心地址管理页面
最后是个人中心地址管理页面,在这个页面中,我们会封装addressService层和fetch层,addressService层主要是负责页面中前后端交互的方法,如添加地址、删除地址、编辑地址和获取地址等,然后fetch层主要是负责从RAP接口中获取数据并返回一个promise对象到service层中,具体的封装方式和使用方式请自行查看源码。
另外在这个页面中,我们使用到了vue-router和vuex,接下来我将会简要介绍它们在个人中心地址管理页面中的使用方式。
首先是vue-router,他是用于构建单页面应用的,是基于路由和组件,路由用于访问特定的路径,然后特定的路径与特定的组件相联系相映射,传统页面中,是通过超链接来实现页面的跳转和切换的,但在vue-router中,则是路由的切换,即组件的切换。
我们先来看看是如何配置一个routes、创建一个router实例并把它注入到vue实例中去的:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) //构造配置 let routes=[{ path:'/', components: require('../components/member.vue') },{ path:'/address', components:require('../components/address.vue'), children:[{ path:'', redirect: 'all' },{ path:'all', components:require('../components/all.vue') },{ path:'form', name:'form', components:require('../components/form.vue') }] }] //创建router实例 let router=new Router({ routes }) export default router import Vue from 'vue' import router from './router' import store from './vuex' //根组件注入 let view=new Vue({ el:'#app', router, store }) //router-view标签作为配置路由后组件的容器 <div id="app"> <router-view></router-view> </div>
通过这样路由的配置和注入,我们就可以实现单页面下多组件的切换和嵌套了,如果上述有不懂的地方,请到vue-router的官网处查看文档和说明。
接着我们来讲一下vuex,vuex是对SPA即单页面应用进行数据的状态管理,如果想了解具体vuex是什么还有它的用途,请点击这篇文章:Vuex新手入门指南
vuex其实也是组件间通信的一种方式,说起组件间的通信,我们不如来一一列举一下他们的方式有哪些:
1.引用类型数据
用法:如果父组件有一个数据类型是引用类型的数据,当这个数据直接传递给子组件以后,在子组件对这个数据源进行修改的时候,父组件中该数据也会同步修改。
2.自定义事件
即子组件内部定义了一个自定义事件,可以用父组件在子组件上进行监听:
//子组件 this.$emit('change',18) //父组件 <foo :obj="obj" @change="changeAge"></foo> //父组件 methods:{ changeAge(age){ this.obj.age=age } }
3.全局事件(global bus)
//bus.js import Vue from 'vue' const bus=new Vue() export default bus //触发组件 import bus from 'js/bus.js' bus.$emit('change',18) //订阅组件 import bus from 'js/bus.js' bus.$on('change',(age)=>{ this.obj.age=age })
4.vuex状态管理
vuex的使用与vue-router有一点相似,具体代码如下:
import Vue from 'vue' //使用vuex插件 import Vuex from 'vuex' Vue.use(Vuex) import address from 'js/addressService.js' //创建Store实例 const store=new Vuex.Store({ state:{ lists:null }, mutations: { init(state,lists){ state.lists=lists } }, actions: { getLists({commit}){ address.getList().then(response=>{ commit('init',response.data.lists) }) } } }) export default store
之后同样的在跟组件对store实例进行注入即可,在上述实例中,state属性表示的是实例的状态,类似vue实例里的data,需要高度注意的是,不允许直接修改state里面的值,只允许定义一系列的类似事件的mutations来触发进行state的管理。而mutations属性里面存放的是同步事件,因此是对数据进行同步管理,要进行异步操作的话必须使用actions属性;actions属性里面存放一些异步的操作,在异步的操作进行完成之后再触发mutations里面的同步事件来对state里面的数据的状态进行同步的操作。
在组件中,我们一般通过dispatch来触发actions里面的异步事件进行异步操作,一般使用计算属性来获取state中的数据,之所以使用计算属性,是因为状态管理里的数据可能是变化的,因此我们希望它在页面中是响应式的,因此我们选择使用计算属性来对数据进行依赖的绑定。
具体代码如下:
computed:{ list(){ if(this.$store.state.lists){ return this.$store.state.lists } return false } }, created(){ if (!this.list){ //防止在新增地址或修改地址后多次触发mutations中的init this.$store.dispatch('getList') } }
总之,vuex中状态管理的过程可总结为以下流程:
(1).通过dispatch(actionFnName)分发来触发actions中的异步操作=>
(2).待异步操作结束之后通过commit(mutationsFnName,data)来触发mutations中的同步事件来进行同步操作=>
(3).通过同步操作改变state中的数据的状态=>
(4).状态改变后,组件中的计算属性因为绑定了该数据作为依赖,因此数据的改变会响应式地展示在页面中,即页面展示的数据也会得到同步的改变
本文如何利用vue重构有赞商城的思路以及总结整理到此结束。人生如棋,仅有舍弃一些棋子,才能换得一生的平静和幸福。上帝是公平的,冬天里失去的东西,到春天的时候会还给你;这方面舍弃的东西,在另一方面会补给你。舍与得,其实是一个无限循环的圈。越敢舍,越能得;越愿舍,越易得。舍得之间,人生迥异,以舍为得,妙用无穷!小编再次感谢大家对我们的支持!