转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 需求
在切换了城市后,当刷新页面时,首页的城市保持为最后一次选择的城市:
2 localStorage 的使用
🔗前置知识:
《发出请求的“客户端”:⑥ 浏览器存储——Cookie 和 Session》
需求分析:
目前,我们项目在选择了城市后,首页和城市选择页能同步数据,但当刷新后,又会恢复为数据中默认的城市。即,没有存储下最后选择的“城市”。
实现这个需求,我们只需要用上 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;
}
}
})
保存后,返回页面查看:
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 的代码也拆分成了几个部分,未来的可维护性也大大提升:
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"></span>
</div>
<div class="header-input">
<span class="iconfont"></span>
输入城市/景点/游玩主题
</div>
<router-link to="/city">
<div class="header-right">
{{this.city}} <!-- 3️⃣-④:将 this.$store.state.city 改为 this.city;-->
<span class="iconfont arrow-icon"></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 中用数组和对象这两种写法,都没有问题:
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>
保存后,返回页面查看,控制台无报错,功能一切正常:
以上,我们就在项目中使用了 localStorage 实现本地存储“城市”,并使用了 Vuex 中的 mapState 和 mapMutations。
🏆本篇总结:
- 使用 localStorage 时,在外层包裹一个
try...catch; - 当 store 中的
index.js变得复杂时,我们需要对它进行拆分,以提高项目的可维护性;
祝好,qdywxs ♥ you!