项目源码 : 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.eot
、iconfont.svg
、iconfont.ttf
、iconfont.woff
文件放入iconfont
文件夹下 - 打开
iconfont.css
,修改里面的路径 - 在
main.js
中引入iconfont.css
文件,import 'styles/iconfont.css'
- 在
Header.vue
中相应的位置加入<span class="iconfont"></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中添加轮播的结构代码,如下所示,在export
中return {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
文件,将swiperList
,iconsList
,recommendList
,WeekendList
等本地开发的模拟数据放置到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"></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.vue
、Recommend.vue
、Weekend.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"></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 :absolute
和overflow :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'
,通过ajax
从static/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.vue
和List.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
的变化。通过:ref
和this.$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-content
的div
结构;
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)安装vuex
:npm 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
派发一个名字为changeCity
的action
,并传入参数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.js
中Actions
操作。
(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 课程总结与后续学习指南