(17)城市选择页开发——⑦ localStorage 的使用及 Vuex 的高级使用 | Vue.js 项目实战: 移动端“旅游网站”开发

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

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

1 需求

在切换了城市后,当刷新页面时,首页的城市保持为最后一次选择的城市:

视频01.gif

2 localStorage 的使用

🔗前置知识:
《发出请求的“客户端”:⑥ 浏览器存储——Cookie 和 Session》

需求分析:

目前,我们项目在选择了城市后,首页和城市选择页能同步数据,但当刷新后,又会恢复为数据中默认的城市。即,没有存储下最后选择的“城市”。

视频02.gif

实现这个需求,我们只需要用上 localStorage 就可以了。

1️⃣打开 store 中的 index.js

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    city: localStorage.city || '北京' /*
    																 1️⃣-③:state 中 city 的默认值首先从 localStorage
    																 中获取,如果没有,再使用默认城市“北京”;
                                      */
  },
  mutations: {
    changeCity (state, city) {
      state.city = city // 1️⃣-①:当改变城市时,不但对 state 中的城市进行修改;
      localStorage.city = city // 1️⃣-②:还把这个改变的城市存入 localStorage;
    }
  }
})

保存后,返回页面查看:

视频03.gif

OK,功能上没有问题,需求已经实现。

但,使用 localStorage 时有一个细节点需要注意:在某些浏览器上,如果用户关闭了“本地存储”这样的功能,或者开启了“隐身”模式。那么,使用 localStorage 有可能会使浏览器抛出异常,直接导致代码无法运行。

所以,为了解决这个问题,一般建议在 localStorage 外包裹一个 try...catch 。这样就可以让 try 块中有异常抛出时,继续运行其中的语句。

1️⃣-④:返回 store 下的 index.js

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

Vue.use(Vuex)

let defaultCity = '北京' // 1️⃣-⑤:定义一个变量 defaultCity,表示默认城市,它的值为北京;

try { // 1️⃣-⑥:使用 try...catch 包裹 localStorage 的内容;
  
  if (localStorage.city) { /*
  												 1️⃣-⑦:如果 localStorage.city 有值,那么,
                           让默认城市变为 localStorage 中的城市;
                            */
    defaultCity = localStorage.city
  }
} catch (e) {}

export default new Vuex.Store({
  state: {
    city: defaultCity // 1️⃣-⑧:state 中的 city 的值 改为 defaultCity;
  },
  mutations: {
    changeCity (state, city) {
      state.city = city
      
      try { // 1️⃣-⑨:使用 try...catch 包裹 localStorage 的内容。
        localStorage.city = city
      } catch (e) {}
    }
  }
})

除了使用 localStorage 时,要在外层用 try...catch 包裹之外,我们 store 中的 index.js 文件也逐渐变得复杂了。实际上,在真正的项目开发之中,我们还会对它做进一步的拆分。

❓如何对 store 中的 index.js 文件进行拆分呢?

答:这里,我们把 state 和 mutation 单独拆分出来。

2️⃣在 store 下新建一个 state.js 文件:

// 2️⃣-①:把 index.js 中“默认城市”这部分的代码复制粘贴到 state.js;
let defaultCity = '北京'
try {
  if (localStorage.city) {
    defaultCity = localStorage.city
  }
} catch (e) {}

export default { // 2️⃣-②:然后导出城市这块的内容;
  city: defaultCity
}

2️⃣-③:在 store 下新建一个 mutations.js 文件;

export default { // 2️⃣-④:将 index.js 中 Mutations 这部分的内容粘贴到 mutations.js;

  changeCity (state, city) { // 2️⃣-⑤:导出 changeCity 这个 mutation 的内容;
    state.city = city
    try {
      localStorage.city = city
    } catch (e) {}
  }
}

2️⃣-⑥:打开 store 中的 index.js

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

import state from './state' // 2️⃣-⑦:从当前目录下引入 state;
import mutations from './mutations' // 2️⃣-⑧:从当前目录下引入 mutations;

Vue.use(Vuex)

export default new Vuex.Store({
  /*
  2️⃣-⑨:然后 Store 中 state 和 mutations 对应的值改为我们引入的 state 和 mutations;
  state: state, 
  mutations: mutations
   */

  state, // 2️⃣-⑩:由于键和值都一样,所以这里我们可以进一步简化为 state 和 mutations。
  mutations
})

保存后,返回页面查看,效果与之前相同,但我们的代码实际上更为规范,整个 Vuex 的代码也拆分成了几个部分,未来的可维护性也大大提升:

视频04.gif

3 代码优化

3.1 mapState 的使用

在上一篇中,我们引入了 Vuex 实现组件间的数据共享。但在组件中调用 state 中的数据时,写了很长的一串字符: {{this.$store.state.city}}

其实在 Vuex 中,给我们提供了有一个比较高级的 API——mapState,可以让我们的代码更为简洁。

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.city}} <!-- 3️⃣-④:将 this.$store.state.city 改为 this.city;-->

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

<script>
import { mapState } from 'vuex' // 3️⃣-①:引入 mapState;
export default {
  name: 'HomeHeader',

  computed: { // 3️⃣-②:添加计算属性,对 mapState 使用展开运算符 ... ;
    
    ...mapState(['city']) /*
    											3️⃣-③:mapState 指的是,我们把 Vuex 里的数据 city,映射到这个组件
                          的计算属性 city 之中(即,将 city 这个公用数据,映射到这个组件的计算
                          属性中,这个计算属性的的名字依然叫 city);
                          (❗️mapState 中是一个数组而不是字符串,可以方便之后再加入变量。)
                           */
  }
}
</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
    min-width: 1.04rem
    padding: 0 .1rem
    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.currentCity}} <!-- 3️⃣-⑨:这里,就是将 this.$store.state.city 改为
																	 this.currentCity。-->

            </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'
import { mapState } from 'vuex' // 3️⃣-⑥:引入 mapState;
export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  computed: { // 3️⃣-⑦:添加计算属性;
    
    ...mapState({ /*
    							3️⃣-⑧:mapState 中除了是数组,也可以是对象。这里,我们将公用数据中的 city 映
                  射到这个组件的计算属性中,它在这个计算属性中的名字叫 currentCity;
                   */
      currentCity: 'city'
    })
  },
  methods: {
    handleCityClick (city) {
      this.$store.commit('changeCity', city)
      this.$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>

保存后,返回页面查看。mapState 中用数组和对象这两种写法,都没有问题:

视频05.gif

3.2 mapMutations 的使用

Vuex 中,除了 state 提供了 mapState 这个 API,Mutations 也有对应的简便方法——mapMutations

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.currentCity}}
            </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'

import { mapState, mapMutations } from 'vuex' // 4️⃣-①:引入 mapMutations;

export default {
  name: 'CityList',
  props: {
    hot: Array,
    cities: Object,
    letter: String
  },
  computed: {
    ...mapState({
      currentCity: 'city'
    })
  },
  methods: { // 4️⃣-②:在 methods 中运用展开运算符 ... 使用 mapMutations;
    handleCityClick (city) {
      
      // this.$store.commit('changeCity', city) // ❗️原来调用 Mutations 的方式。
      this.changeCity(city) /*
      											4️⃣-④:现在,当调用 Mutations 时,就可以直接调用 changeCity,
                            同时把 city 传递过去。
                             */

      this.$router.push('/')
    },
    ...mapMutations(['changeCity']) /*
    																4️⃣-③:mapMutations 指的是,把 Vuex 中一个
                                    叫 changeCity 的 mutation,映射到这个组件的一个
                                    名叫 changeCity 的方法里;
                                     */
  },
  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 ,在这个组件中也使用 mapMutations;

<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'
import { mapMutations } from 'vuex' // 4️⃣-⑥:引入 mapMutations;

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.changeCity(city) // 4️⃣-⑧:调用 changeCity 方法,并传入 city。

      this.$router.push('/')
    },
    ...mapMutations(['changeCity']) /*
    																4️⃣-⑦:在 methods 中,使用 mapMutations,将 Vuex 中
                                    名叫 changeCity 的 mutation 映射到这个组件中名叫
                                    changeCity 的方法里;
                                     */
  },
  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>

保存后,返回页面查看,控制台无报错,功能一切正常:

视频06.gif

以上,我们就在项目中使用了 localStorage 实现本地存储“城市”,并使用了 Vuex 中的 mapState 和 mapMutations。

🏆本篇总结:

  1. 使用 localStorage 时,在外层包裹一个 try...catch
  2. 当 store 中的 index.js 变得复杂时,我们需要对它进行拆分,以提高项目的可维护性;

祝好,qdywxs ♥ you!