Vue.js

Vue.js第7课-项目实战-城市列表开发(part03)

2019-05-31  本文已影响69人  e20a12f8855d

八、搜索功能实现

打开 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 中添加一下这个方法,然后在城市列表传入点击事件。


长得好看的都会关注我的 o(≧v≦)o~~

上一篇下一篇

猜你喜欢

热点阅读