Vue.js学习记录-13-Vue去哪儿网项目实战:城市列表页开发-Search + List
-
Search:城市选择信息输入检索 (增量式)
功能点2:用户可以在搜索栏中可输入信息进行城市信息的检索,检索结果以列表形式展现,选定城市后会进行首页的路由跳转。
功能点2分析:用户故事角度
作为用户,我想在搜索栏中输入信息后会有结果信息以列表形式展现,并且列表内的内容选择后可以进行页面的跳转,依次来实现城市信息的变更。
具体实现:
组件data初始化:
name: 'CitySearch', props: { cities: Object 父组件City传递的城市信息对象 }, data() { return { keyword: '', 城市名称关键字 list: [], 存放城市名称信息搜索结果 timer: null 定时器变量 } }
父组件通过属性进行数据传递:
<city-search :cities="cities"></city-search>
细节1:搜索栏需要对关键字属性进行双向绑定,并实现监听,当搜索栏中存在关键字会进行下拉列表的展示。
<template>:v-model实现属性的双向绑定。
<div class="search"> <input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音"> </div>
<script>:对keyword进行响应式侦听,通过定时器来实现在指定时间内进行指定操作。
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) => { // 针对item的spell、name进行关键字是否为其子串进行判断 if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) { result.push(value) } }) // 将结果赋值给list数组 this.list = result } }, 100) } },
细节2:下拉列表的城市信息展示分两种:存在城市信息已列表形式展示,并可以上下滑动/无城市信息显示提示信息。
-
better-scroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件
-
引入better-scroll
import BScroll from 'better-scroll'
-
在组件实例初始化渲染时,利用钩子函数进行better-scroll插件实例初始化
mounted() { // 钩子函数实例化better-scroll this.scroll = new BScroll(this.$refs.search) }
-
组件实例依赖绑定DOM元素,采用ref进行DOM元素引用
<div class="search-content" ref="search" v-show="keyword">
此外该DOM元素的展示与否,依赖于keyword的是否为空
-
城市信息列表的布局:<ul>
<div class="search-content" ref="search" v-show="keyword"> <ul> <li class="search-item border-bottom"> // 城市信息列表 </li> <li class="search-item border-bottom" v-show="hasNoData"> 没有找到匹配数据 </li> </ul>
-
-
无城市信息显示提示信息
对上述城市信息列表布局中的第二个<li>标签进行分析:hasNoData控制此标签元素的展示与否
计算属性hasNoData的构建:在模板标签内部不宜出现Js表达式,计算属性实质是对结果数组list长度进行判断。
computed: { hasNoData() { return !this.list.length } },
细节3:对下拉列表的信息进行点击后,携带城市信息实现页面跳转,这里采用Vuex进行数据共享。
对上述城市信息列表布局中的第一个<li>标签进行分析:
<li v-for="item of list" :key="item.id" @click="handleCityClick(item.name)" class="search-item border-bottom"> {{item.name}} </li>
上述代码中老生长谈的v-for,key的设置,以及插值表达式展示数据value。我们在这里就不提了,着重看一下这个点击事件:handleCityClick
在这个handleCityClick方法中,将城市名称item.name作为参数传入,结合vuex实现数据共享,也就是我们上篇文章中未提到的组件通过dispatch发起数据调度。
我们详细的看一下这个handleCityClick方法: 下文是常规的vuex调用流程,并未使用vuex的map映射概念
handleCityClick(city) { // 组件通过store提供的dispatch方法,向store触发事件并携带参数 this.$store.dispatch('changeCity', city) // 组件也可以跳过dispatch方法,直接使用store中的commit方法进行事件的触发 // this.$store.commit('changeCity', city) // 由于采用了keep-alive进行了DOM内容存储,提升体验度这里完成上述业务逻辑后,将搜索区域下拉框关键字置为空,从而控制下拉列表的展示 this.keyword = '' // 点击更改城市信息之后,实现页面跳转 this.$router.push('/') },
-
补充说明:
-
常规流程是 vue component 向 Actions 发起 dispatch ,Actions 向 Mutations 进行 commit,但是vue component 也可以直接向 Mutations 进行 commit。
-
关于keep-alive,后续会提到,这里是将DOM内容暂存内存中,但是这样会使得搜索框的下拉列表再次进入City界面时仍保持原状,影响体验,于是这里博主添加了this.keyword = ’ ’ ,目的是通过关键字置空从而控制下拉列表的展示。
未添加前页面下拉列表演示:
-
方法内部实现路由跳转:实例.$router.push(‘url’) 即可。
-
下述组件的说明中,会采用vuex的高级映射特性,简化代码的书写。
-
List:城市列表展示 (增量式)
组件data初始化:
name: 'CityList', props: { cities: Object, hot: Array, letter: String },
父组件通过属性进行数据传递:
<city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>
功能点5:当前城市信息展示区域的选定城市信息展示
这里我们接着上篇文章中完成一半的功能点5继续往下说明,在这个List组件中,共分为三部分区域:当前城市、热门城市、字母城市列表。
这三部分的样式设计是一样的,只是区域下的逻辑各有不同。那么针对功能点5进行结尾说明:
通过vuex实现数据共享,数据源信息存储使用在store模块中的state中,在获取数据方面上我们可以通过vue的高级特性映射拿到mapState。
import { mapState, mapMutations } from 'vuex'
这里引入mapMutations目的是简化数据交互操作,功能点3上会使用该映射变量。
通过计算属性进行state数据源信息获取:
computed: { ...mapState({ currentCity: 'city' }) },
这里的使用方式与上篇文章有所不同,关于变量的命名可以类比ES6语法中,key-value形式中key和value相同时可以简写为一个。
模板中使用:
<div class="button">{{this.currentCity}}</div>
功能点3:用户可以在热门城市、字母城市列表中选择城市信息,选定城市后会进行首页的路由跳转。
整个List列表采用了better-scroll的滚动场景布局,依旧是首先引入better-scroll,通过ref绑定DOM元素实例化BScroll。
// 引入better-scroll插件 import BScroll from 'better-scroll' // ref绑定DOM元素 <div class="list" ref="wrapper"> // 钩子函数实例化BScroll对象 // 采用生命周期钩子函数进行DOM引用获取 mounted() { // 创建实例 this.scroll = new BScroll(this.$refs.wrapper) },
该功能点涉及的区域为:热门城市、字母城市列表
请求返回的数据中分别对应:hot、cities
<template>:
<div class="area"> <div class="title border-topbottom">热门城市</div> <div class="button-list"> <div class="button-wrapper" v-for="item of hot" :key="item.id" @click="handleCityClick(item.name)"> <div class="button">{{item.name}}</div> </div> </div> </div>
热门城市区域布局概况:循环遍历接收到的hot数组,利用插值表达式进行数据展示,此外绑定了点击事件handleCityClick。
<!-- 采用ref进行区块滚动DOM结构的标识 --> <div class="area" v-for="(item, key) of cities" :key="key" :ref="key"> <div class="title border-topbottom">{{key}}</div> <div class="item-list"> <div class="city border-bottom" v-for="innerItem of item" :key="innerItem.id" @click="handleCityClick(innerItem.name)"> {{innerItem.name}} </div> </div> </div>
字母城市列表布局概况:由于接受的是Object对象,JSON数据采用了[A]-[A1,A2,…]该类型的包装,该区域首先循环遍历外侧数据作为字母表展示,接着对获取到的item进行遍历,遍历出字母列表下的城市列表内容。此外这里也绑定了点击事件handleCityClick。
注:这里的ref="key"绑定了该DOM,是为了实现字母表导航条的随动效果而添加的,随动效果对于该区域来讲是被动的,我将会在Alphabet组件中进行详细说明。
<script>:
handleCityClick事件:携带城市信息,用户点击后,通过vuex实现城市信息的变更。
上文已经提到了:引入了mapMutations变量进行简化数据交互操作。
methods: { handleCityClick(city) { // 组件通过store提供的dispatch方法,向store触发事件并携带参数 // this.$store.dispatch('changeCity', city) // 组件也可以跳过dispatch方法,直接使用store中的commit方法进行事件的触发 // this.$store.commit('changeCity', city) // 使用mapMutations进行方法映射 this.changeCity(city) // 点击更改城市信息之后,实现页面跳转 this.$router.push('/') }, // 映射名称为changeCity的mutation到组件方法中 ...mapMutations(['changeCity']) },