Vue.js第7课-项目实战-城市列表开发(part03)
八、搜索功能实现
打开 city 目录中 search.vue 这个组件,新建一个列表的区块 search-content,让这个区块展示搜索的内容,此时他和 input 框是同级,所以需要在这两个元素外再包裹一层,然后给 search-content 一个样式布局,让他绝对定位到搜索框下,这是为了之后调用 better-scroll 这个插件。
search.vue
<template>
<div class="search">
<div>
<input class="ipt" type="text" placeholder="输入城市名或拼音" v-model="keyword">
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="border-bottom">北京</li>
<li class="border-bottom">北京</li>
</ul>
</div>
</div>
</div>
</template>
完成基本的样式布局后,我们来实现一些逻辑,首先要把 input 框里的内容和我的数据做一个绑定,所以在 data 中返回一个 keyword,默认为空,通过 v-model 实现一个数据的双向绑定。然后 search.vue 这个组件还要接收 City.vue 传过来的 cities 数据,所以在 City.vue 中的模板 city-search 里通过属性的方式传一个 cities,接着回到 search.vue 中,通过 props 接收父组件传过来的 cities。再到 data 中返回一个 list 数组,这个 list 数组用来存放和输入相匹配的结果项,默认为空。
search.vue
<template>
<div class="search">
<div>
<input class="ipt" type="text" placeholder="输入城市名或拼音" v-model="keyword">
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="border-bottom" v-for="item of list" :key="item.id" >{{item.name}}</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CitySearch",
props: {
cities: Object
},
data() {
return {
keyword: "",
list: []
};
},
};
</script>
city.vue
<city-search :cities="cities"></city-search>
data() {
return {
cities: {}
};
},
写一个侦听器 watch,在里边监听 keyword 的改变,这里还是使用节流的方式来实现,先在 data 中定义一个 timer 定时器,默认值为 null,然后在监听 keyword 的方法中,判断,当 timer 为 null 时,清除这个定时器。下面写这个定时器的方法,当延时 100ms 的时候,箭头函数会被执行。先定义一个 result 变量,默认为空数组,然后通过 for 循环出 cities 中的每一项,再将 cities 中的每一项通过 forEach 遍历出来。forEach 中传一个箭头函数,这个函数接收一个 value,可以打印 value 看一下,他里面有一个 name 和 spell 值,我们可以通过判断这两个值中是否有输入的 keyeord 匹配的值,也就是如果从 name 和 spell 中能搜索到关键词,我们就把这一项添加到 result 中,然后让 data 中的 list 等于 result。
search.vue
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);
}
},
知识点补充:上边我们先用 for in 循环了 this.cities,这是因为 this.cities 他是一个对象,所以用 for in 来循环,for in 循环出的是 key,如果想取到 key 对应的的值,就要在后面跟上索引。然后再将循环出的值通过 forEach() 来循环,forEach() 来遍历数组,里面可以传一个方法。
得到和输入相匹配的数据后,就可以通过 v-for 将他们渲染出来了。这个时候,基本的业务逻辑就编写完成了,打开页面,输入城市的中文或字母,下面就会展示出匹配的城市:
接下来子再通过 better-scrool 来实现一个下拉效果,首先引入 better-scroll,然后在钩子函数 mounted 中创建一个 better-scroll 实例,通过 ref 将 search-content 元素传入到这个实例中,这样就实现了一个下拉的效果。
search.vue
<div class="search-content" ref="search"></div>
import BScroll from "better-scroll";
mounted() {
this.scroll = new BScroll(this.$refs.search);
},
最后再做一些细节上的处理,首先在输入框输入内容后,再清除掉,下边之前匹配出的城市依然存在,这个时候,只需要判断一下,当 keyword 值为空的时候,就让这个 list 为空,下边的列表也就不显示了。
当输入一个不匹配的字符串,此时下面是什么都不显示的,我们可以通过 v-show 做一个没有匹配项的提示,在 li 标签下添加一份 li 标签,通过 v-show 判断一下,当 list 中没有数据时,显示这个元素。可以直接将 js 的逻辑的运算 !list.length 放到 v-show 中,但是建议还是不要在指令中添加逻辑运算,我们可以使用 computed 计算属性,设置这个值,模板里面尽量保持简洁的语法。
现在,页面上始终都会显示这个查询结果元素,他把下面的元素都覆盖掉了,来解决一下这个问题。我们可以让 search-content 这个元素的显示与否通过一个变量来决定,v-show="keyword",意思是,当有这个 keyword 的时候,才显示 search_content 查询结果的元素。
附上最终的 search.vue 的代码:
search.vue
<template>
<div class="search">
<div>
<input class="ipt" type="text" placeholder="输入城市名或拼音" v-model="keyword">
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="border-bottom" v-for="item of list" :key="item.id">{{item.name}}</li>
<li class="border-bottom" v-show="hasNoData">没有找到匹配数据</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import BScroll from "better-scroll";
export default {
name: "CitySearch",
props: {
cities: Object
},
data() {
return {
keyword: "",
list: [],
timer: null
};
},
computed: {
hasNoData() {
return !this.list.length;
}
},
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);
}
},
mounted() {
this.scroll = new BScroll(this.$refs.search);
}
};
</script>
<style lang="stylus" scoped>
@import '~style/varibles';
.search {
background-color: $bgColor;
overflow: hidden;
padding: 0 0.2rem;
height: 0.9rem;
line-height: 0.9rem;
background-color: #f5f5f5;
.ipt {
width: 100%;
background-color: #fff;
text-align: center;
color: #666;
height: 0.5rem;
line-height: 0.5rem;
box-sizing: border-box;
padding: 0 0.2rem;
}
.search_content {
overflow: hidden;
position: absolute;
z-index: 1;
top: 1.62rem;
left: 0;
right: 0;
bottom: 0;
width: 100%;
background-color: #f5f5f5;
padding: 0 0.2rem;
box-sizing: border-box;
}
}
</style>
以上就完成了城市选择页的搜索内容,最后记得提交代码。
九、使用 Vuex 实现数据共享
这一章,我们使用 Vuex 来实现首页和城市页的数据共享。先创建一个分支 city-vuex 并切换到这个分支,进行开发。
1、首页右上角“当前城市”和“城市页”当前城市的共享
先来看一下项目中现有组件的一个目录结构:
我们现在要实现的的是 City.vue 和 Home.vue 组件之间的通信,之前讲过我们可以通过 bus 总线的方式来实现非父子组件的通信,但是这种会比较麻烦,我们换一种方式,使用 Vue 官方推荐的数据框架 Vuex,下图是官网上的一个 Vuex 的图解:
Vuex 可以进行多个页面复杂的传值,接下来我们看一下如何在项目中使用 Vuex。首先通过 npm install 安装 vuex:
然后在项目中引入 vuex,之前我们在安装插件的时候,都是在 src/main.js 中引入并通过 Vue.use() 来使用的,但是因为 vuex 处理的数据可能会比较复杂,所以我们在 src 目录下新建一个 store 目录,并在里面新建一个 index.js,在这里去引入 vue 和 vuex 并使用:
src/stroe/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
city: "北京"
}
})
state 这个对象里边存放的就是公用的数据,他对应的就是图解中的 State,组件都可以去使用这里的数据。
接着打开 src/main.js,这个时候就可以通过 import 直接引入 src/stroe/index.js 了,然后在下面的 vue 实例中添加 stroe 这个属性就可以了。此时运行一下项目,如果没有报错,就说明引用成功。
src/main.js
import store from "./store"
new Vue({
el: "#app",
router,
store,
components: { App },
template: "<App/>"
});
打开 home/Home.vue,之前首页 header 部分右侧的城市数据是父组件通过 ajax 请求到 static/mock/index.json 中的 city 数据,然后通过属性的方式把这个数据传给子组件 header.vue,子组件 header.vue 再通过 props 接收,最后渲染到页面上。现在我们不用这种方式获取数据并渲染了,把 Home.vue 中 home-header、data、getHomeInfoSucc() 中的 city 都去掉,然后打开 home/header.vue,修改一下之前的插值表达式,将之前 {{this.city}} 修改为:
{{this.$store.state.city}}
因为我们在 stroe/index.js 下将数据存到了 State 中,所以直接通过state.city 就能获取到 city 的数据。这个时候打开页面,就可以看到“北京”正常渲染到头部右侧了。我们把城市列表页头部中的“当前城市”之前写死的“北京”也换成这种方式来渲染。
2、改变 State,更新当前城市
下面我们再实现一个功能,就是点击城市列表页下面的“热门城市”,他会显示到当前城市中。也就是我们要改变那张图中的 State,看一下图中绿色虚线框圈出的内容,首先得调用 Actions,然后再调用 Mutations,调用 Actions 的时候,是需要 Dispatch 方法的,调用 Mutations 的时候,是需要 Commit 方法的,下面我们走一下这个流程:
在 city/list.vue 中我们给每一个热门城市绑定一个点击事件 handleCityClick,并把 item.name 传进来,然后将这个方法写在 methods 中,他接收一个 city,这个 city 就是被点击的城市。
现在我们已经获取到被点击的城市名了,接下来,在这个组件里,我要调用 vuex 中的 Actions,看那张图,有一个 Dispatch 的方法,我们在调用 Actions 的时候,一定要调用 Dispatch 这个方法,所以在这个 handleCityClick 方法中这么写:当改变 city 的时候,通过 Dispath 去派发一个 changeCity 的一个 Actions 的行动,将 city 作为第二个参数传过来。
methods :{
handleCityClick(city){
this.$store.dispatch(changeCity,city);
}
}
Dispatch 的意思是派发一个名字是 changeCity 的 Actions 行动,然后把 city 传过去。当然这么写是没有效果的,因为在创建 store 的时候只有一个 city,并没有任何的 Actions,所以打开 store/index.js,写一个 actions 对象,他这里需要有一个和 dispatch 中名字一样的 Actions,也就是 changeCity,这个方法接收两个参数,第一个参数是一个上下文 ctx,第二个也就是传递过来的数据,就是那个 city。当你点击城市的时候,actions 会被派发,store/index.js 这里正好对应的 Actions 接收到传递过来的 city。
store/index.js
actions:{
changeCity (ctx,city){
}
},
此时 Actions 中已经接收到传递过来的城市,他需要调用 Mutations 来改变 State(公用的数据),看图解,Mutations 是需要 Commit 来提交的,在 city/list.vue 下的 methods 中再加一个 Commit 提交:
city/list.vue
methods :{
handleCityClick(city){
this.$store.dispatch("changeCity",city); // 派发
this.$store.commit("changeCity",city); // 提交
}
}
他要把这个 changeCity 和 city 提交给 Mutations,所以和在 store/index.js 中创建 actions 一样,接下来要创建一个 Mutations,这里也可以写一个 changeCity,每一个 mutations 对应的参数也会有两个,第一个是 state,第二个是外部传过来的 city。
store/index.js
mutations:{
changeCity(state,city){
}
}
我想 Actions 去调用 Mutations,那如何去调用呢?看一下图,Actions 如果想调用 Mutations,必须执行一个方法 Commit,那就在 actions 中执行一个下这个方法,之所以 Actions 中第一个参数是 ctx,作用就是他可以借助 ctx 帮助我们拿到 Commit 这个方法(在 list.vue 中通过 commit 向 Mutations 提交了方法,就需要 index.js 中的 Actions 通过 commit 接收这个方法,之后去 Mutations 中写这个接收到的方法的逻辑代码),然后去执行 changeCity 这个 Mutations,传过去一个内容是 city。然后在 Mutations 中做一个事情,State 指的是所有公用的数据,让这个数据等于 city 就可以了。
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state : {
city:"北京"
},
actions:{
changeCity (ctx,city){
ctx.commit("changeCity",city);
}
},
mutations:{
changeCity(state,city){
state.city = city;
}
}
})
此时,打开页面,点击热门城市,当前城市就会变换了。
上面这一过程,就是图解中 State → Actions → Mutations 这一过程,其实我们也可以省去 Actions 这一步,直接 State → Mutations,接下来我们把 store/index.js 中 actions 部分注释掉,然后去 city/list.vue 中,把使用 dispath 给 actions 派发 changeCity 和 city 去掉,直接通过 commit 方法调用 mutations 就可以了。
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state : {
city:"北京"
},
// actions:{
// changeCity (ctx,city){
// ctx.commit("changeCity",city);
// }
// },
mutations:{
changeCity(state,city){
state.city = city;
}
}
})
city/list.vue
methods :{
handleCityClick(city){
// this.$store.dispatch("changeCity",city); // 派发
this.$store.commit("changeCity",city); // 提交
}
}
回到页面上,可以看到逻辑是没有任何问题的。点击“热门城市”,“当前城市”就会改变。
还有两处也要实现一下这样的效果,就是热门城市下的城市列表和搜索结果中的城市列表。打开 city/list.vue ,给城市列表也加一个点击事件,需要注意的是,这里传的是 city.name,而不是 item.name,注意循环的变量名。
city/list.vue
<div class="alp_li border-bottom" v-for="city of item" :key="item.id" @click="handleCityClick(city.name)">{{city.name}}</div>
还有 city/search.vue,这个组件里没有 handleCityClick 这个方法,所以要在 methods 中添加一下这个方法,然后在城市列表传入点击事件。