(16)城市选择页开发——⑥ Vuex 实现组件数据共享 | Vue.js 项目实战: 移动端“旅游网站”开发

258 阅读11分钟
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 需求

在城市选择页点击任意城市名后,首页头部的城市名跟着发生改变;当选择了城市后,跳转回首页:

视频01.gif

2 首页与城市选择页数据共享

需求分析:

点击城市选择页的“城市”后,首页头部的“城市”跟着变化,这就要求首页与城市选择页之间要进行通信,传递数据。且在这两个组件之间,没有一个公用的父级组件,它们之间无法通过一个父级组件进行数据的中转。

之前《深入理解 Vue 组件——⑤ 非父子组件间的传值》中提到过非父子组件间通信的两种方案: Vuex 和 发布订阅模式。

发布订阅模式我们已经学习过了,但它用在现在的“qdywxs-travel 项目”中依然比较麻烦。所以,这里我们采用 Vuex 的方案。

2.1 Vuex 是什么

Vuex 是 Vue 官方提供的一个数据框架。

在 Vue 的大型项目开发中,Vue 只能承担视图层的主要内容。当我们涉及到大量数据之间的传递时,往往需要一个数据框架——Vuex 来进行辅助。

Vue 官网的“生态系统”中,可以找到 Vuex:

在“Vuex 是什么”这个章节里,可以找到一张关于 Vuex 的图。只要弄明白这张图,那么我们也就学会使用 Vuex 了:

❓如何来理解这张图呢?

在一个项目中,如果多个页面(或多个组件)之间进行传值非常困难的时候,我们可能会想:如果可以把所有公共的数据,放在一个公共的空间来存储。当某一个组件改变了公共的数据后,其他的组件也能感知到。这样,不就能很方便的实现我们的需求了吗?

Vuex 的设计理念就是这样的:整个 Vuex 绿色虚线的部分,就是公用数据的存储区域,这个区域可以理解为一个仓库

这个仓库由几个部分共同组成的:

  • State 存放所有的公用数据,当组件想用某个公用的数据,直接调用 State;
  • 如果某个组件想要改变公用数据,组件不能直接更改数据,必须走一个“流程”:
    • 组件先调用 Actions
    • Actions 再去调用 Mutations
    • Mutations 再一个一个同步的对 State 进行修改。

如果是异步的操作,或者比较复杂的、批量的同步操作,放在 Actions 里。有时,我们也可以越过 Actions,让组件直接去调用 Mutations 修改 State 中的数据。

但,只有通过 Mutations,我们才能改变公用数据的值。

❗️注意:组件调用 Actions 时,是通过一个 Dispatch 方法;组件或 Actions 调用 Mutations 时,是通过一个 Commit 方法。

Vuex 的内容,其实就是这样一个单向数据改变流程。在使用 Vuex 时,我们还可以使用 Devtools 对代码进行调试(❗️上图建议保存,在后面的内容中,对照上图跟着文章来理解)。

2.2 使用 Vuex

要使用 Vuex,我们需要在项目中进行安装。

1️⃣打开终端,在项目目录下运行 npm install vuex --save

2️⃣安装好 Vuex 后,我们对照图片内容来使用:

2️⃣-①:在 src 目录下 新建一个 store 文件夹,并在 store 文件夹中创建一个 index.js 文件;

import Vue from 'vue' // 2️⃣-②:首先引入 Vue;

import Vuex from 'vuex' // 2️⃣-③:再引入 Vuex;

Vue.use(Vuex) /*
              2️⃣-④:通过 Vue.use 使用 Vuex(因为 Vuex 也是一个插件,Vue 之中用 Vue.use 
              来使用插件);
               */

export default new Vuex.Store({ /*
                                2️⃣-⑦:最后,导出一个通过 Vuex 创建的仓库 Store。
                                ❗️创建仓库是通过 new Vuex.Store() 
                                 */
  
  state: { // 2️⃣-⑤:根据 Vuex 图来看,仓库中有一个 state 的内容,里面存放所有的公用数据;
    
    city: '北京' /*
                2️⃣-⑥:而对于首页和城市选择页来说,公用的数据就是 city,这里我们让默认城市为“北京”;
                 */
  }
})

2️⃣-⑧:打开 main.js ,在项目中引入 Store;

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import fastClick from 'fastclick'
import VueAwesomeSwiper from 'vue-awesome-swiper'

import store from './store' /*
														2️⃣-⑨:引入当前目录下 store 中的 index.js(这里可以
                            省略 store后面的 /index.js,Vue 会自动去找);
                             */
import 'styles/reset.css'
import 'styles/border.css'
import 'styles/iconfont.css'
import 'swiper/dist/css/swiper.css'

Vue.config.productionTip = false
fastClick.attach(document.body)
Vue.use(VueAwesomeSwiper)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,

  store, // 2️⃣-⑩:在创建根 Vue 实例时,把 store 传入进去。

  components: { App },
  template: '<App/>'
})

保存后,返回页面查看,控制台无报错,项目中的功能正常即正确使用了 Vuex:

视频02.gif

❓组件如何使用公用数据呢?

答:“当组件想用某个公用的数据,直接调用 State”。而首先使用到公用数据的组件,是首页的 Header.vue 的“城市”。

3️⃣打开 pages 下 home 中的 Home.vue

<template>
  <div>
    <home-header></home-header> <!-- 3️⃣-①:删除首页 Header 组件接收的数据 city
																(因为我们将使用 State 中的公用数据,不再需要父组件传递
																过来的数据了); -->
    <!-- <home-header :city="city"></home-header> -->

    <home-swiper :list="swiperList"></home-swiper>
    <home-icons :list="iconList"></home-icons>
    <home-recommend :list="recommendList"></home-recommend>
    <home-weekend :list="weekendList"></home-weekend>
  </div>
</template>

<script>
import HomeHeader from './components/Header'
import HomeSwiper from './components/Swiper'
import HomeIcons from './components/Icons'
import HomeRecommend from './components/Recommend'
import HomeWeekend from './components/Weekend'
import axios from 'axios'

export default {
  name: 'Home',
  components: {
    HomeHeader,
    HomeSwiper,
    HomeIcons,
    HomeRecommend,
    HomeWeekend
  },
  data () {
    return {
      // city: '', // 3️⃣-②:删除 data 中的数据 city;
      
      swiperList: [],
      iconList: [],
      recommendList: [],
      weekendList: []
    }
  },
  methods: {
    getHomeInfo () {
      axios.get('/api/index.json')
        .then(this.getHomeInfoSucc)
    },
    getHomeInfoSucc (res) {
      res = res.data
      if (res.ret && res.data) {
        const data = res.data
        
        // this.city = data.city // 3️⃣-③:删除后端请求到的数据 city;
        
        this.swiperList = data.swiperList
        this.iconList = data.iconList
        this.recommendList = data.recommendList
        this.weekendList = data.weekendList
      }
    }
  },
  mounted () {
    this.getHomeInfo()
  }
}
</script>

<style>
</style>

3️⃣-④:打开 mock 下的 index.json 删除 data 中的 city

{
  "ret": true,
  "data": {
    // "city": "北京", // ❗️删除 data 中的 city。
    
    "swiperList": [{
        "id": "0001",
        "imgUrl": "https://qdywxs.github.io/travel-images/swiperList01.jpg"
      },{
        "id": "0002",
        "imgUrl": "https://qdywxs.github.io/travel-images/swiperList02.jpg"
      },{
        "id": "0003",
        "imgUrl": "https://qdywxs.github.io/travel-images/swiperList03.jpg"
      },{
        "id": "0004",
        "imgUrl": "https://qdywxs.github.io/travel-images/swiperList04.jpg"
      }],
    "iconList": [{
        "id": "0001",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList01.png",
        "desc": "景点门票"
      }, {
        "id": "0002",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList02.png",
        "desc": "滑雪季"
      }, {
        "id": "0003",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList03.png",
        "desc": "泡温泉"
      }, {
        "id": "0004",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList04.png",
        "desc": "动植园"
      }, {
        "id": "0005",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList05.png",
        "desc": "游乐园"
      }, {
        "id": "0006",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList06.png",
        "desc": "必游榜单"
      }, {
        "id": "0007",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList07.png",
        "desc": "演出"
      }, {
        "id": "0008",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList08.png",
        "desc": "城市观光"
      }, {
        "id": "0009",
        "imgUrl": "https://qdywxs.github.io/travel-images/iconList09.png",
        "desc": "一日游"
      }],
    "recommendList": [{
        "id": "0001",
        "imgUrl": "https://qdywxs.github.io/travel-images/recommendList01.jpg",
        "title": "故宫",
        "desc": "东方宫殿建筑代表,世界宫殿建筑典范"
      }, {
        "id": "0002",
        "imgUrl": "https://qdywxs.github.io/travel-images/recommendList02.jpg",
        "title": "南山滑雪场",
        "desc": "北京专业级滑雪圣地"
      }, {
        "id": "0003",
        "imgUrl": "https://qdywxs.github.io/travel-images/recommendList03.jpg",
        "title": "天安门广场",
        "desc": "我爱北京天安门,天安门上太阳升"
      }, {
        "id": "0004",
        "imgUrl": "https://qdywxs.github.io/travel-images/recommendList04.jpg",
        "title": "水立方",
        "desc": "中国的荣耀,阳光下的晶莹水滴"
      }, {
        "id": "0005",
        "imgUrl": "https://qdywxs.github.io/travel-images/recommendList05.jpg",
        "title": "温都水城养生馆",
        "desc": "各种亚热带植物掩映其间,仿佛置身热带雨林"
      }],
    "weekendList": [{
        "id": "0001",
        "imgUrl": "https://qdywxs.github.io/travel-images/weekendList01.jpg",
        "title": "北京温泉排行榜",
        "desc": "细数北京温泉,温暖你的冬天"
      }, {
        "id": "0002",
        "imgUrl": "https://qdywxs.github.io/travel-images/weekendList02.jpg",
        "title": "北京必游TOP10",
        "desc": "来北京必去的景点非这些地方莫属"
      }, {
        "id": "0003",
        "imgUrl": "https://qdywxs.github.io/travel-images/weekendList03.jpg",
        "title": "寻找北京的皇城范儿",
        "desc": "数百年的宫廷庙宇,至今依旧威严霸气"
      }, {
        "id": "0004",
        "imgUrl": "https://qdywxs.github.io/travel-images/weekendList04.jpg",
        "title": "学生最爱的博物馆",
        "desc": "周末干嘛?北京很多博物馆已经免费开放啦"
      }, {
        "id": "0005",
        "imgUrl": "https://qdywxs.github.io/travel-images/weekendList05.jpg",
        "title": "儿童剧场,孩子的乐园",
        "desc": "带宝贝观看演出,近距离体验艺术的无穷魅力"
      }]
  }
}

3️⃣-⑤:打开 home 中 components 下的 Header.vue

<template>
  <div class="header">
    <div class="header-left">
      <span class="iconfont back-icon">&#xe658;</span>
    </div>
    <div class="header-input">
      <span class="iconfont">&#xe63c;</span>
      输入城市/景点/游玩主题
    </div>
    <router-link to="/city">
      <div class="header-right">

        {{this.$store.state.city}} <!-- 3️⃣-⑦:通过 $store 调用 state 中的 city;
        													 (❗️$store 指的 就是我们创建的仓库 Store,而每个子组件
																	 能使用 $store,是因为创建根 Vue 实例时,把 store 传入
																	 进去了。) -->

        <span class="iconfont arrow-icon">&#xe65c;</span>
      </div>
    </router-link>
  </div>
</template>

<script>
export default {
  name: 'HomeHeader'

  /* 3️⃣-⑥:删除 props;
  props: {
    city: String
  }
   */
}
</script>

<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.header
  display: flex
  line-height: $headerHeight
  color: #fff
  background: $bgColor
  .header-left
    float: left
    width: .64rem
    .back-icon
      display: block
      text-align: center
      font-size: .56rem
  .header-input
    flex: 1
    margin-top: .12rem
    margin-left: .2rem
    padding-left: .12rem
    height: .64rem
    line-height: .64rem
    color: #ccc
    background: #fff
    border-radius: .1rem
  .header-right
    float: right
    width: 1.24rem
    text-align: center
    color: #fff
    .arrow-icon
      margin-left: -0.1rem
</style>

同时,城市选择列表页的“当前城市”,也应该与首页头部的城市名相同。

3️⃣-⑧:打开 city 中 components 下的 List.vue

<template>
  <div class="list" ref="wrapper">
    <div>
      <div class="area">
        <div class="title border-topbottom">当前城市</div>
        <div class="button-list">
          <div class="button-wrapper">
            <div class="button">
              {{this.$store.state.city}} <!-- 3️⃣-⑨:原本写死的 城市“北京”,
																				 现在从 Store 中获取数据 city; -->
            </div>
          </div>
        </div>
      </div>
      <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">
            <div class="button">{{item.name}}</div>
          </div>
        </div>
      </div>
      <div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
        <div class="title border-topbottom">{{key}}</div>
        <ul class="item-list">
          <li class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">
            {{innerItem.name}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.wrapper)
  },
  watch: {
    letter () {
      const element = this.$refs[this.letter][0]
      this.scroll.scrollToElement(element)
    }
  }
}
</script>

<style lang="stylus" scoped>
.border-topbottom
  &:before
    border-color: #ccc
  &:after
    border-color: #ccc
.border-bottom
  &:before
    border-color: #ccc
.list
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  .title
    padding-left: .2rem
    line-height: .54rem
    background: #eee
    color: #666
    font-size: .26rem
  .button-list
    overflow: hidden
    padding: .1rem .6rem .1rem .1rem
    .button-wrapper
      float: left
      width: 33.33%
      .button
        margin: .1rem
        padding: .1rem 0
        text-align: center
        border: .02rem solid #ccc
        border-radius: .06rem
  .item-list
    .item
      line-height: .76rem
      padding-left: .2rem
</style>

保存后,返回页面查看。当更改 Store 里公用数据中的 city 时,首页的“城市”和城市选择页的“当前城市”都会跟着发生变化:

视频03.gif

OK,首页成功获取到公用数据之后,下一步,就可以完成我们的需求了:当点击城市选择页的“城市”时,首页的“城市”跟着发生变化。即,city 中的子组件 List.vue 需要改变仓库中 State 的数据 city

4️⃣打开 city 下 components 中的 List.vue

<template>
  <div class="list" ref="wrapper">
    <div>
      <div class="area">
        <div class="title border-topbottom">当前城市</div>
        <div class="button-list">
          <div class="button-wrapper">
            <div class="button">
              {{this.$store.state.city}}
            </div>
          </div>
        </div>
      </div>
      <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)"
          > <!-- 4️⃣-①:给每个“城市”按钮都绑定一个点击事件,触发时调用 handleCityClick 方法,
						方法传入 item.name(即城市名); -->
            
            <div class="button">{{item.name}}</div>
          </div>
        </div>
      </div>
      <div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
        <div class="title border-topbottom">{{key}}</div>
        <ul class="item-list">
          <li class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">
            {{innerItem.name}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  methods: { // 4️⃣-②:在 methods 中定义 handleCityClick 方法;

    handleCityClick (city) { // 4️⃣-③:方法接收一个参数 city;

      this.$store.dispatch('changeCity', city) /*
      																				 4️⃣-④:当城市被点击时,通过 this.$store 
                                               的 dispatch 方法触发一个名叫 changeCity
                                               的 action,并将接收到的 city 作为第二个参数;
                                                */

    }
  },
  watch: {
    letter () {
      const element = this.$refs[this.letter][0]
      this.scroll.scrollToElement(element)
    }
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.wrapper)
  }
}
</script>

<style lang="stylus" scoped>
.border-topbottom
  &:before
    border-color: #ccc
  &:after
    border-color: #ccc
.border-bottom
  &:before
    border-color: #ccc
.list
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  .title
    padding-left: .2rem
    line-height: .54rem
    background: #eee
    color: #666
    font-size: .26rem
  .button-list
    overflow: hidden
    padding: .1rem .6rem .1rem .1rem
    .button-wrapper
      float: left
      width: 33.33%
      .button
        margin: .1rem
        padding: .1rem 0
        text-align: center
        border: .02rem solid #ccc
        border-radius: .06rem
  .item-list
    .item
      line-height: .76rem
      padding-left: .2rem
</style>

4️⃣-⑤:打开 store 下的 index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    city: '上海'
  },
  actions: { // 4️⃣-⑥:在 Store 中,增加一个 actions 对象;

    changeCity (ctx, city) { /*
                             4️⃣-⑦:actions 中有一个与 List.vue 触发的名字一样的 action,
                             即 changeCity;
                             (❗️changeCity 是一个方法,它接收两个参数:第一个 ctx 是上下文
                             context,第二个参数是传递过来的数据 city。)
                             */
      
      ctx.commit('changeCity', city) /*
                                     4️⃣-⑧:changeCity 这个 action,通过 commit 方法,
                                     触发一个名叫 changeCity 的 mutation,并将 city 作为
                                     第二个参数传递过去;
                                     (❗️changeCity 接收的第一个参数 ctx,可以帮助我们拿到
                                     commit 方法;mutation 的名字可另起,不必与 action
                                     的名字 changeCity 相同。)
                                     */
    }
  },
  mutations: { // 4️⃣-⑨:Store 中,增加一个 mutations 对象;
    
    changeCity (state, city) { /*
    													 4️⃣-⑩:changeCity 同样接收两个参数,第一个是 state(指所有的
                               公用数据),第二个是传递进来的 city;
                                */
      
      state.city = city /*
      									4️⃣-⑪: 在 changeCity 这个 mutation 中,让 state 中的 city 等于
                        传过来的 city;
                         */
    }
  }
})

保存后,返回页面查看。当点击“热门城市”时,首页的“城市”和“当前城市”都会跟着发生改变:

视频04.gif

OK,现在我们已经完整走了一遍流程,再简单回忆一下更改数据的流程:

  • 需要更改数据的子组件中,通过 Dispatch 方法调用 action
  • Store 中的 actions 通过 Commit 方法调用 mutation
  • mutation 完成数据的修改。

前面我们说过,“如果是异步的操作,或者比较复杂的、批量的同步操作,放在 Actions 里。有时,我们也可以越过 Actions,让组件直接去调用 Mutations 修改 State 中的数据”。

而在我们的项目中,更改 State 的过程中,并没有什么异步的操作,且这个操作也非常简单,不是批量的操作。所以此时,组件没必要去调用 Actions 做一次转发,它直接调用 Mutations 就可以了。

4️⃣-⑫:打开 store 中的 index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    city: '上海'
  }, // ❗️删掉 actions 这部分的内容。
  mutations: {
    changeCity (state, city) {
      state.city = city
    }
  }
})

4️⃣-⑬:打开 city 下 components 中的 List.vue

<template>
  <div class="list" ref="wrapper">
    <div>
      <div class="area">
        <div class="title border-topbottom">当前城市</div>
        <div class="button-list">
          <div class="button-wrapper">
            <div class="button">
              {{this.$store.state.city}}
            </div>
          </div>
        </div>
      </div>
      <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>
      <div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
        <div class="title border-topbottom">{{key}}</div>
        <ul class="item-list">
          <li
            class="item border-bottom"
            v-for="innerItem of item"
            :key="innerItem.id"
            @click="handleCityClick(innerItem.name)"
          > <!-- 4️⃣-⑮:给列表中的每个“城市”绑定事件,触发后执行 handleCityClick,
						并传递一个参数“城市名” innerItem.name; -->

            {{innerItem.name}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  methods: {
    handleCityClick (city) { // 4️⃣-⑭:通过 commit 方法直接调用 mutation;
      this.$store.commit('changeCity', city)
    }
  },
  watch: {
    letter () {
      const element = this.$refs[this.letter][0]
      this.scroll.scrollToElement(element)
    }
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.wrapper)
  }
}
</script>

<style lang="stylus" scoped>
.border-topbottom
  &:before
    border-color: #ccc
  &:after
    border-color: #ccc
.border-bottom
  &:before
    border-color: #ccc
.list
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  .title
    padding-left: .2rem
    line-height: .54rem
    background: #eee
    color: #666
    font-size: .26rem
  .button-list
    overflow: hidden
    padding: .1rem .6rem .1rem .1rem
    .button-wrapper
      float: left
      width: 33.33%
      .button
        margin: .1rem
        padding: .1rem 0
        text-align: center
        border: .02rem solid #ccc
        border-radius: .06rem
  .item-list
    .item
      line-height: .76rem
      padding-left: .2rem
</style>

4️⃣-⑯:打开 city 下 components 中的 Search.vue

<template>
  <div>
    <div class="search">
      <input
        class="search-input"
        type="text"
        placeholder="输入城市名或拼音"
        v-model="keyword"
      >
    </div>
    <div
      class="search-content"
      ref="search"
      v-show="keyword"
    >
      <ul>
        <li
          class="search-item border-bottom"
          v-for="item of list"
          :key="item.id"
          @click="handleCityClick(item.name)"
        > <!-- 4️⃣-⑰:给每一项搜索出的城市绑定事件,触发时执行 handleCityClick 方法,
					并传入搜索出的“城市名”; -->

          {{item.name}}
        </li>

        <li
          class="search-item border-bottom"
          v-show="hasNoData"
        >
          没有找到匹配数据
        </li>
      </ul>
    </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
    }
  },
  methods: { // 4️⃣-⑱:在 methods 中定义 handleCityClick 方法;

    handleCityClick (city) { /*
                             4️⃣-⑲:方法接收一个参数 city,执行时通过 commit 直接调用
                             mutation 更改数据。
                              */
      this.$store.commit('changeCity', city)
    }
  },
  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 '~styles/varibles.styl'
.search
  height: .72rem
  padding: 0 .1rem
  background: $bgColor
  .search-input
    box-sizing: border-box
    width: 100%
    padding: 0 .1rem
    height: .62rem
    line-height: .62rem
    color: #666
    text-align: center
    border-radius: .06rem
.search-content
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  z-index: 1
  background: #eee
  .search-item
    padding-left: .2rem
    line-height: .62rem
    background: #fff
    color: #666
</style>

保存后,返回页面查看。省略了 Actions 这个步骤后,当点击城市选择页的任意城市名时,首页也会同步更改:

视频05.gif

3 实现切换城市后的页面跳转

🔗前置知识:
《JavaScript 基础——浏览器提供的对象:② DOM》

实现网页上的页面跳转有两种方式:

  • 一种是通过 <a> 标签的方式;
  • 一种是通过 JS 的方式(如 location.href )。

在 Vue 中,也有两种方式:

  • 一种是通过 <router-link> 标签的方式;
  • 另一种也是通过 JS 方式实现跳转。

但在 Vue Router 中,JS 方式与之前不同,它用的是编程式导航的形式。编程式导航提供给我们一个 push 方法,帮助我们做页面跳转。

使用这个方法,我们就可以完成“点击城市后,跳转至首页”的功能。

5️⃣打开 city 下 components 中的 Search.vue

<template>
  <div>
    <div class="search">
      <input
        class="search-input"
        type="text"
        placeholder="输入城市名或拼音"
        v-model="keyword"
      >
    </div>
    <div
      class="search-content"
      ref="search"
      v-show="keyword"
    >
      <ul>
        <li
          class="search-item border-bottom"
          v-for="item of list"
          :key="item.id"
          @click="handleCityClick(item.name)"
        >
          {{item.name}}
        </li>

        <li
          class="search-item border-bottom"
          v-show="hasNoData"
        >
          没有找到匹配数据
        </li>
      </ul>
    </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
    }
  },
  methods: {
    handleCityClick (city) {
      this.$store.commit('changeCity', city)

      this.$router.push('/') /*
      											 5️⃣-①:改变了城市的内容后,通过实例属性 $router 上的 push 方法,
                             跳转至“首页 /”;
                             (❗️因为项目中引入了 Vue Router,所以每一个组件里都有 $router 
                             这样一个实例属性;)
                             (❗️想要跳转至哪一个页面,就 push 哪个地址。)
                              */
    }
  },
  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 '~styles/varibles.styl'
.search
  height: .72rem
  padding: 0 .1rem
  background: $bgColor
  .search-input
    box-sizing: border-box
    width: 100%
    padding: 0 .1rem
    height: .62rem
    line-height: .62rem
    color: #666
    text-align: center
    border-radius: .06rem
.search-content
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  z-index: 1
  background: #eee
  .search-item
    padding-left: .2rem
    line-height: .62rem
    background: #fff
    color: #666
</style>

5️⃣-②:打开 city 下 components 中的 List.vue

<template>
  <div class="list" ref="wrapper">
    <div>
      <div class="area">
        <div class="title border-topbottom">当前城市</div>
        <div class="button-list">
          <div class="button-wrapper">
            <div class="button">
              {{this.$store.state.city}}
            </div>
          </div>
        </div>
      </div>
      <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>
      <div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
        <div class="title border-topbottom">{{key}}</div>
        <ul class="item-list">
          <li
            class="item border-bottom"
            v-for="innerItem of item"
            :key="innerItem.id"
            @click="handleCityClick(innerItem.name)"
          >
            {{innerItem.name}}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  methods: {
    handleCityClick (city) {
      this.$store.commit('changeCity', city)

      this.$router.push('/') /*
      											 5️⃣-③:改变了城市的内容后,通过实例属性 $router 上的 push 方法,
                             跳转至“首页 /”;
                              */
    }
  },
  watch: {
    letter () {
      const element = this.$refs[this.letter][0]
      this.scroll.scrollToElement(element)
    }
  },
  mounted () {
    this.scroll = new BScroll(this.$refs.wrapper)
  }
}
</script>

<style lang="stylus" scoped>
.border-topbottom
  &:before
    border-color: #ccc
  &:after
    border-color: #ccc
.border-bottom
  &:before
    border-color: #ccc
.list
  overflow: hidden
  position: absolute
  top: 1.58rem
  left: 0
  right: 0
  bottom: 0
  .title
    padding-left: .2rem
    line-height: .54rem
    background: #eee
    color: #666
    font-size: .26rem
  .button-list
    overflow: hidden
    padding: .1rem .6rem .1rem .1rem
    .button-wrapper
      float: left
      width: 33.33%
      .button
        margin: .1rem
        padding: .1rem 0
        text-align: center
        border: .02rem solid #ccc
        border-radius: .06rem
  .item-list
    .item
      line-height: .76rem
      padding-left: .2rem
</style>

保存后,返回页面查看:

视频06.gif

以上,我们就实现了“首页”与“城市选择页”之间的联动。

🏆本篇总结:

  • 使用 Vuex 实现组件间数据共享;
  • 使用编程式导航实现页面跳转。

祝好,qdywxs ♥ you!