仿去哪儿网webapp(二)
1,热销组件开发
注,关于开启flex,flex子项开始省略号样式不生效情况,需要在flex:1的子项添加min-width:0,省略号即可生效
原因
flex: 1的用处 在一个大的div中已知一个或多个内部div元素的宽度 为某个未知宽度的div元素设置flex:1 将沾满剩余空间 一般情况下,min-width的默认值是0,但是flexbox容器的flex项的min-width属性默认为auto 如图
<div> <div class="title">热销推荐</div> <ul> <li class="item"> <img src="http://img1.qunarzz.com/sight/p0/1511/d2/d2aec2dfc5aa771290.water.jpg_140x140_abb362a7.jpg" class="item-img"> <div class="item-info"> <p class="item-title">大连圣亚海洋世界大连圣亚海洋世界大连圣亚海洋世界</p> <p class="item-desc">浪漫大连首站,浪漫的海洋主题乐园</p> <button class="item-buttton">查看看那详情</button> </div> </li> </ul> </div>
样式
.item overflow :hidden display :flex height :1.9rem .item-img flex: 0 0 1.7rem height :1.7rem padding :.1rem .item-info flex:1 padding :.1rem min-width: 0 .item-title line-height :.54rem font-size:.32rem overflow: hidden white-space: nowrap text-overflow: ellipsis
2.关于父组件传数据,刷新页面,swiper轮播图最新展示的是最后一页图片,原因,刚开始刷新页面,数据异步请求,此时传递的是一个空数组, 需要在子组件中v-if判断数据是否请求
回来。
父组件发送给ajax请求,获取数据定义在父组件中
data() { return { swiperList: [],
<home-swiper :list="swiperList"></home-swiper>
子组件接收数据
<div class="wrapper" v-if="swiperList.length"> <swiper ref="mySwiper" :options="swiperOptions" > <swiper-slide v-for="(item, index) in swiperList" :key="index"> <img class="item" :src="item.imgUrl" alt="" /> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </div>
3.移动端整个页面不想要浏览器系统滚动,可以给根标签添加如下样式,必须要采取定位,不加定位,没有效果
.list overflow: hidden position: absolute top: 0
left: 0 right: 0 bottom: 0
4.城市列表组件list, 城市序号组件alphapet
实现一个需求,当点击右边的字母时,右边的城市列表自动滚动到对应的字母城市
城市序号组件,注,cities是一个对象,vf循环,key是代表属性值(字母符号)
<template> <ul class="list"> <li class="item" v-for="(item, key) in cities" :key="key" @click="handleLetterClick" > {{ key }} </li> </ul> </template> <script> export default { data() { return {}; }, methods: { // 点击字母排序列表 handleLetterClick(e) { let letter = e.target.innerText; // 给list兄弟组件传递字母 this.$bus.$emit("change", letter); } }, props: ["cities"] }; </script>
城市列表组件
注,1.传递过来的字母数据如何与每个城市列表对应呢,通过ref属性定位,better-scroll有个api, scrollToElement(), 可以滚动到对应元素
2.this.$refs[letter]是一个数组,因为排序城市的节点是vf循环过来的
<!-- 排序城市 --> <div class="area" v-for="(items, key) in cities" :key="key" :ref="key"> <div class="title border-1px-bottom border-1px-top">{{ key }}</div> <div class="item-list " v-for="(item, index) in items" :key="item.id"> <div class="item border-1px-bottom"> {{ item.name }} </div> </div> </div> </div> </div> </template> <script> import BSscroll from "better-scroll"; export default { data() { return {}; }, props: ["hotCities", "cities"], mounted() { this.__initScroll(); this.$bus.$on("change", this.handleLetterChange); }, methods: { __initScroll() { this.$nextTick(() => { if (!this.BSscroll) { this.BSscroll = new BSscroll(this.$refs.wrapper, { click: true, probeType: 1 }); } else { this.BSscroll.refresh(); } }); }, // 获取兄弟组件的字母数据 handleLetterChange(letter) { if (letter) { // 获取每个城市列表的节点,letter是变量 // const cityNode = this.$refs[letter] const cityNode = this.$refs[letter][0]; // console.log(cityNode) this.BSscroll.scrollToElement(cityNode); } } }, watch: { // 监视数据,最后一次刷新 hotCities(newVal) { this.__initScroll(); }, cities(newVal) { this.__initScroll(); } } };
5.实现一个需求,当滑动右侧的字母排序栏,对应的城市列表也会自动滚动到当前
逻辑分析,1.父组件传过来的cities数据是一个对象,将它计算成为一个数组letters,
2.添加触摸原生事件,获取A的位置startY(通过ref属性), 获取鼠标触摸到当前字母的位置endY, 每个字母的高度为20px, 获取当前字母在计算字母数组中的索引
3.用事件总线将字母传给兄弟组件list, 借用better-scroll的scrollToElement()让城市列表滚动到当前字母的
<template> <ul class="list" ref="listRef"> <li class="item" v-for="(item, index) in letters" :key="index" :ref="item" @click="handleLetterClick" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd" > {{ item }} </li> </ul> </template> <script> // 每个字母高度20px export default { data() { return { touchstatus: false }; }, mounted() {}, computed: { letters() { // 将字母放入数组中 const letters = []; for (let i in this.cities) { letters.push(i); } return letters; } }, methods: { // 点击字母排序列表 handleLetterClick(e) { // console.log('aaa') let letter = e.target.innerText; // 给list兄弟组件传递字母 this.$bus.$emit("change", letter); }, // 触摸刚开始 handleTouchStart() { this.touchStatus = true; }, // 在触摸中 handleTouchMove(e) { // 获取A的位置,字母A到当前城市的距离,74 const startY = this.$refs.A[0].offsetTop; // 获取鼠标滑动到当前字母到当前城市的距离, 79为header的高度 const endY = e.touches[0].clientY - 79; // 计算字母在数组letters的索引,向下取整, 20为每个字母的高度 let index = Math.floor((endY - startY) / 20); if (index >= 0 && index < this.letters.length) { this.$bus.$emit("change", this.letters[index]); } console.log(index); }, // 触摸结束 handleTouchEnd() { this.touchStatus = false; } }, props: ["cities"] }; </script>
mounted() { this.__initScroll(); this.$bus.$on("change", this.handleLetterChange); }, // 获取兄弟组件的字母数据 handleLetterChange(letter) { if (letter) { // 获取每个城市列表的节点,letter是变量 // const cityNode = this.$refs[letter] const cityNode = this.$refs[letter][0]; // console.log(cityNode) this.BSscroll.scrollToElement(cityNode); } }
此时触摸事件的性能极差,我们需要优化代码,两方面优化
1.每次鼠标滑动,都要计算一次A的top值,浪费性能。刚开始cities是空数组传进来,之后异步请求数据后,cyties才有数据传过来,可以在update跟新钩子函数时,只计算一次A的
top值即可
2.可以采用节流,利用setTimeout()
在data中定义数据
data() { return { touchstatus: false, startY: 0, timer: null }; },
update钩子中
updated() { // 获取A的位置,74 this.startY = this.$refs.A[0].offsetTop; },
// 在触摸中 handleTouchMove(e) { // 每隔16毫秒,重新开启触摸事件,节流 if (this.timer) { clearTimeout(this.timer); this.timer = null; } this.timer = setTimeout(() => { // 获取鼠标滑动到字母的位置 const endY = e.touches[0].clientY - 79; // 计算字母在数组letters的索引 let index = Math.floor((endY - this.startY) / 20); if (index >= 0 && index < this.letters.length) { this.$bus.$emit("change", this.letters[index]); } }, 16); },
此时,性能最大化了
6.实现一个需求,输入地名,底部会展示城市列表数据
逻辑分析,输入框输入关键字keyword
1.cities是一个对象,我们for循环对象,得到属性名,在forEach循环属性值(数组),判断keyword是不是在数组里头。定义空数组res,将刷选成功的城市对象push到res数组中,
然后赋值给list
2.监视list数组,创建scroll
3.用v-show判断是否显示搜索的城市,以及是否显示没有匹配到数据字段
<template> <div> <div class="search"> <input type="text" class="search-input" placeholder="输入城市名或拼音" v-model="keyword" /> </div> <div class="search-content" ref="search" v-show="keyword"> <ul> <li class="search-item border-1px-bottom" v-for="(item, index) in list" :key="item.id" > {{ item.name }} </li> <li class="search-item border-bottom" v-show="hasNoData"> 没有找到匹配数据 </li> </ul> </div> </div> </template> <script> import BSscroll from "better-scroll"; export default { data() { return { keyword: "", list: [], timer: null }; }, props: ["cities"], computed: { hasNoData() { return !this.list.length; } }, mounted() { this.__initscroll(); }, methods: { __initscroll() { this.$nextTick(() => { if (!this.SearchScroll) { this.SearchScroll = new BSscroll(this.$refs.search); console.log("bbb"); } else { this.SearchScroll.refresh(); } }); } }, watch: { // 监视keyword,节流搜索 keyword(newVal) { const { keyword, cities } = this; if (this.timer) { clearTimeout(this.timer); this.timer = null; } if (!keyword) { this.list = []; } this.timer = setTimeout(() => { const res = [];
//i是属性名,item为对象 for (let i in cities) { cities[i].forEach(item => { if ( item.spell.indexOf(keyword) !== -1 || item.name.indexOf(keyword) !== -1 ) { res.push(item); } }); } this.list = res; // 有list数据,就可以开启滚动 this.__initscroll(); }, 100); } } }; </script> <style scoped lang="stylus"> @import '~assets/style/mixins.styl' @import '~assets/style/varibles.styl' .search height: .72rem padding: 0 .1rem background: $bgColor .search-input box-sizing: border-box width: 100% height: .62rem padding: 0 .1rem line-height: .62rem text-align: center border-radius: .06rem color: #666 .search-content z-index: 1 overflow: hidden position: absolute top: 1.58rem left: 0 right: 0 bottom: 0 background: #eee .search-item line-height: .62rem padding-left: .2rem background: #fff color: #666 </style>
7,实现一个需求,在热门城市或字母排序的城市,点击该城市,当前城市的名称改变,Home组件的城市名称也改变,用vuex实现数据管理
点击城市
<!-- 热门城市 --> <div class="area"> <div class="title border-1px-bottom border-1px-top">热门城市</div> <div class="button-list "> <div class="button-wrapper" v-for="(hot, index) in hotCities" :key="hot.id" @click="handleCityClick(hot.name)" > <div class="button">{{ hot.name }}</div> </div> </div> </div> <!-- 排序城市 --> <div class="area" v-for="(items, key) in cities" :key="key" :ref="key"> <div class="title border-1px-bottom border-1px-top">{{ key }}</div> <div class="item-list " v-for="(item, index) in items" :key="item.id" @click="handleCityClick(item.name)" > <div class="item border-1px-bottom"> {{ item.name }} </div>
通过mutations关联vuex
methods: { handleCityClick(city) { this.changeCity(city); this.$router.push("/"); }, ...mapMutations(["changeCity"]),
vuex中
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default new Vuex.Store({ state: { city: "上海" }, mutations: { changeCity(state, city) { state.city = city; } }, actions: {}, modules: {} });
在home组件中
<router-link to="/city"> <div class="header-right"> {{city}} <span class="iconfont iconarrow-down-filling"></span> </div> </router-link>
computed: { ...mapState(['city']) }
在list组件中
<!-- 当前城市 --> <div class="area"> <div class="title border-1px-bottom border-1px-top">当前城市</div> <div class="button-list "> <div class="button-wrapper"> <div class="button">{{ currentCity }}</div> </div> </div> </div>
computed: { ...mapState({ currentCity: "city" }) },
此时在vuex中存储的city,不是永久的,刷新下网页,city还是默认的数据,我们借助localstorage实现永久存储,优化代码
某些浏览器用户关闭了存储功能,或者隐身模式,导致localstorage失效,需要try,catch下,(重要)
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); 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) {} } }, actions: {}, modules: {} });
8, 当在home组件切换到city组件,需要发送请求,city组件返回到home组件也要发送请求,获取数据,此时浪费性能了
需要用到keep-alive组件保存组件,在app组件中设置
<template> <div> <!-- <Home></Home> --> <keep-alive> <router-view></router-view> </keep-alive> </div> </template>
9. 此时在city组件中选中不同的城市,自动返回到home组件,因为keep-alive影响,不会发生请求了,但是我们需要在切换不同的城市时,需要发送不同城市的请求,
keep-alive Props: include - 字符串或正则表达式。只有名称匹配的组件会被缓存。 exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。 max - 数字。最多可以缓存多少组件实例。 用法: <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。 当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。 在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。 主要用于保留组件状态或避免重新渲染。
此时需要用到activated钩子函数
逻辑分析,1.从vuex中获取切换到的city, 在mouted钩子函数保存第一次渲染的lastcity(keep-alive影响),请求ajax,加上个city的prama参数,
2.当切换的城市变更后(keep-alive影响),自动触发activated钩子,在次发送新的ajax请求
3.关键点activated钩子, lastcity和city判断
data() { return { swiperList: [], iconList: [], recommendList: [], weekendList: [], // 保存切换不同的城市 lastCity: "" }; }, mounted() {
//第一次渲染,将第一个城市保存,应为keep-alive的影响,该钩子函数只执行一次 this.lastCity = this.city; this.reqdata(); }, computed: { ...mapState(["city"]) }, // 使用了keep-alive组件,切换后,自动触发activated钩子 activated() { // 判断第一次渲染的最后一个城市与当前切换的城市 if (this.lastCity !== this.city) { this.lastCity = this.city; this.reqdata(); } }, methods: { async reqdata() { // const { data: res } = await axios.get("/api/elm"); const { data: res } = await axios.get(`/api/elm?city=${this.city}`); // console.log(res) if (res.error === 0) { this.swiperList = res.data.swiperList; this.iconList = res.data.iconList; this.recommendList = res.data.recommendList; this.weekendList = res.data.weekendList; } } },