第10章 Vue项目开发之城市
10-1.city页面路由配置
1.添加路由配置
// router/index.js文件
import City from '@/pages/city/City'
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/city',
name: 'City',
component: City
}
]
})
2.添加相对应的页面
在pages文件下面添加city文件夹和City.vue文件
效果图
3.初始化City.vue页面
<template>
<div>
city
</div>
</template>
<script>
export default {
name: 'City'
}
</script>
<style lang="stylus" scoped>
</style>
10-2.city-header部分制作
//city/City.vue
<template>
<div>
<city-header></city-header>
</div>
</template>
<script>
import CityHeader from './components/Header'
export default {
name: 'City',
components: {
CityHeader
}
}
</script>
<style lang="stylus" scoped>
</style>
//city/components/header.vue
<template>
<div class="header">
城市选择
<router-link to="/">
<div class="iconfont back-city"></div>
</router-link>
</div>
</template>
<script>
export default {
name: 'CityHeader'
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.header
position relative
height $headHeight
line-height $headHeight
background $bgColor
text-align center
color #ffffff
.back-city
position absolute
left 0
top 0
width .64rem
text-align center
padding 0 .1rem
font-size .48rem
color #fff
</style>
效果图
10-3.city-search部分制作
上面咱们已经完成了头部的制作,这一节咱们来city-search的ui部分制作,随后等咱们把city列表制作完成后,咱们再来制作city-search相关的逻辑部分,代码如下
//city/components/Search.vue
<template>
<div class="search">
<input class="search-input" type="text" placeholder="输入城市名称或者拼音" />
</div>
</template>
<script>
export default {
name: 'CitySearch'
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.search
height .722rem
padding 0 .1rem
background $bgColor
.search-input
box-sizing border-box
width 100%
height .62rem
padding 0 .1rem
line-height .62rem
border-radius .06rem
color #666
text-align center
</style>
city/City.vue,在city的主页面引入我们制作好的city-search模块
image.png
10-3 city-list、city-ajax 、city-vuex、city-search-logic 部分的制作
城市整体效果图Ui上面的制作,直接从github下载下来,git checkout 到不同的分支,就能看到代码了,总结嘛,不能能把所有的项目中的代码都展示出来,更多的是展示难点、思路、注意事项等等一些小细节地方。
city-ajax部分和index-ajax 方式是一样,在这里咱们就不再次赘述了
知识点1:BetterScroll 的使用,让城市列表可以滚动起来
//安装better-scroll
npm install better-scroll -S
在这个使用better-scroll的时候我们需要注意三点
- dom结构(要符合这种结构)
<div class="wrapper">
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
<!-- you can put some other DOMs here, it won't affect the scrolling
</div>
- 样式(要滚动的list 要脱离文档流)
.list
overflow: hidden;
position: absolute;
top: 1.6rem;
left: 0;
right: 0;
bottom: 0;
- 在vue中的调用和使用方法
//dom部分
<div class="list" ref="wrapper">
.....
</div>
//js部分
import BScroll from 'better-scroll'
mounted () {
this.scroll = new BScroll(this.$refs.wrapper)
}
知识点2兄弟组件数据传递
我们知道:
City.vue是父组件
components/List.vue是一个子组件
components/Alphabet.vue也是一个子组件
那么子组件(Alphabet.vue)如何和子组件(List.vue)进行通信呢?
现在有这样的一个需求,就是当我们点击右侧的字母(代码在Alphabet.vue中),列表(List.vue)能自动滚动相对应的列表字母模块部分,那么这个过程就是一个子组件和子组件的通信(兄弟组件数据传递)
思路:
第一步:子组件(Alphabet.vue)点击字母的时候,通过$emit发送一个'change'的方法,并且把携带的点击入参传递给父组(City.vue)
//dom部分
<li
class="item"
v-for="item of letters"
:key="item"
@click="handleLetterClick" //触发点击事件
>{{item}}</li>
//js部分
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
}
}
第二步:父组件(City.vue)通过属性来监听‘change’事件,同时创建一个新的方法,在此方法中来接受子组件传递过来的参数,随后把入参放入到data初始化的letter中,再然后,把letter获得入参以属性的方式传递给city-list组件
//1)dom 来监听子组件发出来的change
<city-alphabet :cities="cities" @change="handleLetterClick"></city-alphabet>
//4)dom 父组件从子组件那拿来的数据(letter)传递给新的子组件
<city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list>
//2)初始化data中的letter值 用来存储子组件出来的入参
data () {
return {
letter: ''
}
},
//3)js change 创建的方法 来接受子组件传递过来的值,并把它存储到data里面
handleLetterClick (letter) {
this.letter = letter
}
第三步:子组件(List.vue)通过属性props来接受父组件传过来的值
//js
props: {
letter: String//接受父组件传递过来的值
},
//js 监听传过来值的变化
watch: {
letter () {
if (this.letter) {
const element = this.$refs[this.letter][0] //通过获取字母的值
this.scroll.scrollToElement(element) //滚动到指定元素模块
}
}
}
//dom 需要在字母模块添加ref属性
<div
class="area"
v-for="(item,key) of cities"
:key="key"
:ref="key"//这个key值刚好和兄弟组件传过来的值相同
>
<div class="title border-topbottom">{{key}}</div>
<div class="item-list">
<div class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">{{innerItem.name}}</div>
</div>
</div>
知识点3 完成一个手指滑动右侧字母,左侧区域跟着滚动
这部分咱们需要给右侧的字母绑定上三个事件:
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
为了只让在touchmove里面去触发这些操作,所以我们需要定义个开关(标示位),我们把这个标示位放在了data里面
touchStatus: false //设置为false
所以当我们开始滑动的时候我们把touchStatus设置为true
handleTouchStart () {
this.touchStatus = true
}
当我们手指划出触发操作区域的时候,我们需要把标示为设置为false
handleTouchEnd () {
this.touchStatus = false
}
所以只有当标示位为true的这种情况,我们采取进滑动字母相对应的操作
handleTouchMove () {
if (this.touchStatus) {
//滑动过程中所对应的逻辑
}
}
思路
在滑动这个过程中,最终我们在这个页面上下滑动的时候,我们需要知道你滑动的位置是第几个字母
1、我们需要知道A字母距离顶部的距离
2、我们需要知道手指滑动到当前字母距离顶部的的距离
3、把上面两个做一个差值,那么我们就可以得到当前位置距离A字母之间的高度
4、我们把得到这个差值高度除以每个字母的高度,那么我们就得到了是第几个字母了
根据上面这个思路,我们需要得到这个字母的数组:
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
}
通过计算属性,我们就可以把dom上的数据获取从父组件传递过来的cities改为letters
<li
class="item"
v-for="item of letters" //通过计算属性来获得字母值
:key="item"
:ref="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}
</li>
根据上面的思路咱们开始来编写相对应逻辑
handleTouchMove (e) {
//标示位开始
if (this.touchStart) {
const startY = this.$refs['A'].offsetTop //获取字母A距离顶部的距离
const touchY = e.touches[0].clientY - 79 //获取手机滑动当前字母距离顶部距离(79是header和搜索框的高度)
const index = Math.floor((touchY-startY) / 20) //获得是第几个字母
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index]) //在有效的索引里面去 查找是第几个字母
}
}
}
其实写到这块我们的功能是完成了的,但是细想还有一些地方需要优化?
初始化
data () {
return {
startY: 0,
timer: null
}
},
优化一:每次都去求获取字母A距离顶部的距离?
updated () {
this.startY = this.$refs['A'][0].offsetTop
},
优化二:滑动字母的时候,需要做一下事件节流(通过一个定时器timer)
handleTouchMove (e) {
if (this.touchStatus) {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const startY = this.startY
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}, 16)
}
},
知识点4 实现一个城市搜索功能
需求
1.根据字母或者汉字可以进行检索想要的内容
2.当搜索框没数据的时候,不显示搜索区域内容
3.当搜索框有数据且数据不在搜索内容时,显示暂无搜索内容
4.当搜索出来的内容比较多的时候,搜索内容可以进行滚动(better-scroll)
第一步:获取从父组件传递过来的cities值
props: {
cities: Object
},
第二步:data里面初始化keyword、list、timer
data () {
return {
keyword: '',
list: [],
timer: null
}
},
第三步:watch方法监听keyword的更改、其中这里面包含timer优化、list数据获取、检索操作的逻辑
watch: {
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.keyword) {
this.list = []
return false
}
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)
}
},
第四步:数据处理好了,要铺到Ui上面
为了可以滚动:一定要符合better-scroll的dom结构;search-content样式要脱离文档流。
只有当有关键字才会显示搜索内容;
当关键字搜索没有数据的时候,显示”没有搜索到匹配内容“
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="search-item border-bottom" v-for="item of list" :key="item.id">{{item.name}}</li>
<li class="search-item border-bottom" v-show="hasNoData">没有搜索到匹配内容</li>
</ul>
</div>
第五步:搜索数据有了,但是过多的时候也要可以滚动,better-scroll
mounted () {
this.scroll = new Bscroll(this.$refs.search)
}
知识点5 vuex实现数据共享
如果学过react的同学肯定知道redux,react是处理ui层的,那么数据层就是通过redux来完成,方便我们不同页面之间的传值,一直值的更改等等
同样在vue中,vue也只负责ui部分,vuex则是用来处理数据层的
1.安装vuex
npm install vuex -S
2.使用和调用vuex
因为vuex是处理数据模块的,所以我们在src目录下创建一个store目录,在store目录下面创建一个
index.js
import Vue from 'vue'
import Vuex from 'vuex'
export default new Vuex.Store({
state: {
city: '北京'
}
})
创建好之后,我们在main.js文件中去调用这个文件
import store from './store'
new Vue({
el: '#app',
store,//根实例引入store
router,
components: { App },
template: '<App/>'
})
3.应用
在咱们这个项目中,首页右上角的城市名称是通过后端返给我们,那么我们可以通过vuex来初始化一个城市,也可以通过vuex来更改城市这个值。
在store/index.js 其实我们已经做了city的初始化的值:北京
那么在首页和城市页面我们如何获取vuex当中这个值呢?
//pages/home/components/Header.vue
{{this.$store.state.city}}
//pages/city/components/List.vue 当前城市
{{this.$store.state.city}}
点击热门城市或者点击城市搜索出来列表切换城市的显示,那么我们去如何更改state这个值呢?
//点击热门城市事件
@click="handleCityClick(item.name)"
methods: {
handleCityClick (city) {
//要调用store里面的dispatch方法
this.$store.dispatch('changeCity', city)
}
}
上面我们已经触发了一个dispatch的方法,那么我们通过actions来接受这个方法
store/index.js
export default new Vuex.Store({
state: {
city: '上海'
},
actions: {
changeCity(ctx, city) {
//console.log(city)
//那么action如何调用mutations呢?通过commit方法
ctx.commit('changeCity',city)
}
},
mutations: {
changeCity (state, city) {
state.city = city
}
}
})
从上面可以看出在我们发送dispatch的时候,并没有触发异步请求,或者批量的数据操作,所以上面操作,我们可以直接跳过actions这部分,不需要去触发dispatch操作,而是直接调用commit对mutations的操作
所以上面的代码就可以改为:
//点击热门城市事件
@click="handleCityClick(item.name)"
methods: {
handleCityClick (city) {
//要调用store里面的dispatch方法
this.$store.commit('changeCity', city) //将dispatch 改为commit
}
}
//store/index.js
export default new Vuex.Store({
state: {
city: '上海'
},
//删除actions的相关操作
mutations: {
changeCity (state, city) {
state.city = city
}
}
})
讲到这里其实就实现了vuex的数据一个设置以及显示的一些操作,但是我们更具当前的产品需求我们还是需要完善一下页面跳转。
之前我们实现页面跳转是通过
1.router-link 的to属性来实现
2.那么还有一种通过js 来实现页面跳转的$router.push
那么我们希望我们在选择完城市后,能自动跳转到首页,那么
this.$router.push('/')
知识点6 vuex的高级使用以及localStorage
store/index.js文件的拆分和localStorage的应用
在上面使用vuex中我们给city设置了一个初始值:'上海',但是当我们切换完城市后,返回首页,如果我们刷新首页,那么我们选择的城市就又变回为了默认值:'上海',那么针对这种情况,我们需要引入本地缓存localStorage,但是呢,有些浏览器会屏蔽localStorage的一些东西,为了程序的健壮性,减少没必要的浏览器异常,所以在对localStorage进行相关操作的时候,我们先进行一层try catch的操作
//store/index.js
let defaultCity = '上海'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) {}
export default new Vuex.Store({
state: {
city: defaultCity
},
mutations: {
changeCity (state, city) {
state.city = city
try {
localStorage.city = city
} catch (e) {}
}
}
})
写到这里我们发现,将来如果我们业务比较复杂的话,store/index.js会变的越来越庞大,那么这不是我们希望看到的,所以我们要对store/index.js进行拆分。
那么如何进行拆分呢?
store/index.js 只是一个总文件,而这个总文件包含很多部分:state、actions、mutations等等,
那么我们将可以将这些模块拆分成为:state.js、actions.js、mutations.js
最后再把他们引入到store/index.js文件中
那么,根据这个思路咱们接下来拆分一下store/index.js
//store/state.js
let defaultCity = '北京'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) {
}
export default {
city: defaultCity
}
//store/mutions.js
export default{
changeCity (state, city) {
state.city = city
try {
localStorage.city = city
} catch (e) {}
}
}
那么store/index.js 就变为了:
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations
})
vuex的高级应用以及针对项目当中的优化
我们上面调用城市的时候是通过{{this.$store.state.city}}
来实现的
如果这么写的话,略显页面比较冗余。那么有没有其他方法会比较简单一些呢?
vuex帮我们封装了一些方法和aip有一个mapState的方法就可以帮我们实现,那么应该如何使用呢?
import { mapState } from 'vuex'
//第一种通过数组方法获取
computed: {
...mapState(['city']) //这样就把把store中的city值获取到
}
//第二种通过对象方法获取(起一个别名)
computed: {
...mapState({
currentCity: 'city'
}) //这样就把把store中的city值获取到
}
//如果是第一种方法获取的
将原来的 {{this.$store.state.city}} 改为 {{this.city}}
//如果是第二种方法获取的
将原来的 {{this.$store.state.city}} 改为 {{this.currentCity}}
获取vuex中store的数据我们可以通过mapState方法,那么设置vuex数据呢?
我们可以通过vuex给我们提供的mapMutations方法,那么如何实现呢?
import {mapMutations} from 'vuex'
methods: {
handleCityClick (city) {
//this.$store.commit('changeCity', city) 改为下面:
this.changeCity(city)
this.$router.push('/')
},
...mapMutations(['changeCity'])
}
讲的这里我们使用了vuex给我们提供的state、actions、mutations,我们登录vue官网,我们发现vuex还给我们提供了两个一个是getter、另一个是module
那么我们来看一下getter的使用
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mutations,
getters: {
doubleCity (state) {
return state.city + ' ' + state.city
}
}
})
那么页面上应该如何使用或者调用呢?
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['doubleCity'])
}
//页面上调用
{{this.doubleCity}}
那么我们此时会想,这有什么用处呢?因为mapState就可以实现的方法,我为什么还要使用mapGetters来实现呢?
其实呢,我们发现getters方法有点类似vue组件当中的computed方法,他可以把我们state值进行处理后返给我们一个新值,从来来避免一些数据的冗余。
getter讲完了,那么module我们在什么情况下去使用呢?
因为我们在store/index.js中 只写了city相关的(state、actions、mutations)等等操作,当我们在实际开发的过程中,我们肯定不单单只有city这一个模块的,如果有很多页面的功能模块的话,我们拆分的state.js、actions.js、mutations.js会变得很臃肿的,这不是我们期盼看到的。
所以我们通过module模块对我们的功能模块进行进一步的拆分,每个功能模块包含自己的(state、actions、mutations等等)。如下面例子:
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
知识点7 使用keep-alive优化网页
keep-alive是个抽象组件(或称为功能型组件),实际上不会被渲染在DOM树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次再渲染的时候,还会保持其中的所有状态,并且会触发activated钩子函数。因为缓存的需要通常出现在页面切换时,所以常与router-view一起出现:
//app.vue
<keep-alive>
<router-view/>
</keep-alive>
如此一来,每一个在router-view中渲染的组件,都会被缓存起来。
如果只想渲染某一些页面/组件,可以使用keep-alive组件的include/exclude属性。include属性表示要缓存的组件名(即组件定义时的name属性),接收的类型为string、RegExp或string数组;exclude属性有着相反的作用,匹配到的组件不会被缓存。假如可能出现在同一router-view的N个页面中,我只想缓存列表页和详情页,那么可以这样写:
<keep-alive :include="['Home', 'City']">
<router-view />
</keep-alive>
那么针对咱们这个项目,当我们增加上keep-alive属性后,当我们访问过的页面请求过后,再去请求的时候,那么就不会再去触发ajax请求,而在此项目中首页的数据变更是需要我们切换不同的城市来实现变更的,也就是当城市只要变更我们就需要对首页数据进行一次请求。那么我们应该如何更新首页的数据呢?
//通过vuex的mapState属性我们可以获取 city的值
import { mapState } from 'vuex'
computed: {
...mapState(['city'])
},
//通过ajax的入参来请求不同城市的数据
getHomeInfo () {
axios.get('/api/index.json?city=' + this.city).then(this.getHomeInfoSucc)
},
//当我们触发了keep-alive属性后,那么就会多出一个activated的生命周期钩子
//通过城市的变更,我们在这个生命周期函数中再去请求接口,入参用最新的城市,那么怎么来区分城市的变更呢?
//data里面定义一个新的字段lastCity
data () {
return {
lastCity: '',//定义
swiperList: [],
iconList: [],
recommendList: [],
weekendList: []
}
},
//如果当前的city和最后一次请求的城市不一致,那么我们把最后一次请求的城市赋值为最新的城市,且用最新的城市作为接口请求的入参
activated () {
if (this.lastCity !== this.city) {
this.lastCity = this.city
this.getHomeInfo()
}
}