1 城市选择页面
1.1 图
模块
<template>
<div>
<city-header></city-header>
<city-search></city-search>
<city-list></city-list>
<city-alphabet></city-alphabet>
</div>
</template>
2 布局和功能
2.1 header 路由配置
<router-link to=""></router-link>
router-link相当于一个a标签,属性to相当于属性href
2.2 布局
box-sizing
box-sizing 属性定义了 user agent 应该如何计算一个元素的总宽度和总高度。
box-sizing: border-box
- 你想要设置的边框和内边距的值是包含在width内的
伪元素
border-topbottom1像素的边框的颜色更改:
.border-topbottom
&:before
border-color: #ccc
&:after
border-color: #ccc
- 学习伪元素的应用实例:
用
::after伪元素,attr()CSS表达式和一个自定义数据属性data-descr创建一个纯CSS,词汇表提示工具。在MDN页面看这个例子。
overflow: hidden
- 子元素设置float属性,父元素设置
overflow:hidden
子元素设置float属性之后,父元素不能自适应子元素的高度,如果给父元素设置overflow:hidden,则会触发父元素的BFC,使得父元素可以自适应子元素的高度。 - 给列表list加一个overflow: hidden,使其无法拖动进度条,使用第三方包BetterScroll。👇👇👇
3 Better-scroll 的使用和字母表布局
3.1 Better-scroll
- 安装
npm install better-scroll --save - 初始化
(The simplest initialization code is as follow)创建实例的时候 要接收一个DOM选择器或DOM元素import BScroll from '@better-scroll/core' let wrapper = document.querySelector('.wrapper') let scroll = new BScroll(wrapper) - 使用
ref帮助我们获取DOM<template> <div class="list" ref="wrapper"> ...... </div> </template> <script> import BScroll from '@better-scroll/core' export default { name: 'CityList', mounted () { this.scroll = new BScroll(this.$refs.wrapper) } } </script> <style lang="stylus" scoped> ...... </style>
3.2 字母表布局
- 定位布局定位字母表位置
- flex弹性布局,更改主轴方向。使字母表垂直居中。
- 通过text-aligin:center设置元素内字母水平居中。
<template>
<ul class="list">
<li class="item">A</li>
......
</ul>
</template>
<script>
export default {
name: 'CityAlphabet'
}
</script>
<style lang="stylus" scoped>
@import '~style/variable.styl'
.list
display: flex
flex-direction: column
justify-content: center
position: absolute
top: 1.58rem
right: 0
bottom: 0
width: .4rem
.item
line-height: .4rem
text-align: center
color: $bckColor
</style>
4. 页面的动态数据渲染 list.vue
对象循环和数组循环
//对象循环(item, key) of cities :key="key" 注意key
<div class="area" v-for="(item, key) of cities" :key="key">
<div class="title border-topbottom">{{key}}</div>
<div class="item-list">
//注意数组循环的key
<div class="item border-bottom" v-for="innerItem of item" :key="innerItem.id">
{{innerItem.name}}
</div>
</div>
</div>
5. 兄弟组件之间的联动
5.1 点击字母,列表滚动到对应的字母区域
字母表子组件 → 城市父组件 → 列表子组件
- Alphabet.vue触发change事件并传递letter值。子组件Alphabet通过事件触发的形式向父组件City传值
- 父组件City.vue 监听内部组件向外触发的自定义事件,再通过属性
letter向另一个子组件list传值。 - list.vue组件
- ref前加了v-bind:是因为要绑定"key"
5.2 移动端的touch事件,在字母表上拖拽,触发列表滚动到相应字母区域
-
移动端的touch事件:touchStart\touchMove\touchEnd
- 监测到touchMove事件
- a. 计算当前touch的位置是哪个字母
- b. 触发 list 滚动到对应字母的位置
- 监测到touchMove事件
-
对象由 key: value 键值对组成,
for...in...遍历的是对象的属性key,用对象[key]得到的是value。 -
offsetTop 是一个只读属性,返回当前元素相对于 offsetParent 节点顶部边界的偏移像素值。offsetParent 元素是一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的元素。
-
clientY clientY 事件属性返回当事件被触发时鼠标指针相对于浏览器页面(客户区)的垂直坐标。 (代码见下)
6. 列表切换性能优化
6.1 减少handleTouchMove中startY重复计算的次数
初次渲染 alphabet 用的是 cities:{} 页面刚加载的时候 alphabet 里什么东西都不会显示,当 city.vue ajax获取到数据后,city 值才发生变化,apphabet 才被渲染出来。
startY在updated()中计算。
当向 aplabet 传递的值发生变化的时候,aphabet 会重新渲染,updated 这个生命周期钩子就会被执行。
6.2 函数节流
当鼠标在字母表上来回移动的时候 handleTouchMove 执行的频率非常高,通过节流限制函数执行的频率,提高网页的性能。
setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数。clearTimeout是 windows 下提供的原生方法,用于删除特定的 setTimeout 的延迟任务,我们在定义 setTimeout 的时候返回值就是当前任务的唯一 id 值,那么 clearTimeout 就会拿着 id 在延迟消息队列里查找对应的任务,将其踢出队列即可。
节流:规定在一段时间内,只能触发一次函数。如果这个时间内触发多次函数,只有一次生效。
(代码见下)
7 list.vue 实现
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
data () {
return {
touchStatus: false,
startY: 0,
timer: null
}
},
updated () {
this.startY = this.$refs['A'][0].offsetTop//计算字母A到list顶部的距离
},
computed: {//计算属性
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
},
methods: {
handleClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) {
if (this.touchStatus) {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79//79为header的height
const letterIndex = Math.floor((touchY - this.startY) / 20)//20为每个字母li的height
console.log(this.letters[letterIndex])
if (letterIndex >= 0 && letterIndex < this.letters.length) {
this.$emit('change', this.letters[letterIndex])
}
}, 16)
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
<style lang="stylus" scoped>
</style>
8 城市搜索框 - 城市列表 逻辑
功能1:
- 1.1 在input框中输入keyword,watch监听keyword的变化,并触发搜索展示功能。
- 1.2 运用Better-scroll实现keyword结果的scroll。
功能2:
v-show - 2.1 若input框中无输入,则不显示下拉框。
- 2.2 若搜索不到对应keyword的结果,则显示
li“没有找到匹配数据”,如果找到了,则这条li不显示。
<template>
<div>
<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
</div>
<div class="search-content" ref="search" v-show="keyword">
<ul>
<li class="search-item border-bottom" v-for="item of citiList" :key="item.id">{{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: '',
citiList: [],
timer: null
}
},
computed: {
hasNoData () {
return !this.citiList.length
}
},
watch: {
keyword () {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.keyword) {
this.citiList = []
return
}
this.timer = setTimeout(() => {
for (let i in this.cities) {
this.cities[i].forEach((value) => {
if (value.spell.indexOf(this.keyword) > -1 || value.name.indexOf(this.keyword) > -1) {
this.citiList.push(value)
}
})
}
}, 1000)
}
},
mounted () {
this.scroll = new Bscroll(this.$refs.search)
}
}
</script>
<style lang="stylus" scoped>
</style>
3 Vuex 实现数据共享(主页 ←→ 城市选择页)
3.1 Vuex
- state 存放公用数据。
- action 异步方法。
- mutation 同步的对数据的改变的方法。
- getter 作用类似于 computed 计算属性的作用,根据 state 中的数据算出来一些新的数据的时候用到。
- 当用到一个非常复杂的业务场景,把所有的 mutation 放到一个 mutation 文件中,非常冗余,这时候可以借助 module 对复杂的 mutations、state、action...进行拆分,增加代码的可维护性。
3.2 什么情况下使用vuex?
首页和城市选择页面没有共同的父级组件,无法进行数据的中转。且使用bus总线的方法比较麻烦。
数据框架 vuex,当多个组件之间、页面之间进行数据的传值很困难(麻烦)的时候,把公共数据放到公共空间中存储,某个组件或页面改变了公共数据的值,是全局改变的。 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
}
}
})
组件中怎么写:
methods: {
handleCityClick (city) {
this.$store.dispatch('ChangeCity', city)
this.$router.push('/')
}
},
3.3 Vuex 的高级使用及 localStorage
本地存储
- 问题:选择城市后,刷新,又变成了原先默认的城市。
- 解决:html5 中提供了 localStorage API,能实现类似 cookie 的功能,做到本地存储。
- 使用 localStorage 的时候再外层包裹一层
try cache,目的:某些浏览器,如果用户关闭了本地存储的功能或者使用了隐身模式,使用 localStorage 有可能导致浏览器直接抛出异常。 store->index.js
- 使用 localStorage 的时候再外层包裹一层
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
})
state.js
let defaultCity = '上海'
try {
if (localStorage.city) {
defaultCity = localStorage.city
}
} catch (e) {}
export default {
city: defaultCity
}
mutations.js
export default {
changeCity (state, city) {
state.city = city
try {
localStorage.city = city
} catch (e) {}
}
}
mapState
this.$store.state.city这样写太复杂,使用 mapState 把 vuex 里的数据映射到组件的计算属性上,this.city。import { mapState } from 'vuex'computed: { ...mapState(['city'])//ES6展开运算符`...`把公用数据 city 映射到一个叫 city 的计算属性上 }this.$store.dispatch('ChangeCity', city)这样写太复杂,使用mapMutation ,把mutation里的ChangeCity函数映射到组件里一个名字叫 changeCity 的方法里。import { mapMutations } from 'vuex'methods: { ...mapMutations(['ChangeCity'])//ES6展开运算符`...`把公用数据 city 映射到一个叫 city 的计算属性上 }
4 vue-router 路由
功能:点击选择了城市后,直接跳转到首页。
除了使用 <router-link> 创建 a 标签来定义导航链接,还可以借助 router 的实例方法,通过编写JS代码来实现。
this.$router.push('/')
5 使用 vue 内置标签 keep-alive 优化网页性能
keep-alive
问题
解决:使用 <keep-alive> 标签
路由的内容被加载一次,就把路由中的内容放到内存中缓存,下一次再进路由,不需要重新渲染组件执行钩子函数 mounted,只是从内存中把以前的数据调出来显示就可以了。
App.vue:
<keep-alive>
<router-view/>
</keep-alive>
问题
然而,有些功能需要做到:路由切换,要重新请求 Ajax 。
- 如:城市改变后回到首页,首页再重新发一次 ajax 请求相应城市的数据.
解决
使用 keep-alive 会多出一个生命周期函数 activated(),下一次再进路由的时候,会重新执行 activated()
- 当页面只要被展示,
activated()一定会被执行。<script> import axios from 'axios' import { mapState } from 'vuex' export default { name: 'Home', data () { return { lastCity: ''//定义lastCity 记录上次加载时的city } }, computed: { ...mapState(['city']) //获取vuex state中的city 作为当前city }, methods: { getHomeInfo () { axios.get('/api/index.json?city=' + this.city) //ajax请求路径加上city .then(this.getHomeInfoSucc) }, getHomeInfoSucc (res) { res = res.data console.log(res) if (res.ret && res.data) { const data = res.data this.swiperList = data.swiperList this.iconList = data.iconList this.recommendList = data.recommendList this.weekendList = data.weekendList } } }, mounted () { this.lastCity = this.city //首次路由 lastCity = city this.getHomeInfo() }, activated () { //每次路由切换 都会执行 activated 函数 if (this.city !== this.lastCity) { //如果切换了城市 再进行 ajax 请求数据 this.lastCity = this.city this.getHomeInfo() } } } </script>
参考
juejin.cn/post/702883…
juejin.cn/post/684490…
路由:router.vuejs.org/zh/guide/es…