項(xiàng)目簡(jiǎn)介
去哪兒的webapp版,實(shí)現(xiàn)其中的home界面,城市選擇頁(yè)面,詳情頁(yè)部分邏輯和界面
代碼結(jié)構(gòu)

- build與config
使用vue-cli腳手架工具以及webpack搭建好的開(kāi)發(fā)環(huán)境,build,config等目錄中包含相關(guān)配置文件,具體待研究,但修改其中的代碼后,本地服務(wù)器都要重新啟動(dòng)部分配置才能生route效。 - node_modules
用npm工具導(dǎo)入的部分插件和工具 -
src
1.assets,存放項(xiàng)目的靜態(tài)資源,如圖標(biāo),圖標(biāo)css,以及常用的常量。
2.common,放的是公共的組件,意為可能被各個(gè)頁(yè)面引用的組件,包括兩個(gè)組件,fade組件(封裝著transition動(dòng)畫(huà)的樣式),gallay(公共畫(huà)廊組件:是一個(gè)輪播畫(huà)廊)
3.pages,最核心的代碼放置處(再后面會(huì)詳細(xì)介紹,分別對(duì)應(yīng)三個(gè)頁(yè)面的)
image.png
4.router,放有路由配置文件。
5.store,放有vuex相關(guān)配置和代碼 - static
此文件夾放有整個(gè)項(xiàng)目的json數(shù)據(jù)文件,因?yàn)闆](méi)有后端服務(wù)器,所以請(qǐng)求的是本地的數(shù)據(jù)。(此處為了請(qǐng)求的路徑定位,還修改了config->indexks文件的內(nèi)容)
核心代碼實(shí)現(xiàn)
- router:
router的作用就是路由,讓url對(duì)應(yīng)的path和應(yīng)該調(diào)用的組件對(duì)應(yīng)起來(lái)。其中詳情頁(yè)的頁(yè)面比較特殊的,因?yàn)閷?duì)于每個(gè)ID,有自己的詳情頁(yè)。
export default new Router({
routes: [{
path: '/',
name: 'Home',
component: Home
}, {
path: '/city',
name: 'City',
component: City
}, {
path: '/detail/:id',
name: 'Detail',
component: Detail
}],
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
- main
main是根組件所在地,我們看下它的結(jié)構(gòu),它引入了路由,fastClick是針對(duì)部分手機(jī)兼容的,幾個(gè)CSS是一些基礎(chǔ)樣式,邊框樣式,圖標(biāo)樣式,還引入了我們的awesomeSwiper,store。
在根組件傳入,路由,和store
import Vue from 'vue'
import App from './App'
import router from './router'
import fastClick from 'fastclick'
import 'styles/reset.css'
import 'styles/border.css'
import 'styles/iconfont.css'
import VueAwesomeSwiper from 'vue-awesome-swiper'
import store from './store'
import 'swiper/dist/css/swiper.css'
Vue.config.productionTip = false
fastClick.attach(document.body)
Vue.use(VueAwesomeSwiper)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
- Home
- Home.vue
主頁(yè)有個(gè)城市顯示在右上位置。這個(gè)東西我們使用了vuex技術(shù)存儲(chǔ)在了store里。
通過(guò)這句可以取到store里的city值。
這里首先使用了mapstate這個(gè)輔助函數(shù),他可以把多個(gè)state狀態(tài)轉(zhuǎn)化成該子組件的計(jì)算屬性。使用...對(duì)象展開(kāi)運(yùn)算符還可以把這個(gè)對(duì)象拆分成一個(gè)個(gè)屬性。
...mapState(['city'])
這個(gè)頁(yè)面主要使用ajax發(fā)送了一些請(qǐng)求,
其中在mounted也就是第一次掛載的時(shí)候,請(qǐng)求一次,且更換城市。
用一個(gè)lastCity保存一下上一次的城市,再一次切換到此城市的時(shí)候,有可能vuex里面的city已經(jīng)改了,比如在城市選擇頁(yè)修改了,那么我們就要重新發(fā)ajax請(qǐng)求,請(qǐng)求這個(gè)city特應(yīng)的主頁(yè)。
- header
這里也會(huì)取到vuex的city值,并放在header顯示。
這里有個(gè)小細(xì)節(jié)是把代碼重復(fù)用到的變量放進(jìn)styl這個(gè)常量表里。
@import '~styles/varibles.styl'
.header
display: flex
line-height: $headerHeight
- swiper
這里用到了vue-asome-swiper這個(gè)插件。
最外層是模板
然后是wrapper包裹塊
然后是swiper這個(gè)標(biāo)簽,這是插件實(shí)現(xiàn)好的被你引入的。
slide意為可滑動(dòng)的區(qū)域。
下面還有個(gè)swiper-pagenation表示顯示幾個(gè)點(diǎn)的選項(xiàng)卡。
<template>
<div class="wrapper">
<swiper :options="swiperOption" v-if="list.length">
<!-- slides -->
<swiper-slide v-for="item of list" :key="item.id">
<img class="swiper-img" :src="item.imgUrl" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</div>
</template>
他們需要如此配置:(里面是各個(gè)參數(shù)的作用放在一起了)
swiperOption: {
pagination: '.swiper-pagination',
loop: true
//代表不自動(dòng)滑動(dòng)
autoplay: false
}
//fraction代表,用1/2的方式,默認(rèn)是...的方式。
paginationType: 'fraction',
//這兩個(gè)的作用是:如果不加這兩個(gè)參數(shù),重新進(jìn)入swiper的時(shí)候,寬度計(jì)算會(huì)出現(xiàn)問(wèn)題。
observeParents: true,
observer: true
- icons
這里主要實(shí)現(xiàn)細(xì)節(jié):
計(jì)算屬性里把a(bǔ)jax得到的數(shù)據(jù)進(jìn)行一番計(jì)算,生成pages數(shù)組,每個(gè)pages數(shù)組里放的又是一個(gè)個(gè)圖標(biāo),這樣可以達(dá)到分頁(yè)的效果了。
computed: {
pages () {
const pages = []
this.iconsList.forEach((item, index) => {
const page = Math.floor(index / 8)
if (!pages[page]) {
pages[page] = []
}
pages[page].push(item)
})
return pages
}
}
看swipter的實(shí)現(xiàn)細(xì)節(jié),我們先在外層循環(huán)一下pages里面的頁(yè),再對(duì)每一頁(yè)的數(shù)組進(jìn)行一次維護(hù)。
<swiper :options="swiperOption">
<swiper-slide v-for="(page , index) of pages" :key = "index">
<div class="icon" v-for="item of page" :key="item.id">
<div class="icon-img">
<img class="icon-img-content" :src='item.imgUrl'>
</div>
<p class="icon-desc">{{item.desc}}</p>
</div>
</swiper-slide>
</swiper>
說(shuō)一下用到的CSS穿透:swiper自帶的一些class里面的屬性我們?cè)趺锤模?/p>


- Recommand
這里要提到的是:
ellipsis() 這個(gè)函數(shù)是為了讓一行不用顯示滿(mǎn),多余的不換行而是變成...
把這個(gè)函數(shù)直接封裝在常量表,他長(zhǎng)這個(gè)樣子
ellipsis()
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
還有,我們可以用一個(gè)router-link來(lái)進(jìn)行跳轉(zhuǎn)操作,目的地由:to屬性指定,然后還可以指定這個(gè)標(biāo)簽的類(lèi)型,用tag就好
<router-link
:to="'/detail/'+item.id"
tag="li"
class="item border-bottom"
v-for="item of recommandList"
:key="item.id"
>
結(jié)構(gòu): ul>router-link>img+(div>title+desc+button)
- city
- City.vue
同樣要ajax請(qǐng)求,同時(shí)這個(gè)頁(yè)面有l(wèi)etter這個(gè)屬性。這個(gè)字符串會(huì)跟其子組件的某些letter進(jìn)行雙向綁定。
接收到ajax返回的數(shù)據(jù)后,會(huì)傳給需要的子組件。 - header
header沒(méi)什么特別的,就布局一下。但是也有一個(gè)router-link標(biāo)簽to向返回主頁(yè)。充當(dāng)返回鍵。 - List
這個(gè)組件的html結(jié)構(gòu)是
而且城市列表是一個(gè)嵌套循環(huán)結(jié)構(gòu)。
list>(當(dāng)前城市+熱門(mén)城市+城市列表)
其中當(dāng)前城市從mapstate中取得,并作為一個(gè)計(jì)算屬性。
整個(gè)頁(yè)面是只有屏幕框大小的,overflow-hidden
而list的滑動(dòng)效果怎么實(shí)現(xiàn)呢?
position: absolute
overflow: hidden
top: 1.58rem
left: 0
right: 0
bottom: 0
Bscroll傳一個(gè)元素進(jìn)去,就可以在這個(gè)元素實(shí)現(xiàn)滑動(dòng)效果,于是我們得以滑動(dòng)list上的城市。
具體操作:在html標(biāo)簽上指定ref="wrapper",然后通過(guò)this.$refs.wrapper可以得到元素的引用。
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper)
}
同時(shí),點(diǎn)擊事件會(huì)更改當(dāng)前城市,在熱門(mén)城市和普通城市的div都綁定了handle事件,在其中觸發(fā)changecity事件修改vuex中的事件
handleCityClick (city) {
// this.$store.commit('changeCity', city)
this.changeCity(city)
this.$router.push('/')
},
//這里用展開(kāi)運(yùn)算符和map輔助函數(shù)完成將mutations里面changeCity和this.changeCity綁定起來(lái)。
...mapMutations(['changeCity'])
還對(duì)letter這個(gè)變量進(jìn)行了偵聽(tīng)器,當(dāng)letter變化的時(shí)候。
通過(guò)refs取到各個(gè)list的子項(xiàng)元素,其是一個(gè)數(shù)組,且只有一項(xiàng),然后讓當(dāng)前的scroll滾動(dòng)到對(duì)應(yīng)位置。
if (this.letter) {
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
- alphabet
字母表的標(biāo)簽指令如下:
<li
class="item"
v-for="item of letters"
:key="item"
@click="handleClick"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:ref = "item"
>
class,外層循環(huán),key,點(diǎn)擊事件,拖動(dòng)事件,ref屬性方便取到組件。
touchStatus: false,
startY: 0,
timer: null
updated在頁(yè)面的data改變且頁(yè)面要重新渲染的時(shí)候就會(huì)變化。
下面這個(gè)函數(shù)會(huì)在點(diǎn)擊字母的時(shí)候向外相應(yīng)一個(gè)change函數(shù)且被其父組件city組件捕捉到,而city組件根據(jù)傳出來(lái)的參數(shù)改變其 this的letter,且這個(gè)letter又與list這個(gè)子組件的letter雙向綁定,達(dá)到兩個(gè)子組件通信的效果。
handleClick (e) {
this.$emit('change', e.target.innerText)
},
下面看觸摸事件的處理:
handleTouchStart () {
this.touchStatus = true
},
handleTouchEnd () {
this.touchStatus = false
}
這兩個(gè)不必說(shuō),看最主要的函數(shù):
handleTouchMove (e) {
if (this.touchStatus) {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - this.startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}, 16)
}
},
這個(gè)函數(shù)首先使用了事件防抖,16ms內(nèi)只觸發(fā)一次函數(shù),然后計(jì)算一下每次觸摸大的位置,與字母表與頂端相減,然后算出你點(diǎn)了哪個(gè)元素,把這個(gè)字母通過(guò)index發(fā)送出去。
- search
有一個(gè)li標(biāo)簽在最底下,它有v-show屬性,根據(jù)有沒(méi)有搜索到一些值來(lái)顯示。
用V-model對(duì)輸入框的數(shù)據(jù)進(jìn)行了雙向綁定。然后watch了他的變化。也使用了事件防抖。遍歷整個(gè)cities數(shù)組,用indexof查找spell和name有沒(méi)有共同,有則加入結(jié)果,然后將this.list重新賦值為這個(gè)res。
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.keyword) {
this.list = []
return
}
this.timer = setTimeout(() => {
const result = []
for (let i in this.cities) {
this.cities[i].forEach((value) => {
if (value.spell.indexOf(this.keyword) > -1 ||
value.name.indexOf(this.keyword) > -1) {
result.push(value)
}
})
}
this.list = result
}, 100)
- Detail
- Detail.vue
這里面同樣發(fā)送了ajax請(qǐng)求。注意對(duì)get參數(shù)的傳遞方式。可以這樣。其中route.params.id是從url取參數(shù)。
axios.get('/api/detail.json?', {
params: {
id: this.$route.params.id
}
})
.then(this.getHomeInfoSucc)
- banner
banner中就調(diào)用了畫(huà)廊組件,也即是公共組件里的gallary
<fade-animation>
<common-gallary
:imgs="bannerImgs"
v-show="showGallary"
@close="handleGallaryClose"
></common-gallary>
</fade-animation>
這個(gè)組件有個(gè)show屬性,
