项目简介
去哪儿的webapp版,实现其中的home界面,城市选择页面,详情页部分逻辑和界面
代码结构
- build与config
使用vue-cli脚手架工具以及webpack搭建好的开发环境,build,config等目录中包含相关配置文件,具体待研究,但修改其中的代码后,本地服务器都要重新启动部分配置才能生route效。 - node_modules
用npm工具导入的部分插件和工具 -
src
1.assets,存放项目的静态资源,如图标,图标css,以及常用的常量。
2.common,放的是公共的组件,意为可能被各个页面引用的组件,包括两个组件,fade组件(封装着transition动画的样式),gallay(公共画廊组件:是一个轮播画廊)
3.pages,最核心的代码放置处(再后面会详细介绍,分别对应三个页面的)
4.router,放有路由配置文件。
5.store,放有vuex相关配置和代码 - static
此文件夹放有整个项目的json数据文件,因为没有后端服务器,所以请求的是本地的数据。(此处为了请求的路径定位,还修改了config->indexks文件的内容)
核心代码实现
- router:
router的作用就是路由,让url对应的path和应该调用的组件对应起来。其中详情页的页面比较特殊的,因为对于每个ID,有自己的详情页。
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是根组件所在地,我们看下它的结构,它引入了路由,fastClick是针对部分手机兼容的,几个CSS是一些基础样式,边框样式,图标样式,还引入了我们的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
主页有个城市显示在右上位置。这个东西我们使用了vuex技术存储在了store里。
通过这句可以取到store里的city值。
这里首先使用了mapstate这个辅助函数,他可以把多个state状态转化成该子组件的计算属性。使用...对象展开运算符还可以把这个对象拆分成一个个属性。
...mapState(['city'])
这个页面主要使用ajax发送了一些请求,
其中在mounted也就是第一次挂载的时候,请求一次,且更换城市。
用一个lastCity保存一下上一次的城市,再一次切换到此城市的时候,有可能vuex里面的city已经改了,比如在城市选择页修改了,那么我们就要重新发ajax请求,请求这个city特应的主页。
- header
这里也会取到vuex的city值,并放在header显示。
这里有个小细节是把代码重复用到的变量放进styl这个常量表里。
@import '~styles/varibles.styl'
.header
display: flex
line-height: $headerHeight
- swiper
这里用到了vue-asome-swiper这个插件。
最外层是模板
然后是wrapper包裹块
然后是swiper这个标签,这是插件实现好的被你引入的。
slide意为可滑动的区域。
下面还有个swiper-pagenation表示显示几个点的选项卡。
<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>
他们需要如此配置:(里面是各个参数的作用放在一起了)
swiperOption: {
pagination: '.swiper-pagination',
loop: true
//代表不自动滑动
autoplay: false
}
//fraction代表,用1/2的方式,默认是...的方式。
paginationType: 'fraction',
//这两个的作用是:如果不加这两个参数,重新进入swiper的时候,宽度计算会出现问题。
observeParents: true,
observer: true
- icons
这里主要实现细节:
计算属性里把ajax得到的数据进行一番计算,生成pages数组,每个pages数组里放的又是一个个图标,这样可以达到分页的效果了。
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的实现细节,我们先在外层循环一下pages里面的页,再对每一页的数组进行一次维护。
<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>
说一下用到的CSS穿透:swiper自带的一些class里面的属性我们怎么更改?
- Recommand
这里要提到的是:
ellipsis() 这个函数是为了让一行不用显示满,多余的不换行而是变成...
把这个函数直接封装在常量表,他长这个样子
ellipsis()
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
还有,我们可以用一个router-link来进行跳转操作,目的地由:to属性指定,然后还可以指定这个标签的类型,用tag就好
<router-link
:to="'/detail/'+item.id"
tag="li"
class="item border-bottom"
v-for="item of recommandList"
:key="item.id"
>
结构: ul>router-link>img+(div>title+desc+button)
- city
- City.vue
同样要ajax请求,同时这个页面有letter这个属性。这个字符串会跟其子组件的某些letter进行双向绑定。
接收到ajax返回的数据后,会传给需要的子组件。 - header
header没什么特别的,就布局一下。但是也有一个router-link标签to向返回主页。充当返回键。 - List
这个组件的html结构是
而且城市列表是一个嵌套循环结构。
list>(当前城市+热门城市+城市列表)
其中当前城市从mapstate中取得,并作为一个计算属性。
整个页面是只有屏幕框大小的,overflow-hidden
而list的滑动效果怎么实现呢?
position: absolute
overflow: hidden
top: 1.58rem
left: 0
right: 0
bottom: 0
Bscroll传一个元素进去,就可以在这个元素实现滑动效果,于是我们得以滑动list上的城市。
具体操作:在html标签上指定ref="wrapper",然后通过this.$refs.wrapper可以得到元素的引用。
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper)
}
同时,点击事件会更改当前城市,在热门城市和普通城市的div都绑定了handle事件,在其中触发changecity事件修改vuex中的事件
handleCityClick (city) {
// this.$store.commit('changeCity', city)
this.changeCity(city)
this.$router.push('/')
},
//这里用展开运算符和map辅助函数完成将mutations里面changeCity和this.changeCity绑定起来。
...mapMutations(['changeCity'])
还对letter这个变量进行了侦听器,当letter变化的时候。
通过refs取到各个list的子项元素,其是一个数组,且只有一项,然后让当前的scroll滚动到对应位置。
if (this.letter) {
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
- alphabet
字母表的标签指令如下:
<li
class="item"
v-for="item of letters"
:key="item"
@click="handleClick"
@touchstart.prevent="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:ref = "item"
>
class,外层循环,key,点击事件,拖动事件,ref属性方便取到组件。
touchStatus: false,
startY: 0,
timer: null
updated在页面的data改变且页面要重新渲染的时候就会变化。
下面这个函数会在点击字母的时候向外相应一个change函数且被其父组件city组件捕捉到,而city组件根据传出来的参数改变其 this的letter,且这个letter又与list这个子组件的letter双向绑定,达到两个子组件通信的效果。
handleClick (e) {
this.$emit('change', e.target.innerText)
},
下面看触摸事件的处理:
handleTouchStart () {
this.touchStatus = true
},
handleTouchEnd () {
this.touchStatus = false
}
这两个不必说,看最主要的函数:
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)
}
},
这个函数首先使用了事件防抖,16ms内只触发一次函数,然后计算一下每次触摸大的位置,与字母表与顶端相减,然后算出你点了哪个元素,把这个字母通过index发送出去。
- search
有一个li标签在最底下,它有v-show属性,根据有没有搜索到一些值来显示。
用V-model对输入框的数据进行了双向绑定。然后watch了他的变化。也使用了事件防抖。遍历整个cities数组,用indexof查找spell和name有没有共同,有则加入结果,然后将this.list重新赋值为这个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
这里面同样发送了ajax请求。注意对get参数的传递方式。可以这样。其中route.params.id是从url取参数。
axios.get('/api/detail.json?', {
params: {
id: this.$route.params.id
}
})
.then(this.getHomeInfoSucc)
- banner
banner中就调用了画廊组件,也即是公共组件里的gallary
<fade-animation>
<common-gallary
:imgs="bannerImgs"
v-show="showGallary"
@close="handleGallaryClose"
></common-gallary>
</fade-animation>
这个组件有个show属性,