一、项目概述:
和我去旅行app是一个为旅游者提供信息服务的平台,主要功能模块包括:城市首页、城市选择页、城市详情页等。项目源码:gitee.com/wang-xinshe…
二、技术栈:
- Vue框架:Vue Vue-router Vuex Vue-cli
- 使用插件:vue-awesome-swiper(轮播) better-scroll(滑块) axios(数据请求)
- Css预处理: less工具
- Api: 使用mock静态json数据
三、项目页面结构分析
1.首页home部分
- header:返回键+搜索框+城市选择
- Swiper:图片轮播vue-awesome-swiper实现
- Icons:图标区域轮播iconfont引入和使用
- Recommend:城市详情
- Weekend:周末游玩推荐
- 其他:less变量和混合的使用,axios数据请求
2.城市选择页city部分
- Header:返回键+路由跳转
- Search:搜索框+搜索逻辑实现
- CityList:better-scroll的使用
- CityAlphabet:字母表,函数节流实现列表性能优化
- 其他:Vuex 实现数据共享、LocalStorage 实现页面数据存储、keep-alive 优化路由性能
3.详情页detail部分
- Banner:图片轮播区
- Header:返回键+路由跳转
- List:递归组件实现详情列表实现、
- FadeAnimation:组件动画 transition实现header渐隐渐显效果
- Gallary:公用画廊组件拆分、transition slot 插槽实现 animation 简单动画效果
- 其他:对全局事件的解绑
四、具体代码实现:
- home部分icon图标实现分页切换
<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>
// 定义一个 pages 数据,计算轮播页数并进行遍历
pages () {
const pages = []
this.list.forEach((item,index) => {
// 定义一个 page ,表示当前的元素应当展示在轮播图的第几页,超过 8 就展示在第二页,以此类推
const page = Math.floor(index / 8)
// 如果页面不存在,则返回一个空数组,其作用是将 iconList 数组转换为 二维数组
if (!pages[page]) {
pages[page] = []
}
pages[page].push(item)
})
return pages
}
2. 利用less定义变量和混合进行代码复用
//mixin.ess
.textoverflow {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
//varibles.less
@bc : #00bcd4;
@textColor : #333;
@headerHeight : .86rem;
3.利用vuex共享数据并用localstorage存储城市信息
//store下的index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations
})
//store下的state.js
// 使用 try 进行包裹,以防 在无痕模式,或者其他原因,导致 localStorage 失效
let defaultCity = '深圳'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) { }
export default {
// 如果没有选中的数据,则会渲染 '北京'
// city: localStorage.city || '北京'
city: defaultCity
}
//store下的mutations.js
export default {
changeCity (state, city) {
state.city = city
// localStorage 存储选中的数据,刷新也会继续渲染选中的数据,接上面使用 try
try {
localStorage.city = city
} catch (e) {}
}
}
4.vue-router实现页面跳转
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/pages/home/Home'
import City from '@/pages/city/City'
import Detail from '@/pages/detail/Detail'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},{
path: '/city',
name: 'City',
component: City
},
{
path: '/detail/:id',
name: 'Detail',
component: Detail
}],
// 每一次做 路由 切换的时候,就会 重新 将页面滑到顶端
scrollBehavior: function (to, from, savedPosition) {
return savedPosition || { x: 0, y: 0 }
}
})
//路由跳转
<router-link to=">
</router-link>
5.better-scroll 插件实现类似于原生 app 的页面上下拖动效果
mounted() {
this.scroll = new Bscroll(this.$refs.wrapper,{
click: true
})
}
6.alphabet 滑动逻辑和函数节流优化(用函数节流优化 handleTouchMove,提高性能)
//获取 A 字母距离顶部高度,滑动时计算当前位置距离顶部的距离,计算差值除以每个字母高度得出当前字母,并出发change时间改变位置
data () {
return {
touchStatus: false,
startY: 0,
timer: null
}
},
updated () {
this.startY = this.$refs['A'][0].offsetTop
},
methods: {
handleLetterClick(e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart() {
this.touchStatus = true
},
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])
}
}, 8);
}
},
handleTouchEnd() {
this.touchStatus = false
}
}
7.detail 页 header 渐变效果模板内容逻辑实现
/*
通过 showAbs 、 v-show 和 opacity 完成该效果的实现。\
利用 activated 钩子监听 scroll 触发 this.handleScroll。并在 methods 的 handleScroll 中完成渐隐渐现的算法逻辑。通过 document.documentElement.scrollTop 计算 opacity 属性即可实现该动画效果)
*/
<router-link to="/" custom v-slot="{ navigate }" v-show="showAbs">
<div @click="navigate" class="header-abs" role="link">
<div class="iconfont header-abs-back"></div>
</div>
</router-link>
methods: {
handleScroll () {
const top = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset
if (top > 60) {
let opacity = top / 140
opacity = opacity > 1 ? 1 : opacity
this.opacityStyle = { opacity }
this.showAbs = false
} else {
this.showAbs = true
}
}
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
destroyed () {
console.log("destroyed")
window.removeEventListener('scroll', this.handleScroll)
}
8.性能优化:
//keep-alive 优化
/*
当查看 network 时候,可以看到从首页到城市选择页切换过程中每次切换都会发送 ajax 请求。所以我们对此进行优化。
在 App.vue 中给 外部添加一个 标签。其含义是路由的内容被加载过一次之后,就把路由的内容放置到内存中,下一次再使用路由的时候,无需重新加载组件、执行钩子函数。只需要从内存中拿出以前的内容显示就可以了。
<div id="app">
<keep-alive exclude="Detail">
<router-view></router-view>
</keep-alive>
</div>
*/
/*
结合 keep-alive 新增的 activated 生命周期钩子,实现每次点击曾经选中过的城市,不发送 择变化的时候再进行 ajax 请求的优化。
*/
//判断首页city是否是当前city,减少数据请求
mounted(){
this.lastCity = this.city
this.getHomeInfo()
},
activated() {
// 判断城市是否变化,变化就请求 ajax 更新数据
if (this.lastCity !== this.city) {
this.lastCity = this.city
this.getHomeInfo()
}
}
//城市选择页搜索框函数节流优化搜索功能,提高性能
watch: {
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)
}
},
五、项目接口联调和项目真机测试
- 将mock数据替换成后端接口地址
- 将localhost地址替换成电脑ip地址