如下是本次项目的代码连接和预览连接:
代码连接:https://github.com/Leonardo-zyh/Vue-youzanStore
预览连接:https://leonardo-zyh.github.io/Vue-youzanStore/dist/ css
首先此次重构有赞商城使用的是一个多页面应用的重构思路,所以在进行重构以前要对项目文件进行一些配置和调整,具体的操做的话能够点击如下这个连接进行查看:基于vue-cli搭建一个多页面应用html
在完成了多页面应用的基础结构的搭建以后,会出现项目根目录下有一个src文件夹,src文件里有components、modules、pages三个文件夹的状况,而components文件夹是用来放置一些共用的vue组件的,而modules文件夹里是放置一些共用的css、js模块,至于最后的pages文件夹则是用来放置有赞商城的不一样页面的文件,每一个页面都会在pages内呈一个单独的文件夹,里面会放置关于这个页面的独有的全部文件。vue
在这里先说明一下,重构过程当中全部获取到的数据,都是经过使用在easymock上编写对应的接口(原在数据在rap2上,可是接口数据不稳定且没法搭建在github上),而后经过axios发送异步请求来获取到的模拟的数据,这是模仿真实的开发环境下的操做,具体的实现过程的话能够参考easymock以及我在github上面的源码文件。ios
1.首页
2.目录分类页
3.商品搜索列表页
4.商品详情页
5.购物车页面
6.我的中心地址管理页面
接下来咱们会逐个页面来讲说他们的重构思路
axios
swipermint-uiVolecityqs库
那咱们首先来讲一下轮播组件,首先咱们须要在src目录下的compnents文件夹里新建一个轮播组件文件,轮播的话咱们会直接选择使用swiper插件提供的轮播组件库,咱们只需把它封装到一个组件文件中便可,具体的操做在这里我就不详细说明了,这里只强调两个须要注意的问题:git
1.应不该该在轮播组件放入图片数据呢?
回答:不该该,缘由是为了使得轮播组件独立出来,在不一样的组件中得以复用,而且使其能够适应不一样规格不一样数量的图片,所以咱们的轮播组件只负责展现数据,不负责拿数据,数据应该经过props从父组件中获取。github
<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节点。ajax
关于这个“最热商品推荐”的商品列表的重构也很是简单,只需经过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 } } }
只有在渲染页面时,只要对你想进行的数据后加上该过滤器便可:vue-router
<div class="price">¥{{list.price | currency}}</div>
2.如何作到下来商品列表就发送对应的请求来更新一页新的商品列表?
回答:这里咱们使用到了mint-ui,一个移动端分页效果库,而后咱们使用它文档上面对应的infinite scroll的api来达到咱们想要的效果,具体代码以下:vuex
<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 :href="'/goods.html?id='+list.id">
<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"
表示下拉的触发距离,设置的数值越大,表示滚动条离底部的触发距离越大,越容易触发。
底部导航栏和轮播组件同样,因为能够在其余地方进行复用,所以会把该组件放于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.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
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=Stock Keeping Unit(库存量单位),同一型号的产品,或者说是同一个产品项目(产品条形码是针对企业的产品)。有兴趣能够自行搜索:“淘宝sku算法解析”或看这篇博客
而后如何制做sku页面载入和消失时的动画效果呢?这里咱们使用到了vue提供的自带transition的封装组件,能够经过这个组件来给任何元素和组件添加进入或者离开时的过渡。这个组件提供了八个JavaScript钩子函数以及六个过渡类名的切换,利用这些钩子函数以及类名的切换就能够完成组件的过渡动画了,这里列举一个vue文档上的典型例子给你们参考一下吧:
<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; }
position:fixed;width:100%;
这样内容层就不会再滚动了,以后咱们再经过设置:
scrollTop = document.scrollingElement.scrollTop document.body.style.top = -scrollTop + 'px'
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 } } }
商品的获取渲染以及增长是否被选中属性
获取后台数据加载处理或动态响应式处理
商品选中店铺选中全选,影响价格三级联动。
编辑状态,其他不可切换。对数量操做,加减更改。删除,单商品删除,选中(多个)删除,商品删除店铺删除。
原生事件,滑动删除页面,Volecity。
删除多个商品进行过滤处理
fetch层封装,
同一个场景下思惟层封装
问题呈现,左滑删除样式继承。[0].style.left='0px' this.$refs[`goods-${shopIndex}-${goodIndex}`][0].style.left='0px'
首先获取数据,渲染到页面这些是基本的操做
获取到数据以后,因为有一些属性数据中没有,而且咱们想要它在页面中是呈响应式存在的,所以从接口获取到数据以后不该该直接赋值给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 }) }
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状态。
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).状态改变后,组件中的计算属性由于绑定了该数据做为依赖,所以数据的改变会响应式地展现在页面中,即页面展现的数据也会获得同步的改变