兄弟组件之间的联动
所谓的兄弟组件之间的联动,其实就是实现点击右侧的字母就能跳转至对应的首字母城市,因此列表组件需要知道右侧的字母列表的点击事件所对应的元素字母,这就需要兄弟组件间的数据传递了(Alphabet组件与List组件之间的通信),可以使用到中央数据总线Bus,但是由于这里的业务逻辑不是很复杂,因此可以将Alphabet组件内的信息传递到City组件(子组件向父组件传递信息,发布订阅模式),然后City组件向List组件传递信息(父组件向子组件传递信息,属性传值方式)。
在gitee的分支栏点击新建分支city-components,然后记得将本地master分支切换到city-components分支。
字母点击跳转
打开Alphabet.vue文件,给字母表中的字母添加一个click事件,然后尝试将点击的字母在控制台上进行输出显示:
{{key}}
然后在script标签添加这个handleLetterClick方法:
methods: { handleLetterClick (e) { console.log(e.target.innerText) } }
可以测试一下当你点击右侧列表中的字母,控制台是否真的输出了对应的字母:
前面说了由于这里的业务逻辑不是很复杂,因此可以将Alphabet组件内的信息传递到City组件(子组件向父组件传递信息,发布订阅模式),然后City组件向List组件传递信息(父组件向子组件传递信息,属性传值方式)。
那就开始编写使用发布订阅模式实现Alphabet组件内的信息传递到City组件的代码。修改子组件Alphabet.vue中handleLetterClick函数代码为:
methods: { handleLetterClick (e) { /** 注意此处必须使用innerText而不是innerHTML **/ this.$emit('change', e.target.innerText) } }
接着去父组件City.vue中监听该chang事件,请定义相应的handleLetterChange去接收子组件传递过来的信息:
handleLetterChange (letter) { console.log(letter) }
控制台测试发现当你点击右侧列表中的字母,控制台仍然输出了对应的字母。接下来就是父组件City.vue通过属性传值的方式将数据传递给子组件List.vue。修改父组件City.vue中handleLetterChange函数代码为:
handleLetterChange (letter) { this.letter = letter }
然后在父组件中的data中返回letter,并将其通过属性传值给city-list组件:
... ...
data () { return { ... letter: '' }
然后在子组件List.vue中通过props来接收数据:
props: { cities: Object, hotCities: Array, letter: String },
当List.vue发现letter有变化的时候,就显示跟letter首字母相同的城市列表,这种功能可以通过侦听器来实现:
watch: { letter () { console.log(this.letter) } }
控制台测试发现当你点击右侧列表中的字母,控制台仍然输出了对应的字母。
还记得之前推荐的那篇关于better-scroll的文章:当 better-scroll 遇见 Vue么,里面介绍了如何滚动至某个元素。既然是滚动至某个元素,那么肯定需要选择某个DOM节点了,Vue提供了ref来选择节点(List.vue):
然后使用侦听器来监听letter的变化:
watch: { letter () { if (this.letter) { console.log(this.$refs[this.letter]) } } }
当你点击某个字母时,控制台输出:
而这个索引为0的div.area区域中包含了我们所需要的的城市列表信息:
既然这样就可以直接获取到对应的DOM节点,并将该节点传递给better-scroll,里面有一个scrollToElement方法,之后就能实现点击某个字母,城市列表页就会显示对应的城市信息:
watch: { letter () { if (this.letter) { const element = this.$refs[this.letter][0] this.scroll.scrollToElement(element) } } }
拇指滑动跳转
前面介绍的是点击右侧的字母就能跳转至对应的首字母城市,其实比这个更普通的 就是当你拇指按住字母表上下滑动时,左边List组件也会相应的上下跳动。
实现这个需要,首先我们要监听使用者的手指,记录时候开始点击字母列表(touchstart),什么时候开始滚动(touchmove),以及什么时候离开字母列表(touchend)等等,自然而然地想到使用事件监听。注意我们还需要定义一个标志状态(touchStatus),默认为flase,只有当你手指触摸的时候才会变成true,其实就是指定这三个函数的执行顺序:
{{key}}
现在我们需要知道当你拇指在滑动的时候,你的拇指停留在哪个字母上,这件事情其实是较为复杂的,可以提供一种思路仅供参考:先获取A字母距离顶部的高度,然后当你滑动的时候获取你当前字母距离顶部的高度,接着将后者减去前者得到一个差值,最后用这个差值除于每个字母的长度就能得到这是第几个字母。然后让该字母触发对应的事件即可,那么这样你需要新建一个数组用于存放字母,然后根据索引来获取字母,可以使用计算属性:
computed: { letters () { const letters = [] for (let i in this.cities) { letters.push(i) } return letters } },
上面的计算属性其实就是得到一个类似于['A','B','C','D']的数组。然后你城市字母表遍历的对象就不再是cities,而是letters了(那就不需要使用key了,直接遍历输出item就可以):
{{item}}
接下来继续编写handleTouchMove函数的内容,这个函数就是用于计算当你拇指在滑动的时候,你的拇指停留在哪个字母上。实现的逻辑是先获取A字母距离顶部的高度,然后当你滑动的时候获取你当前字母距离顶部的高度,接着将后者减去前者得到一个差值,最后用这个差值除于每个字母的长度就能得到这是第几个字母。
第一步在template中给DOM节点添加ref属性,用于获取某个DOM节点:ref="item";
第二步,修改handleTouchMove函数为:
handleTouchMove () { if (this.touchStatus) { const startY = this.$refs['A'][0].offsetTop console.log(startY) } },
通过测试发现当你拇指在滑动的时候,控制台始终输出61,这个61就是A字母距离页面顶部(注意是蓝色区域底部)的高度:
然后就可以获取每个字母距离整个页面的高度,里面有一个最小的值就是拇指距离整个页面最小的高度:
如果你想要获取某个字母距离蓝色区域底部的高度,可以将其高度减去蓝色区域底部的高度79(43(Header组件高度)+36(Search组件告高度)=79),然后除以20(每个字母高度)并向下取整就能得到每个字母的索引:
handleTouchMove (e) { if (this.touchStatus) { const startY = this.$refs['A'][0].offsetTop const touchY = e.touches[0].clientY - 79 const index = Math.floor((touchY - startY) / 20) console.log(index) } },
测试发现拇指移动到字母M处,右侧显示正常:
最后使用子组件Alphabet使用发布订阅模式向City组件传递数据:
handleTouchMove (e) { if (this.touchStatus) { const startY = this.$refs['A'][0].offsetTop const touchY = e.touches[0].clientY - 79 const index = Math.floor((touchY - startY) / 20) if (index >= 0 && index < this.letters.length) { this.$emit('change', this.letters[index]) } } },
测试发现功能显示正常。
城市列表页新能优化
接下来就是对上面的代码进行优化,因为startY是一个定值,而按照目前写的代码则是每次都需要去执行,这会造成性能低下。可以在data中return一个startY(初始值为0),然后定义一个生命周期函数update,只有页面的数据被更新同时页面完成了渲染时,该方法才会被执行:
data () { return { touchStatus: false, startY: 0 } }, updated () { this.startY = this.$refs['A'][0].offsetTop },handleTouchMove (e) { if (this.touchStatus) { 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]) } } },
我们知道页面一开始的时候cities变量是空值,也就是Alphabet组件内不会显示任何信息,然后通过ajax获取到数据时Alphabet组才会被重新渲染,之后会触发生命周期函数updated,这时候开始计算蓝色区域底部与字母A标签的距离。
还有一个优化就是函数节流。函数节流就是限制一个函数在一定时间内只能执行一次,这里就是当你拇指在字母表中上下移动时,touchmove函数执行的频率非常高会造成性能低下,因此可以借助于函数节流来优化该代码。具体的函数节流介绍可以参看这篇文章JS进阶篇1---函数节流(throttle),此处采用计时器规定在一定时间内函数才允许执行。打开Alphabet.vue文件,先return一个timer对象,初始值是null,其次修改handleTouchMove函数代码为:
handleTouchMove (e) { if (this.touchStatus) { if (this.timer) { clearTimeout(this.timer) } this.timer = setInterval(() => { 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) } },
原理非常简单,先判断timer计时器对象是否存在,存在就清空该计时器避免缓存,否则就定义一个计时器并设置计时器时间为16毫秒,即每16毫秒才计算一次,这样可避免不必要的计算工作。
搜索功能实现
在gitee的分支栏点击新建分支city-search-logic,然后记得将本地master分支切换到city-search-logic分支,接下来开始进行搜索框的业务逻辑开发。
打开city文件夹中的Search.vue组件,修改其中的template代码为:
123
这里面其实就是新增了一个搜索结果展示的区域,也就是.search-content类所占区域,接着在style标签中新增.search-content类所对应的样式(注意它应该和.search类是平级关系):
.search-content z-index: 1 overflow: hidden position: absolute top: 1.58rem left: 0 right: 0 bottom: 0 background: green
然后需要实现搜索信息与结果展示区域的联动,就需要使用到数据的双向绑定:
最后结果肯定是显示城市,那么需要从父组件City中接收cities,修改City.vue组件中的template代码为:
....
然后在子组件Search.vue中通过props来接收cities:
props: { cities: Object },
然后我们在子组件Search.vue中return一个数组list(该数组用于存储搜索结果)和计时器timer(函数节流使用):
data () { return { keyword: '', list: [], timer: null } }
接着我们定义一个侦听器用于监听keyword的变化:
watch: { keyword () { if (this.timer) { clearTimeout(this.timer) } this.timer = setInterval(() => { // 书写在cities对象中查找某个元素的逻辑 const result = [] for (let i in this.cities) { // i就是字母A,B...,而this.city[i]就是数组,value就是数组中的每一项 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) } }
在city.json文件中,我们定义的cities结构为:
"cities": { "A": [{ "id": 56, "spell": "aba", "name": "阿坝" }, { "id": 57, "spell": "akesu", "name": "阿克苏" }...
也就是cities本身是一个对象,里面又包含了一个数组作为值,而数组中包含的则是一个个对象。注意if语句中的判断条件为value.spell.indexOf(this.keyword) > -1 ||value.name.indexOf(this.keyword) > -1`也就是通过拼音或中文都可以查找是否能找到匹配的数据。
接下来就是对搜索结果的布局进行优化,给搜索结果添加一个.search-item类和一个边框类.border-bottom:
{{ item.name }}
然后给.search-item类添加样式,并修改.search-content类的背景颜色为#eee:
.search-content z-index: 1 overflow: hidden position: absolute top: 1.58rem left: 0 right: 0 bottom: 0 background: #eee .search-item line-height: .62rem padding-left: .2rem background: #fff color: #666
测试发现页面显示正常,但是搜索结果是无法滚动的,此时可以借助于bertter-scroll来实现。
第一步获取到search-content类节点DOM:
{{ item.name }}
第二步导入Bscroll类及创建该对象:
只需这两步就完成了页面滚动的效果。还有一个问题就是当你输入完并清空搜索框的时候,搜索结果依旧还是存在:
其实只需要当你的keyword为空的时候,你将这个list设置为[]即可,在侦听器中添加实现上述功能的逻辑:
watch: { keyword () { if (this.timer) { clearTimeout(this.timer) } if (!this.keyword) { this.list = [] return } ... }
这样就解决了这个问题。新的问题又来了就是当你输入一串非常长的字母或者说是输入的关键词不能匹配任何城市时,前面我们是什么也不显示,其实这个是有一点问题的,我们最好是在页面上添加诸如“找不到对应的城市”等信息。最简单的方式就是使用v-show来进行显示:
{{ item.name }}
没有找到匹配的城市
这样我们就实现了当list的长度为0的时候才显示“没有找到匹配的城市”字眼,但是这样会造成一个问题,就是刚开始你不输入关键词的时候也会出现这个“没有找到匹配的城市”字眼,把热门城市和城市列表给遮住了。这个问题还是可以通过v-show来解决(只不过这次将v-show 标签加到search-content类上):
{{ item.name }}
没有找到匹配的城市
这样就完美地解决了上述问题。前面介绍过html中最好不需要包含计算逻辑(在“没有找到匹配的城市”标签上使用了取反运算):
没有找到匹配的城市
因此需要将这个取反的逻辑使用计算属性来代替:
computed: { hasNoData () { return !this.list.length } },
自然而然就需要修改上面的template中的取反运算代码为:
没有找到匹配的城市
这样就完成了搜索框的业务逻辑。最后就是将我们的代码上传到city-search-logic分支,并且将其与master分支进行合并,相应的步骤如下:
git statusgit add .git commit -m 'search logic finish'git pushgit checkout mastergit merge city-search-logicgit push