如下是本次項目的代碼連接和預覽連接:
代碼連接: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).狀態改變後,組件中的計算屬性由於綁定了該數據做爲依賴,所以數據的改變會響應式地展現在頁面中,即頁面展現的數據也會獲得同步的改變