1.flex 屬性是 flex-grow、flex-shrink 和 flex-basis 屬性的簡寫屬性。
flex-grow | 一個數字,規定項目將相對於其他靈活的項目進行擴展的量。 |
flex-shrink | 一個數字,規定項目將相對於其他靈活的項目進行收縮的量。 |
flex-basis | 項目的長度。合法值:"auto"、"inherit" 或一個后跟 "%"、"px"、"em" 或任何其他長度單位的數字。 |
2.采用絕對定位,相對於父元素
.good
display flex
position absolute
width 100%
top 174px
bottom 46px
overflow hidden
3.使用vue-resourse獲取json並應用到模板
現在越來越多的數據傳輸方式都是json數據格式,包括用jquery開發時,也有很好用的$.ajax來進行數據請求與處理,那么vue-resource提供了一種類似的,並且api更加簡潔易用,壓縮后文件更小。配合ES 6的Lambda寫法,更加優雅
官網:https://github.com/pagekit/vue-resource/blob/master/docs/http.md
props: { seller: { type: Object } }, data () { return { goods: [], //一開始goods為空 listHeight: [], scrolly: 0, selectedFood: {} }; }, created() { //當這個組件被調用的時候,通過后端獲得數據賦值給goods this.$http.get('/api/goods').then((response) => { // '/api/goods'請求的是data.json下的goods數組 response = response.body; if (response.errno === ERR_OK) { this.goods = response.data; this.$nextTick(() => { //可以用$nextTick
來確保Dom變化后
再執行一些事情 this._initScroll(); this._calculateHeight(); }); } });this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; },
注:vue更新到2.0之后,作者就宣告不再對vue-resource更新,而是推薦的axios,它的基本用法可以參考:http://www.kancloud.cn/yunye/axios/234845
4.遍歷取數據
<span class="text"> <span v-show="item.type>0" class=" icon" :class="classMap[item.type]"></span>{{item.name}} </span>
classMap[item.type]是一個數組,通過item.type去取對應的class,item.type是data.json中mock的數據
5.display table
此元素會作為塊級表格來顯示(類似 <table>),表格前后帶有換行符。
在table中可用vertical-align middle實現垂直居中
6.添加better-scroll依賴
鏈接:https://github.com/ustbhuangyi/better-scroll
<div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item, index) in goods" class="menu-item border-1px" :class="{'current':currentIndex === index}" @click="selectMenu(index, $event)"> <span class="text"> <span v-show="item.type>0" class=" icon" :class="classMap[item.type]"></span>{{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item" @click="selectFood(food, $event)"> <div class="icon"> <img :src="food.icon" alt="" width="57"> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}</span><span class="count">好評{{food.rating}}</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span><span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartControl-wrapper"> <cartControl :food="food" @increment="incrementTotal"></cartControl> </div> </div> </li> </ul> </li> </ul> </div>
6.1 $refs
的使用是vue 2 操作dom的一種方式
ref 被用來給元素或子組件注冊引用信息。引用信息將會注冊在父組件的 $refs 對象上。
如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子組件上,引用就指向組件實例:
_initScroll(){ //初始化scroll區域 this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true //結合BScroll的接口使用,是否將click事件傳遞,默認被攔截了 }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { probeType: 3 //結合BScroll的接口使用,3實時派發scroll事件,探針的作用 }); //結合BScroll的接口使用,監聽scroll事件(實時派發的),並獲取鼠標坐標,當滾動時能實時暴露出scroll this.foodsScroll.on('scroll', (pos) => { //事件的回調函數 this.scrollY = Math.abs(Math.round(pos.y));//滾動坐標會出現負的,並且是小數,所以需要處理一下,實時取得scrollY
}) }
vue中更改數據,DOM會跟着做映射,但vue更新DOM是異步的,用 $nextTick
()來確保Dom變化后能調用到_initScroll()方法。調用_initScroll()方法能計算內層ul的高度,當內層ul的高度大於外層wrapper的高度時,可以實現滾動。
6.2 左右兩邊聯動
-
在vue實例生命周期的開始created分別加載
_initScroll
和_calculateHeight
-
通過
_calculateHeight
計算foods內部每一個塊的高度,組成一個數組listHeight -
在_initScroll里面,設置了bscroll插件的一個監聽事件scroll,將food區域當前的滾動到的位置的y坐標設置到一個vue實例屬性scrollY
this.scrollY = Math.abs(Math.round(pos.y));
-
通過計算屬性currentIndex,獲取到food滾動區域對應的menu區域的子塊的索引,然后通過設置一個class來做樣式切換變化
:class="{'current':currentIndex === index}
,實現聯動 -
另外當點擊menu 區域的時候,會觸發selectMenu事件,也會根據點擊到的menu子塊的索引然后去觸發food區域滾動到對應的高度區塊區間
this.foodsScroll.scrollToElement(el, 300);scrollToElement():是better-scroll中的方法,滾動到某個元素,el(必填)表示 dom 元素,time 表示動畫時間,offsetX 和 offsetY 表示坐標偏移量,easing 表示緩動函數
-
這樣完成整個對應
_calculateHeight()方法計算各個右側區間的高度
_calculateHeight(){ let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); //獲取每一個food的dom對象 let height = 0; this.listHeight.push(height); //初始化第一個高度為0 for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; //每一個item都是剛才獲取的food的每一個dom height += item.clientHeight; //主要是為了獲取每一個foods內部塊的高度 this.listHeight.push(height); } } }
實時取得scrollY的值后,需要與左邊進行映射,利用計算屬性:
computed: { currentIndex(){ //計算到達哪個區域的區間的時候的對應的索引值 for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; //當前menu子塊的高度 let height2 = this.listHeight[i + 1]; //下一個menu子塊的高度 //滾動到底部的時候,height2為undefined,需要考慮這種情況 //需要確定是在兩個menu子塊的高度區間 if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; //返回這個menu子塊的索引 } } return 0; }, selectFoods() { //自動將所有的goods.food添加一個count屬性,方便做數量運算 let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; } }
:class="{'current':currentIndex === index}"當currentIndex === index時才設置current這個class
點擊左側 ,右側響應:
關於在selectMenu中點擊,在pc界面會出現兩次事件,在移動端就只出現一次事件的問題:
原因:bsScrooler會監聽事件(例如touchmove,click之類),並且阻止默認事件(prevent stop),並且他只會監聽移動端的,pc端的沒有監聽
在pc頁面上 bsScroller也派發了一次click事件,原生也派發了一次click事件
//bsScroll的事件,有_constructed: true MouseEvent {isTrusted: false, _constructed: true, screenX: 0, screenY: 0, clientX: 0…} //pc的事件 MouseEvent {isTrusted: true, screenX: -1867, screenY: 520, clientX: 53, clientY: 400…}
解決:針對bsScroole的事件,有_constructed: true,所以做處理,return掉非bsScroll的事件
selectMenu(index, event){ if (!event._constructed) { //去掉自帶的click事件點擊,即pc端直接返回 return; } let foodsList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodsList[index]; //類似jump to的功能,通過這個方法,跳轉到指定的dom this.foodsScroll.scrollToElement(el, 300); },
7.shopcart組件
也是采用flex布局,右側部分固定寬度(flex 0 0 105px),左邊自適應寬度(flex 1)
采用固定定位,定位在底部(position fixed)
橫向排列display:inline-block
包含購物車圖標的div超出了父元素的高度,我們使用position:relative,並設置top為負來實現
box-sizing: border-box; 則div 設置的寬高將包含 邊框及 padding
border-radius 50%,形成一個圓
選擇了多少商品:定義成數組,底欄其余部分的變化都基於這個對象的變化而變化
selectFoods: { type: Array, default() { return [{price: 20, count: 2}]; } }
計算部分(都基於selectFoods進行相應計算)computed中的函數可以直接在Tempplate中以指針的形式引用
computed: { totalPrice() {//計算總價,超過起送額度后提示可付款 let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() {//計算選中的food數量,在購物車圖標處顯示,采用絕對定位,top:0;right:0;顯示在購物車圖標右上角 let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }
控制底部右邊內容隨food的變化而變化,payDesc()控制顯示內容,payClass()添加類調整顯示樣式
在template中 <div class="pay" :class="payClass"> {{payDesc}} </div>
在computed中: payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; //這里使用的是es6中的反引號 } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `還差¥${diff}元起送`; } else { return '去結算'; //單引號,單引號和反引號不同 } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } }
總結:通過以上學習我們能發現,selectFoods()的變化起着關鍵作用,它的變化會引起DOM的變化,並最終體現到界面上,而我們不用關注DOM內部的具體實現,這就是vue的一大好處。如果采用jQuery完成這些功能會略顯繁雜。
8 cartcontrol組件,它是shopcart的子組件
可以給按鈕增加padding,方便用戶點擊
this.foodScroll = new BScroll(this.$refs.foodWrapper, { probeType: 3, click: true });
click: true
是否派發click事件
通過import Vue from 'vue';使用set接口,通過vue.set()添加屬性,當它變化時就能被檢測到,從而父組件能獲取到count值(遍歷選中的商品時使用)
methods: { addCart(event) { if (!event._constructed) { // 去掉自帶click事件的點擊 return; } if (!this.food.count) { Vue.set(this.food, 'count', 1); } else { this.food.count++; } // event.srcElement.outerHTML this.$emit('increment', event.target); // 子組件通過 $emit觸發父組件的方法 increment 還 }, decreaseCart(event) { if (!event._constructed) { // 去掉自帶click事件的點擊 return; } this.food.count--; } } };
9 為減號按鈕添加平移、滾動的動畫
<transition name="fade"> //減號和數字平移動畫 <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart($event)"> <transition name="inner"> //數字滾動動畫 <span class="inner iconfont icon-jian"></span> </transition> </div> </transition>
&.fade-enter-active, &.fade-leave-active { transition: all 0.4s linear <--過渡效果的 CSS 屬性的名稱、過渡效果需要多少時間、速度效果的速度曲線--> } &.fade-enter, &.fade-leave-active { opacity: 0 transform translate3d(24px, 0, 0) //這樣可以開啟硬件加速,動畫更流暢,3D旋轉,X軸位移24px } .inner display inline-block <--設置成inline-block才有高度,才能有動畫--> line-height 24px font-size 24px vertical-align top color rgb(0, 160, 220, 0.2) &.inner-enter-active, &.inner-leave-active { transition: all 0.4s linear transform: rotate(0) } &.inner-enter, &.inner-leave-active { opacity: 0 transform rotate(180deg) }
10 購物小球(拋物線小球)
通過兩個層來控制小球,外層控制一個方向的變化,內層控制另外一個方向的變化(寫兩層才會有拋物線的效果),采用fixed布局(是相對於視口的動畫)
<div class="ball-container"> <div v-for="ball in balls"> <transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">//后面三個為鈎子 <div v-show="ball.show" class="ball"> <div class="inner inner-hook"> </div> </div> </transition> </div> </div>
在addCart()方法(在cartControl組件里)中添加(子組件通過 $emit觸發父組件的方法 increment)
this.$emit('increment', event.target);
在父組件(goods組件)的template中寫入
<cartControl :food="food" @increment="incrementTotal"></cartControl>
在(goods組件)method中寫入this.$refs.shopCart指向shopCart組件,該組件中有drop()方法(父組件訪問子組件的方法)
首先在HTML中指定ref
當點擊“加號”按鈕時,cartControl組件通過emit觸發父組件goods中的increment方法,並將event.target對象傳入,increment方法將target傳入shopCart子組件中的drop方法,所以drop方法能獲得用戶點擊按鈕的元素,即能獲取點擊按鈕的位置
<shopCart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice" ref="shopCart"></shopCart>
incrementTotal(target) { this.$refs.shopCart.drop(target); }
drop(el) { for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBalls.push(ball); return; } } }
補充:
但是在vue2.0中$dispatch 和 $broadcast被棄用,因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱,並且這只適用於父子組件間的通信。官方給出的最簡單的升級建議是使用集中的事件處理器,而且也明確說明了 一個空的vue實例就可以做到,因為Vue 實例實現了一個事件分發接口在vue2.0中在初始化vue之前,給data添加一個 名字為eventhub 的空vue對象
某一個組件內調用事件觸發
this.$root.eventHub.$emit('eventName', event.target);
另一個組件內調用事件接收, 在組件銷毀時接除事件綁定,使用$off方法
created:{ this.$root.eventHub.$on('eventName',(target) => { this.functionName(target) }); }, method:{ functionName(target) { console.log(target); } }
因為小球是有去無回的動畫過程,這里采用vue中提供的鈎子
<transition name="drop" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
對應的方法寫在methods中
beforeEnter(el) { //找到所以設為true的小球 let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect();//返回元素相對於視口偏移的位置 let x = rect.left - 32; //點擊的按鈕與小球(fixed)之間x方向的差值 let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; //設置初始位置前,手動置空,覆蓋之前的display:none,使其顯示 el.style.webkitTransform = `translate3d(0,${y}px,0)`; //外層元素做縱向的動畫,y是變量 el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0];//內層元素做橫向動畫,inner-hook(用於js選擇的樣式名加上-hook,表明只是用 //於js選擇的,沒有真實的樣式含義) inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, enter(el) { // let rf = el.offestHeight; this.$nextTick(() => {//異步執行 el.style.webkitTransform = 'translate3d(0,0,0)'; //重置回來 el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; }); }, afterEnter(el) { let ball = this.dropBalls.shift(); //取到做完動畫的球,再置為false,即重置,它還可以接着被利用 if (ball) { ball.show = false; el.style.display = 'none'; } }
關於cubic-bezier(0.49, -0.29, 0.75, 0.41)
,是動畫拋物曲線(貝塞爾曲線)的配置,可以利用http://cubic-bezier.com/#.23,1.14,.83,.67 進行曲線調試
.ball-container
.ball
position fixed
left 32px
bottom 22px
z-index 200
transition: all 0.6s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width 16px
height 16px
border-radius 50%
background rgb(0, 160, 220)
transition: all 0.4s linear
點擊小球時有點卡,可采用異步執行的方式回調
incrementTotal(target) { this.$nextTick(()=>{ this.$refs.shopCart.drop(target); }) }
關於nextTick可以參考:https://segmentfault.com/a/1190000008570874
官方解釋:在下次 DOM 更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM。
11 什么時候需要用的Vue.nextTick()
a.你在Vue生命周期的created()
鈎子函數進行的DOM操作一定要放在Vue.nextTick()
的回調函數中。原因是什么呢,原因是在created()
鈎子函數執行的時候DOM 其實並未進行任何渲染,而此時進行DOM操作無異於徒勞,所以此處一定要將DOM操作的js代碼放進Vue.nextTick()
的回調函數中。與之對應的就是mounted
鈎子函數,因為該鈎子函數執行時所有的DOM掛載和渲染都已完成,此時在該鈎子函數中進行任何DOM操作都不會有問題 。
b.在數據變化后要執行的某個操作
,而這個操作
需要使用隨數據改變而改變的DOM結構的時候,這個操作
都應該放進Vue.nextTick()
的回調函數中。
12 購物車詳情
transform translate3d(0, -100%, 0),可以使詳情頁高度隨內容的增加而增加
12.1 針對購物車顯示的詳情頁添加滑動插件
listShow() { if (!this.totalCount) { this.fold = true; return false; } let show = !this.fold; if (show) {//如果顯示詳情頁 this.$nextTick(() => {//數據變化后,DOM並沒有立即生效,而BScroll嚴重依賴於DOM,所以使用nextTick if (!this.scroll) {//如果實例不存在,新建 this.scroll = new BScroll(this.$refs.listContent, { click: true }); } else {//實例存在,直接調用refresh接口 this.scroll.refresh(); } }); } return show; } }
12.2 購物車清空
setEmpty() { this.selectFoods.forEach((food) => { food.count = 0 }) }