和我去旅行

226 阅读4分钟

一、项目概述:

和我去旅行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 简单动画效果
  • 其他:对全局事件的解绑

四、具体代码实现:

  1. 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">&#xe624;</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地址