Vue.js 入门 :去哪儿网APP案例 学习记录

推荐博客

Vue 2.5 开发 去哪儿 旅游网站项目记录

项目源码 : https://gitee.com/doublesgzl/Travel

第六章 项目实战


6-1 环境配置 (建议看下视频)
6-2 项目代码介绍
6-3 单文件组件与Vue中的路由
6-4 单页应用VS多页应用 解析1解析2解析3 GOOD
6-5 项目代码初始化


在这里插入图片描述

第7章 旅游网站首页开发


1.首页header开发
2.stylus预处理器的使用
3.iconfont 的使用
4.首页轮播图
5.图标区域页面布局和逻辑实现 参考
6.热销推荐组件和周末游组件开发
7.ajax 请求获取首页数据


1.首页header开发

1-1 创建Home.vue的子组件

  • pages/home/components/Header.vue
  • pages/home/Home.vue组件中引入子组件Header.vue
<script>
import HomeHeader from './components/Header'
export default {
  name: 'Home',
  components: {
    HomeHeader
  }
}
2.stylus预处理器的使用

2-1 stylus :CSS预处理器,在css中使用变量等,便于快速编写css代码

  • 安装stylus相关的库
  • npm install stylus --save
  • npm install stylus-loader --save
  • 具体使用:<style lang="stylus" scoped>scoped 确保只对当前的组件有效
  • 使用css变量:在styles文件夹下创建iable.styl文件,写入$bgColor = #00bcd4,然后在Header.vue中的style内通过@import引入iable.styl文件,以下代码中列举了三种引入方式,最后一种是在build/webpack.base.conf.js 中的resolve下的alias中设置了styles别名,这样一来路径就可以简写,在main.js中也可以用别名进行路径的简写。
/*@import '../../../assets/styles/variable.styl'*/
/*@import '~@/assets/styles/variable.styl'*/
@import '~styles/variable.styl'
  .header
    display:flex
    height:0.86rem
    line-height:0.86rem
    background-color:$bgColor
    color :#fff
    .header-left
      width:0.64rem
      float:left
      text-align :center
      .iconfont
        font-size :0.4rem
3.iconfont 的使用
  • 图标库->官方管理库->大麦官网图标库->选择需要的图标->加入购物车->到购物车中下载选中的图标
  • iconfont.css文件放入styles文件夹下,并创建一个iconfont文件夹,将下载的iconfont.eoticonfont.svgiconfont.ttficonfont.woff文件放入iconfont文件夹下
  • 打开iconfont.css,修改里面的路径
  • main.js中引入iconfont.css文件,import 'styles/iconfont.css'
  • Header.vue中相应的位置加入<span class="iconfont">&#xe624;</span>中间显示的是图标对应的代码,在图标网购物车中图标下面有显示,复制过来就行
4.首页轮播图

4-1 在git线上仓库中创建index-swiper分支,在终端Travel路径下依次执行

  • git pull
  • git checkout index-swiper
  • git status (查看现在的本地分支)

4-2 使用第三方轮播插件 vue-awesome-swiper快速构建轮播 (插件下载:github)

  • 插件安装 npm install vue-awesome-swiper@2.6.7 --save
  • 插件使用:在程序入口文件main.js中引入import VueAwesomeSwiper from 'vue-awesome-swiper'import 'swiper/dist/css/swiper.css',并使用轮播插件Vue.use(VueAwesomeSwiper);第二,在home/components底下再新建一个Swiper.vue组件,记得与Home.vue组件连接;第三,在Swiper.vue组件中的template中添加轮播的结构代码,如下所示,在exportreturn {swiperOption: {}},在<swiper-slide>中放置图片;
  • 注意事项:加载页面时,在轮播插件底下的文本内容会先显示,等轮播图片加载完成后,文本内容又被加载进来的图片撑到下面位置,这就导致页面的一个抖动现象。解决方法:在轮播组件外面放置一个<div class="wrapper">,设置该div的宽高比(代码在下面),保证轮播组件的位置,避免文本内容的抖动,写法比较特殊,要特别注意。
<template>
  <swiper :options="swiperOption" ref="mySwiper" @someSwiperEvent="callback">
    <swiper-slide>
      <img class="swipe-img" src="http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20197/ae8987bff39ff10e82675a3643154e66.jpg_750x200_0f187b2e.jpg" alt="去哪儿门票" style="opacity: 1;">
    </swiper-slide>
    <swiper-slide>
      <img class="swipe-img" src="http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20197/e43d3043ee612ad71dd6c68f29e3ed9a.jpg_750x200_b90f2963.jpg" alt="去哪儿门票" style="opacity: 1;">
    </swiper-slide>
    <swiper-slide>
      <img class="swipe-img" src="http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20193/d7bbc21db442366a882e04ddc984669a.jpg_750x200_85e640d9.jpg" alt="去哪儿门票" style="opacity: 1;">
    </swiper-slide>
    <!-- Optional controls -->
    <div class="swiper-pagination"  slot="pagination"></div>
    <div class="swiper-button-prev" slot="button-prev"></div>
    <div class="swiper-button-next" slot="button-next"></div>
    <div class="swiper-scrollbar"   slot="scrollbar"></div>
  </swiper>
</template>
<script>
export default {
  name: 'HomeSwiper',
  data: function () {
    return {
      swiperOption: {}
    }
  }
}
</script>
<style lang="stylus" scoped>
  .swipe-img
    width:100%
</style>
.wrapper
    overflow :hidden
    width: 100%
    height:0
    padding-bottom :31.25%

4-3 给轮播插件添加底部的轮播点
第一步,在data中返回pagination: '.swiper-pagination',发现底部出现蓝色的点;

 data: function () {
    return {
      swiperOption: {
        pagination: '.swiper-pagination'
      }
    }
  }

第二步,设置点的样式,不能写在.wrapper的下面,会失效!!!

// 样式穿透
.wrapper >>> .swiper-pagination-bullet-active
    background: #fff

4-4 swiper轮播图的图片循环

第一步,在swiper下面只保留一个swiper-slide标签,设置v-for=''item of SwiperList"

  <swiper-slide v-for="item of SwiperList" :key="item.id">
      <img class="swipe-img" :src="item.imgUrl" >
  </swiper-slide>

第二步,在data中加入轮播图片数据

data: function () {
    return {
      swiperOption: {
        pagination: '.swiper-pagination',
        loop: true  // 用于循环轮播,否则第一张图片前面就空了
      },
      SwiperList: [{
        id: '0001',
        imgUrl: 'http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20197/ae8987bff39ff10e82675a3643154e66.jpg_750x200_0f187b2e.jpg'

      }, {
        id: '0002',
        imgUrl: 'http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20197/e43d3043ee612ad71dd6c68f29e3ed9a.jpg_750x200_b90f2963.jpg'
      }, {
        id: '0003',
        imgUrl: 'http://mp-piao-admincp.qunarzz.com/mp_piao_admin_mp_piao_admin/admin/20193/d7bbc21db442366a882e04ddc984669a.jpg_750x200_85e640d9.jpg'
      }]
    }
  }

4-5 保存代码到本地仓库并提交到线上仓库

master分支放的是整个项目所有功能最新的代码,index-swiper分支上放的是具体功能开发完成的代码。一般开发时会自己开发一个新的分支,测试没问题后整合到master分支上。

  • git add .
  • git commit -m 'change'
  • git push (把本地index-swiper分支上的内容提交到线上index-swiper分支上)
  • git checkout master (合并,先切换到master分支上)
  • git merge origin/index-swiper (把线上index-swiper分支新增的内容合并到本地merge分支)
  • git push (把master分支的内容也提交到线上)
5.图标区域页面布局和逻辑实现

5-1 在线上git仓库上新建一个index-icons分支,然后在终端获取新创建的分支到本地

  • git pull
  • git checkout index-icons

5-2 实现图标区域布局,定义样式

5-3 图标区域逻辑实现,多页切换的轮播效果展示。
同Swiper组件在data中return一个iconsList数组,在template中使用轮播结构放入每个图标模块。 并在Chrome浏览器上安装Vue devtools插件(一种vue的调试工具),安装完毕后重启浏览器,在浏览器开发者工具的最上面一栏的最后就可以看见Vue一项了。

问题(1):当只有一条数据时,页面中仅可以操控图标模块范围进行滑动,在别的空白区域却不行,这里需要设置swiper的.swiper-container样式为icons的样式,从而解决此问题,.swiper-container类你可以在浏览器开发工具中选中区域进行查看。

<template>
<div class="icons">
  <swiper :options="swiperOption">
    <swiper-slide>
    
      <div class="icon" v-for="item of iconsList" :key="item.id" >
        <div class="icon-img" >
          <img class="icon-img-content" :src="item.imgUrl" >
        </div>
        <p class="icon-text">{{item.desc}}</p>
      </div>
      
    </swiper-slide>
  </swiper>
</div>
</template>
.icons >>> .swiper-container
    width :100%
    height :0
    padding-bottom :50%

问题(2):若共有10条数据,当前页面只能显示8条,另外两条被隐藏了,也无法滑动显示,此时,利用Vue中的计算属性来控制每个轮播页中数据的显示 ,更新swiper的数据绑定。

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
  }
}
   <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-text">{{item.desc}}</p>
        </div>
      </swiper-slide>
    </swiper>

5-4 用省略符代替长串文字,通过CSS样式控制

.icon-text
      ...
      overflow :hidden
      white-space :nowrap
      text-overflow :ellipsis

5-5 将省略符的CSS代码封装
创建styles/mixins.styl文件,写入以下代码,在Icons.vue的style标签内导入@import '~styles/mixins.styl',然后用ellipse()一句话即可

ellipse()
  overflow :hidden
  white-space :nowrap
  text-overflow :ellipsis
.icon-text
      ...
      ellipse()

5-6 保存代码到本地仓库并提交到线上仓库

  • git add .
  • git commit -m 'change'
  • git push
  • git checkout master
  • git merge origin/index-swiper
  • git push
6.热销推荐组件和周末游组件开发

6-1 在git线上仓库中创建index-recommend分支,在终端Travel下依次执行

  • git pull
  • git checkout index-recommend

6-2 新建components/Recommend.vue组件,同Home.vue组件连接;

6-3 先搭建好页面结构和CSS样式,再循环显示数据,套路基本同上

<template>
  <div class="">
    <div class="title">热销推荐</div>
    <ul>
      <!--border-bottom类:1像素边框,在项目最开始已经引入了border.css文件-->
      <li class="item border-bottom" v-for="item of recommendList" :key="item.id">
        <img  class="item-img" :src="item.imgUrl" alt="">
        <div class="item-info">
          <p class="item-title">{{item.title}}</p>
          <p class="item-desc">{{item.desc}}</p>
          <button class="item-btn">查看详情</button>
        </div>
      </li>
    </ul>
  </div>
</template>

6-4 周末游组件开发 代码同6差不多 微调就行
6-5 保存代码到本地仓库并提交到线上仓库
- git branch (查看所有分支)

7.ajax 请求获取首页数据

7-1 线上git仓库创建index-ajax分支,下载到本地仓库

  • git pull
  • git checkout index-ajax

7-2 如何在没有后端支持的情况下实现数据的模拟??
创建static/mock/index.json文件,将swiperListiconsListrecommendListWeekendList等本地开发的模拟数据放置到index.json中。之所以放到static文件夹下,是因为static文件夹里的内容可直接被外界访问到,比如在浏览器中输入http://localhost:8080/static/mock/index.json,就可以显示出/index.json中的内容。

7-3 /index.json中存储的是一些本地开发的模拟数据,并不希望它同代码上传到本地/线上仓库,此时,在.gitignore文件中添加static/mock即可。

7-4 统一在Home.vue中发送一次Ajax请求,而不要试图在每个组件中都发送Ajax请求:
(1)首先在Home.vue中导入import axios from 'axios',记得提前安装axios库;
(2)让页面挂载好之后去执行getHomeInfo()方法;

mounted () {
    this.getHomeInfo()
  }

(3)在函数中定义getHomeInfo()方法,去获取Ajax数据,axios返回的结果是一个promise对象;数据获取成功后调用getHomeInfoSucc()输出结果。

methods: {
    getHomeInfo () {
      axios.get('/api/index.json')
        .then(this.getHomeInfoSucc)
    },
    getHomeInfoSucc (res) {
      console.log(res)
    }
  }

7-5 在实际项目开发中,不建议随意改动axios.get(‘/api/index.json’)中的地址,那如何转换到本地模拟的接口地址(/static/mock/index.json)呢?
【解决】在vue中提供了proxy代理功能,以此实现接口地址的转换。在config/index.js文件中设置:

 proxyTable: {
      '/api':{
        target:'http://localhost:8080',
        pathRewrite:{
          '^/api':'/static/mock'
        }
      }
    }

这里理解成用"/api"代替target里面的地址,后面组件中调用接口时直接用api代替 。比如调用"http://localhost:8080/static/mock/index.json",直接写"/api/index.json"即可。注意,它不是vue提供的,而是webpack dev server 提供的。

7-6 首页父子组件的数据传递
在父组件中

<template>
  <div>
    <home-header :city="city"></home-header>
    <home-swiper :list="SwiperList"></home-swiper>
  </div>
</template>
export default {
  ...
  data () {
    return {
      city: '',
      SwiperList: []
    }
  },
  mounted () {
    this.getHomeInfo()
  },
  methods: {
    getHomeInfo () {
      axios.get('/api/index.json')
        .then(this.getHomeInfoSucc)
    },
    getHomeInfoSucc (res) {
      res = res.data
      if (res.ret && res.data) {
        this.city = res.data.city
        this.SwiperList = res.data.swiperList
      }
    }
  }
 }

在子组件中,props是子组件访问父组件数据的唯一接口。若子组件想引用父组件的数据,需要在props中声明一个变量city,变量city就可以引用父组件的数据(<home-header :city="theCity"></home-header>)。然后在子组件<template>模板里渲染变量city,此时渲染出来的就是父组件中的数据。

<template>
  <div class="header">
   ...
    <div class="header-right">
      {{this.theCity}}
      <span class="iconfont icon-right">&#xe6aa;</span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'HomeHeader',
  props: {
    theCity: String
  }
}
</script>

7-7 在Swiper组件的数据传递中出现问题:默认显示的图片是第四张而不是第一章图片?这是因为Swiper最初是通过空数组创建的,然后Ajax获得数据后才变成真正的数据,为避免此问题,首先判断list数组是否为空,待获取到真正的数据才创建Swiper

<swiper :options="swiperOption" v-if="list.length">

进一步,在<template>模板中尽量避免出现一些逻辑性的代码,比如v-if="list.length",这里可以使用computed计算属性来实现数组长度的计算,然后在v-if中直接使用showSwiper"计算属性即可。

<swiper :options="swiperOption" v-if="showSwiper">
...
computed: {
    showSwiper () {
      return this.list.length
    }
  }

7-8 Icons.vueRecommend.vueWeekend.vue 数据绑定

7-9 Swiper.vue组件中的图片自动轮播

data: function () {
    return {
      swiperOption: {
        pagination: '.swiper-pagination', // 轮播点
        loop: true,
        autoplay: 3000 // autoplay: true虽然自动轮播了但是速度很快,直接在后面跟时间设置就好
      }
    }
  },

7-10 保存代码 合并到master分支

第8章 旅游网站"城市列表"页面开发


1 路由配置 + 搜索框布局
2 列表布局
3 BetterScroll 的使用和字母表布局
4 页面的Ajax动态数据渲染
5 兄弟组件数据传递 及 列表性能优化
6 搜索逻辑实现
7 Vuex实现数据共享
8 Vuex的高级使用及localStorage
9 使用keep-alive优化网页性能


在这里插入图片描述

1 路由配置

1-1 创建city-router分支。在router/index.js中配置City.vue的路由信息,导入import City from '@/pages/city/City'

 routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/city',
      name: 'City',
      component: City
    }
  ]

1-2 【首页到城市列表页面的跳转】在Header.vue中,给“北京”的div外面包围<router-link to="/city"></router-link>,地址根据index.js的路由配置决定。

成功跳转后,“北京”字体颜色变成绿色,是因为添加<router-link to="/city"></router-link>过程中,给div包裹了一个a标签,默认显示绿色。这里给header-right设置字体颜色为白色即可。

<router-link to="/city">
    <div class="header-right">
      {{this.city}}
      <span class="iconfont icon-right">&#xe6aa;</span>
    </div>
    </router-link>

1-3 在“城市列表页面”左上角添加返回按钮,同样用router-link链接。
1-4【搜索框布局】创建Search.vue组件,添加input框设置样式即可。
1-5 将city-router分支合并到master分支上。

2 列表布局

2-1 新建city-list仓库。创建List.vue组件,添加3个list模块,设置样式。
2-2 给整个list设置position :absoluteoverflow :hidden,固定页面的高度,使屏幕不随内容的增加而变长。

.list
    overflow :hidden
    position :absolute
    top: 1.58rem
    left:0
    right :0
    bottom :0
3 BetterScroll 的使用和字母表布局

BetterScroll 插件(github)实现页面的滚动。

3-1 插件安装

  • npm install better-scroll --save
  • 以下是Better-scroll插件使用的结构要求,要按照这种结构编写项目代码
<div class="wrapper">
 <ul class="content">
   <li>...</li>
   <li>...</li>
   ...
 </ul>
</div>

3-2 插件使用

  • 导入import BScroll from 'better-scroll'
  • 设置<div class="list" ref="wrapper">ref 用于获取DOM元素
  • mounted中设置如下,然后页面可以上下拖动了
mounted () {
    // 获取DOM元素,相当于let list = document.querySelector('.list');let scroll = new BScroll(list)
    this.scroll = new BScroll(this.$refs.wrapper)
  }

3-3 字母表
新建Alphabet.vue组件,添加一组<div class="list"><ul><li class="item"></li>...</ul>来做字母表,设置绝对位置,样式如下。

.list
    display :flex   // 弹性布局
    flex-direction :column  // 主轴方向为纵轴
    justify-content :center // 纵向居中
    position :absolute  // 绝对位置
    right:0   // 向右靠
    top:1.58rem // 设置顶部距离
    bottom :0  // 设置底部距离
    width:.4rem //设置字母表的宽度
    .item
      text-align :center
      line-height :.4rem
      color :$bgColor

3-4 将city-list分支合并到master分支上

4 页面的动态数据渲染(Ajax)

4-1 创建city-ajax分支。在City.vue中导入import axios from 'axios',通过ajaxstatic/mock/city.json中模拟数据的获取。

 data () {
    return {
      cities: {},
      hotCities: []
    }
  },
  mounted () {
    this.getCityInfo()
  },
  methods: {
    getCityInfo () {
      axios.get('/api/city.json')
        .then(this.handleGetCityInfoSucc)
    },
    handleGetCityInfoSucc (res) {
      res = res.data
      if (res.ret && res.data) {
        const data = res.data
        this.cities = data.cities
        this.hotCities = data.hotCities
      }
    }
  }

4-2 在City.vue中的template中传递数据给子组件

<city-list :cities="cities" :hot ="hotCities"></city-list>

4-3 在List.vue子组件中,通过props获取父组件传递过来的数据,并使用v-for循环遍历输出。注意:v-for可以遍历数组对象,在遍历对象时,<div class="area" v-for="(items,key) of cities" :key="key">key表示对象中的属性,可以用来当做key,比如var list = {"A":[{"a"},{"b"}],"B":[{"c","d"}]};,其中,key代表的就是“A”,"B","C"

 props: {
    hot: Array,
    cities: Object
  }
<div class="area" v-for="(items,key) of cities"  :key="key">
      <div class="title border-topbottom">{{key}} </div>
      <div class="item-list">
        <div class="item border-bottom"  v-for="innerItem of items" :key="innerItem.id">{{innerItem.name}}</div>
      </div>
</div>

4-4 在Alphabet.vue组件中,用v-for循环遍历props从父组件获取的对象数据;

4-5 将city-ajax分支合并到master分支上;

5 兄弟组件数据传递

点击字母表,希望相应的内容能自动滚动到页面的显示区域中。

5-1 新建city-components分支;
5-2 在Alphabet.vue的循环项li中添加click事件,在methods中写入handleLetterClick()那么,如何在Alphabet.vueList.vue之间传值呢?

 <li class="item" 
 		v-for="(item,key) of cities" 
 		:key="key" 
 		@click="handleLetterClick">
 		{{key}}
 </li>
methods: {
    handleLetterClick (e) {
      console.log(e.target.innerHTML)
    }
  }

5-3 (1)bus总线传值;(2)让Alphabet.vue把数据传给City.vue,再由City.vue把数据传递给List.vue

使用方式(2)进行传值。首先,由Alphabet.vue子组件在循环元素上添加点击事件handleLetterClick()中,通过this.$emit()件向外触发change事件,传递数据给City.vue父组件,this.$emit('change', e.target.innerHTML)其次,由City.vue父组件监听@change="handleLetterChange"事件,并在methods中定义handleLetterChange()来处理从Alphabet.vue接收到的数据。

<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
handleLetterChange (letter) {
      console.log(letter)
    }

5-4 由City.vue父组件将接收到的数据通过属性的形式(:letter="letter")转发给List.vue子组件。

...
<city-list :cities="cities" :hot ="hotCities" :letter="letter"></city-list>
...
 data () {
    return {
      ...
      letter: ''
    }
  },
  methods: {
    ...
    handleLetterChange (letter) {
      this.letter = letter
    }
  }
  

5-5 最后,在List.vue子组件中通过props接收City.vue父组件传递过来的数据。

5-6 拿到点击的字母后(this.letter发生改变),如何使相应的列表内容滚动显示在页面中???

【解决】 List.vue子组件中用watch监听letter的变化。通过:refthis.$refs[this.letter]获取对应的DOM元素,然后通过this.scroll.scrollToElement(this.$refs[this.letter][0])`一步实现。

props: {
    hot: Array,
    cities: Object,
    letter: String
  }
...
<div class="area" v-for="(items,key) of cities"  :key="key" :ref="key">
...
import BScroll from 'better-scroll'
...
mounted () {
    // 获取DOM元素,相当于let list = document.querySelector('.list');let scroll = new BScroll(list)
    this.scroll = new BScroll(this.$refs.wrapper)
  },
watch: {
    // 在watch中监听letter的变化
    letter () {
      if (this.letter) {
        // 返回的ele是一个数组,但在this.scroll.scrollToElement()中参数必须是一个DOM元素,所以传入数据为ele[0]
        const ele = this.$refs[this.letter]
        this.scroll.scrollToElement(ele[0])
      }
    }
  }

5-7 滑动字母表,使左侧的城市列表也会随之变动。

Alphabet.vue中给循环项绑定三个事件:@touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd",在methods中定义这三个函数,为保证在touchStart 事件之后才可以触发touchMove事件,在data中定义touchStatus: false来控制事件顺序。

...
data () {
    return {
      touchStatus: false
    }
},
methods: {
    ...
    handleTouchStart (e) {
      this.touchStatus = true
    },
    handleTouchMove () {

    },
    handleTouchEnd () {
      this.touchStatus = false
    }
}

接下来,需要知道当前滑动到的是字母表中的第几个字母,需要根据位置来进行判断。首先,通过计算属性 遍历cities获取存储字母表的数组letters

computed: {
    letters () {
      const letters = []
      for (let i in this.cities) {
        letters.push(i)
      }
      return letters
    }
}

获取到letters数组后,在template中用letters数组代替cities对象来遍历,

 <li class="item" 
 	v-for="item of letters" 
 	:key="item" 
 	@click="handleLetterClick"  
 	@touchstart="handleTouchStart" 		
 	@touchmove="handleTouchMove" 
 	@touchend="handleTouchEnd">
     {{item}}
</li>

下一步,在handleTouchMove()事件中计算距离,来判断当前手指滑动所在位置的字母,并将该字母传递给City.vue父组件。

handleTouchMove (e) {
      if (this.touchStatus) {
        // startY是字母"A"至list顶部的距离
        const startY = this.$refs['A'][0].offsetTop
        // e.touches[0]表示一些手指的信息,clientY是手指到页面最顶部的距离
        // 当前手指到list顶部的距离 = 手指到页面最顶部的距离 - 页面顶部蓝色区域的高度
        const touchY = e.touches[0].clientY - 79
        // index从0计数: 当前手指所在的位置是第index个字母 = (当前手指到list顶部的距离 - 字母"A"至list顶部的距离)/ 每个字母的高度
        const index = Math.floor((touchY - startY) / 20)
        // 这里要判断index的值,不要让它超出范围,不然后面数据都不存在,会出错
        if (index >= 0 && index <= this.letters.length) {
          this.$emit('change', this.letters[index])
        }
      }
 }

5-8 列表性能优化
(1)在handleTouchMove()事件中,this.startY是固定的,不需要每次都计算一遍,因此把它提取出来,在updated()钩子中执行,在handleTouchMove()中只要调用this.startY即可,

 data () {
    return {
      touchStatus: false,
      startY: 0
    }
  },
  // 当页面的数据被更新的同时页面完成渲染后,updated钩子函数执行
  updated () {
    // startY是字母"A"至list顶部的距离
    this.startY = this.$refs['A'][0].offsetTop
  }

(2)函数节流 限制函数执行的频率

如果this.timer已经存在,把this.timer去除掉,否则创建一个新的this.timer
延迟16ms之后再执行,假设在这16ms之内又进行了手指的滚动,那么它会把上一次要做的动作清除掉并重新执行这一次要做的事情!!!

data () {
    return {
      touchStatus: false,
      startY: 0,
      timer: null
    }
 },
 handleTouchMove (e) {
      if (this.touchStatus) {
        if (this.timer) {
          clearTimeout(this.timer)
        }
        this.timer = setTimeout(() => {
         // e.touches[0]表示一些手指的信息,clientY是手指到页面最顶部的距离
         // 当前手指到list顶部的距离 = 手指到页面最顶部的距离 - 页面顶部蓝色区域的高度
         const touchY = e.touches[0].clientY - 79
         // index从0计数: 当前手指所在的位置是第index个字母 = (当前手指到list顶部的距离 - 字母"A"至list顶部的距离)/ 每个字母的高度
         const index = Math.floor((touchY - this.startY) / 20)
         // 这里要判断index的值,不要让它超出范围,不然后面数据都不存在,会出错
         if (index >= 0 && index <= this.letters.length) {
            this.$emit('change', this.letters[index])
         }
        }, 16)
      }
 }
6 搜索逻辑实现

功能 :当搜索城市名字或拼音时,能把搜索结果显示出来

6-1 新建city-search-logic分支。在data中存一个keyword数据,让input框的内容同keyword做一个双向绑定(v-model)。

<input type="text" class="searchInput" placeholder="输入城市名或拼音" v-model="keyword">
...
data () {
    return {
      keyword: ''
    }
  },

6-2 Search.vue子组件接受City.vue父组件传递的cities数据,用于搜索词的匹配。

<city-search :cities="cities"></city-search>
props: {
    cities: Object
 }

6-3 在data中定义list空数组,用watch监听keyword的变化,并增加一个节流函数,提高代码的执行效率

 <ul>
        <li v-for="item of list" :key="item.id">{{item.name}}</li>
 </ul>
...
data () {
    return {
      keyword: '',
      list: [],
      timer: null
    }
  },
  watch: {
    keyword () {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(() => {
        const result = []
        // 遍历cities对象中的内容
        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)
  }

6-4 修改<li class="search-item">的样式

.search-content
    ...
    background :#eee
    .search-item
      line-height:.62rem
      padding-left :.2rem
      color: #666
      background :#fff

6-5 若搜索结果很多,但是页面显示内容有限,如何实现页面搜索结果的滚动?
引入import BScroll from 'better-scroll',在mounted钩子函数中创建一个scroll,this.scroll = new BScroll(this.$refs.search),其中传入的是类名为search-contentdiv结构;

6-6 在搜索框中输入搜索内容,会显示搜索结果,但此时若是清空搜索框内容,页面中依然存在原来的搜索结果。
keyword监听代码中判断keyword是否为空,若为空,则设置list = [],返回即可。

if (!this.keyword) {
        this.list = []
        return
 }

6-7 在input中输入一长串内容,但无内容匹配。

<ul></ul>中再添加一个li标签,<li class="search-item border-bottom" v-show="list.length">没有找到匹配数据</li>,当list数组为空,则显示“没有找到匹配数据”。

前面说到,当尽量避免在template中使用逻辑运算,可用计算属性代替。

<li class="search-item border-bottom" v-show="hasNoData">没有找到匹配数据</li>
...
computed: {
    hasNoData () {
      return !this.list.length
    }
 }

6-8 实现页面搜索结果与城市列表的切换,即未搜索时显示城市列表,搜索时显示搜索结果。用v-show="keyword"来判断即可,keyword为空,则不显示。

6-9 保存代码,将city-search-logic分支合并到master分支

7 使用Vuex实现首页和城市列表页面的数据共享

功能:将城市页面中的数据传递给首页。

City.vue组件和Home.vue组件没有共有的父组件,无法实现数据的中转,而使用Bus总线又会很麻烦。在Vue中提供了一个vuex数据框架

下图中虚线框部分称为store
在这里插入图片描述

7-1 vuex的初步使用

(0)新建city-vuex分支
(1)安装vuexnpm install vuex --save;
(2)创建src/store/main.js,写入

import Vue from 'vue'
import Vuex from 'vuex'

// 使用Vuex插件
Vue.use(Vuex)

// 导出Vuex创建的一个store仓库
export default new Vuex.Store({
  //  state存放的是全局公用的数据
  state: {
    city: '北京'
  }
})

(3)在src/main.js中 导入vuex

import store from './store/main.js'
...
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

7-2 让Header.vue组件使用vuex的数据
以前,Header.vue中的city是由Home.vue父组件传递进来的,而且该数据是Home.vue父组件通过Ajax获取的。现在我们希望city是前端存储的,而不是后端返回给前端的,所以在Home.vue父组件中把city相关的数据都删除掉,把static/mock/city.json中的city数据也删除,此时页面就不会显示城市名。

如何将公用数据中的城市名称显示出来呢?

Header.vue中删除props数据,将{{this.city}}改成{{this.$store.state.city}},页面中就能正常显示城市名称了。

this.$store就是store/main.js中创建的Vuex.Store(),由于在src/main.js创建根实例时把store传递进去了,然后它会被派发到每一个子组件中,所以在每个子组件中都可以通过this.$store获取到公用数据。

7-3 在城市列表页面中,修改当前城市的显示。

<div class="button">{{this.$store.state.city}}</div>

7-4 当点击列表中的城市时,让公用数据中的this.$store.state.city发生变化,流程:子组件->Actions->Mutations->State

(1)给List.vue组件,热门城市中的每个循环项绑定一个@click=“handleCityClick(item.name)”事件,并传入相应的item.name

(2)在methods方法中,通过dispatch派发一个名字为changeCityaction,并传入参数city;另外,在store/main.js中定义action操作。

methods: {
    handleCityClick (city) {
      this.$store.dispatch('changeCity', city)
    }
  }
export default new Vuex.Store({
  //  state存放的是全局公用的数据
  state: {
    city: '北京'
  },
  actions: {
    changeCity (ctx, city) {
      console.log(city)
    }
  }
})

(3) 调用Mutations改变公用的数据state.city

 actions: {
    // ctx调用commit()方法
    changeCity (ctx, city) {
      ctx.commit('changeCityMutation', city)
    }
  },
  mutations: {
    // state参数指的是所有的公用数据
    changeCityMutation (state, city) {
      state.city = city
    }
  }

(4) 在上方操作中,并没有用到异步执行,可以直接跳过Actions步骤,让子组件直接使用Mutations变更状态。
List.vue中,将this.$store.dispatch('changeCity', city)替换为this.$store.commit('changeCityMutation', city) ,删除store/main.jsActions操作。

(5)实现:点击城市列表中的列表项,实现数据的动态更新。

<div class="item border-bottom"  v-for="innerItem of items" :key="innerItem.id" @click="handleCityClick(innerItem.name)">{{innerItem.name}}</div>

(6)实现:城市搜索时,点击下方搜索结果的城市,实现数据的动态更新。

(7)实现:在城市列表页面中选择某城市后,自动跳转到首页。
涉及到Vue Router中的编程式导航I’m here

methods: {
    handleCityClick (city) {
      this.$store.commit('changeCityMutation', city)
      // 跳转到首页,一步就好
      this.$router.push('/')
    }
  }

(8)页面跳转方式小结

网页页面跳转的两种方式1)a标签的跳转2)JS的window.location.href实现跳转
vue跳转的两种方式1)router-link 标签的跳转2)JS的编程式导航 router.push()实现页面跳转

7-5 合并city-vuex分支到master上。

后面的内容因为电脑故障 导致笔记丢失 可以点击前面章节中的链接分别查看相关内容。


第9章 旅游网站详情页面开发

渐隐渐显Header组件
公用组件的拆分
路由参数的获取与处理
递归组件的使用
通用动画效果的代码封装

9-1 动态路由和banner布局
9-2 公用图片画廊组件拆分
9-3 实现Header渐隐渐显效果
9-4 对全局事件的解绑
9-5 使用递归组件实现详情页列表
9-6 动态获取详情页面数据
9-7 在项目中加入基础动画

第10章 项目的联调,测试与发布上线

项目的联调,测试,及发布的详细流程;
过程中可能遇到的问题及修复方案;
异步组件,提高大型项目的首屏速度;

10-1 项目前后端联调
10-2 真机测试
10-3 打包上线
10-4 异步组件实现按需加载
10-5 课程总结与后续学习指南

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值