一, 布局 Flex
Flex 布局,可以简便,完整,响应式地实现各种页面布局,Flex 是 Flexible Box 的缩写,意为 "弹性布局",用来为盒状模型提供最大的灵活性.任何一个容器都可以指定为 Flex 布局.
// 指定为 Flex 布局
display: flex;
// 主要属性
flex: none | [ < 'flex-grow' > <'flex-shrink' > ?||<'flex-basis' > ]
flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto.后两个属性可选.
flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大
flex-shrink属性定义项目的缩小比例,默认为1,即如果空间不足,该项目将缩小,flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size).浏览器根据这个属性,计算主轴是否有多余空间.它的默认值为auto,即项目的本来大小,设为跟width或height属性一样的值(比如350px),则项目将占据固定空间
flex : 等分 内容缩放 展位空间;
flex : 0 0 80px
Flex 语法 Flex 实践
二,图标组件
子组件 iconMap
<template lang="html">
<span class="iconMap" :class="iconClassMap[iconType]"></span>
</template
export default {
props: { // 图标类型
iconType: Number
},
created() { // 数组类名
this.iconClassMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']
}
}
父组件 goods
import iconMap from '../iconMap/iconMap' // 注意路径写法
// 注册组件
components: {
iconMap
}
<ul>
<li v-for='(item,index) in goods' class="menu-item">
<span class="text"> // json 数据 根据 type 判断 是否有图标
<iconMap v-show="item.type>0" :iconType="item.type"></iconMap>
{{item.name}}
</span>
</li>
</ul>
三,better-scroll 应用
类似 iscroll 实现滚动效果
安装
npm install better-scroll
引入
import BScroll from 'better-scroll'
说明
(1)原理:父容器 wrapper,它具有固定的高度,当它的第一个子元素 content 的高度超出了 wrapper 的高度,我们就可以滚动内容区了,若没有超出则不能滚动了.
(2)better-scroll 的初始化
better-scroll 的初始化时机很重要,因为它在初始化的时候,会计算父元素和子元素的高度和宽度,来决定是否可以纵向和横向滚动.因此,我们在初始化它的时候,必须确保父元素和子元素的内容已经正确渲染了.如果子元素或者父元素 DOM 结构发生改变的时候,必须重新调用 scroll.refresh() 方法重新计算来确保滚动效果的正常.所以 better-scroll 不能滚动的原因多半是初始化 better-scroll 的时机不对,或者是当 DOM 结构发送变化的时候并没有重新计算 better-scroll.
(3)better-scroll 结合 vue
vue.js 提供了我们一个获取 DOM 对象的接口-- vm.$refs.在这里,我们通过了 this.$refs.wrapper 访问到了这个 DOM 对象,并且我们在 mounted 这个钩子函数里,this.$nextTick 的回调函数中初始化 better-scroll .因为这个时候,wrapper 的 DOM 已经渲染了,我们可以正确计算它以及它内层 content 的高度,以确保滚动正常.
这里的 this.$nextTick 是一个异步函数,为了确保 DOM 已经渲染,底层用到了 MutationObserver 或者是 setTimeout(fn, 0).其实我们在这里把 this.$nextTick 替换成 setTimeout(fn, 20) 也是可以的(20 ms 是一个经验值,每一个 Tick 约为 17 ms),对用户体验而言都是无感知的.
(4)异步数据的处理
在我们的实际工作中,列表的数据往往都是异步获取的,因此我们初始化 better-scroll 的时机需要在数据获取后,代码如下:
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
data() {
return {
data: []
}
},
created() {
requestData().then((res) => {
this.data = res.data
this.$nextTick(() => {
this.scroll = new Bscroll(this.$refs.wrapper, {})
})
})
}
}
</script>
这里的 requestData 是伪代码,作用就是发起一个 http 请求从服务端获取数据,并且这个函数返回的是一个 promise(实际项目中我们可能会用 axios 或者 vue-resource ).我们获取到数据的后,需要通过异步的方式再去初始化 better-scroll,因为 Vue 是数据驱动的, Vue 数据发生变化(this.data = res.data)到页面重新渲染是一个异步的过程,我们的初始化时机是要在 DOM 重新渲染后,所以这里用到了 this.$nextTick,当然替换成 setTimeout(fn, 20) 也是可以的.
注意:这里为什么是在 created 这个钩子函数里请求数据而不是放到 mounted 的钩子函数里?因为 requestData 是发送一个网络请求,这是一个异步过程,当拿到响应数据的时候,Vue 的 DOM 早就已经渲染好了,但是数据改变 -> DOM 重新渲染仍然是一个异步过程,所以即使在我们拿到数据后,也要异步初始化 better-scroll.
使用
初始化需要滚动的 dom 结构
借助 ref 属性用来绑定某个 dom 元素,或者来说来绑定某个组件,然后在函数内用 this.$refs.menuwrapper 获取到 dom.
说明:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子组件上,引用就指向组件实例:
<div class="menu-wrapper" ref='menuWrapper'> </div>
<div class="foods-wrapper" ref="foodsWrapper"></div>
在 ajax 内执行_initScroll() 函数在此之前我们要做一些准备和注意事项
(1) dom 结构完全加载完再调用_initScroll() 方法才会生效
(2) 因为要监听内容区域的高度,所以初始化应在 created 过程中去监听 dom 结构是否完全加载,这里是在 $nextTick 对象中进行触发检测
ES6 语法格式:this.$nextTick(() => {})
created() { // 在实例创建完成后被立即调用 $el 属性目前不可见.
axios.get('static/data.json').then((result) = >{
this.goods = result.data.goods
//dom结构加载结束
this.$nextTick(() = >{
this._initScroll(); // 初始化scroll
})
})
}
(3) 在 methods 方法里面定义一个_initScroll 的函数,主要用来对左右两侧 dom 结构进行初始化
methods: {
// 用来对左右两侧dom结构进行初始化
_initScroll() {
// 实例化 better-scroll 插件,传入要滚动的DOM 对象
this.meunScroll = new BScroll(this.$refs.menuWrapper, {
click: true
});
this.foodScroll = new BScroll(this.$refs.foodsWrapper, {
click: true
});
}
}
说明:vue 中更改数据,DOM 会跟着做映射,但 vue 更新 DOM 是异步的,用 $nextTick () 来确保 Dom 变化后能调用到_initScroll() 方法.调用_initScroll() 方法能计算内层 ul 的高度,当内层 ul 的高度大于外层 wrapper 的高度时,可以实现滚动.
此时俩侧可以分别滚动了!
(4) 实现左右联动
原理:我们计算出右侧实时变化的 y 值,落到哪一个区间,我们就显示那一个区间.首先我们要计算整体区间的一个高度,然后分别计算第一个区间的高度,第二个区间的高度,以此类推.然后将区间数存入一个定义好的数组.当我们在滚动的时候实时拿到 y 轴的高度,然后对比在哪一个区间,这样我们就会得到一个区间的索引值去对应左侧的菜品类别,最后我们用一个 vue 的 class 去绑定高亮文本.
1. 定义一个方法在_initScroll 下面,作为计算高度的方法叫做_calculateHeight () ,再定义一个 listHeight:[] 数组,存放获取到的每一块 foods 类的高度.然后通过给每个 li 定义类名来供 js 选择 从而计算出高度存放到 listHeight 数组里.
// 通过 方法 计算foods内部每一个块的高度,组成一个数组listHeight.
// 每个li 定义一个类food-list-hook 通过获取该类 来计算 每一块的高度 存到数组listHeight里
_calculateHeight() {
// 获取 li 通过food-list-hook
let foodList = this.$refs.foodsWrapper.querySelectorAll(".food-list-hook");
let height = 0; // 初始化高度
this.listHeight.push(height) // 把第一个高度存入数组
//通过循环foodList下的dom结构,将每一个li的高度依次送入数组
for (let i = 0, l = foodList.length; i < l; i++) {
let item = foodList[i]; //每一个item都是刚才获取的food的每一个dom
height += item.clientHeight; //获取每一个foods内部块的高度
this.listHeight.push(height) // 将获取的值存放到数组里
}
}
2. 我们获取到区间高度数组后,我们要实时获取到右侧的 y 值,和左侧的索引值做一个对比,定义一个 scrollY 变量用来存放实时获取的 y 值.bs 插件为我们提供了一个实时获取 y 值的方法,我们在初始化 this.foodScroll 的时候加一个 . 属性 probeType: 3,其作用就是实时获取 y 值,相当于探针的作用.
goods: [],// goods json 数组
listHeight: [],// 存放 foods 内部的每一块的高度
scrollY:0
this.foodScroll=new BScroll(this.$refs.foodsWrapper,{
click:true,
//探针作用,实时监测滚动位置
probeType: 3
});
3. 我们再添加一个方法 this.foodScroll.on('scroll',(pos) => {}), 作用是实时滚动的时候把获取到的位置给暴露出来.代码如下.
//结合BScroll的接口使用,监听scroll事件(实时派发的),并获取鼠标坐标,当滚动时能实时暴露出scroll
this.foodScroll.on("scroll", (pos) = >{ // 回调函数
//scrollY接收变量
this.scrollY = Math.abs(Math.round(pos.y)) //滚动坐标会出现负的,并且是小数,所以需要处理一下,实时取得scrollY
// console.log(pos.y)
})
4. 定义一个计算属性 computed,获取到 food 滚动区域对应的 menu 区域的子块的索引 i 值,从而定位到左侧边栏的位置.
computed: {
currentIndex() { //计算到达哪个区域的区间的时候的对应的索引值
// 利用 listHeight 存放 每一块 对应的高度
for (let i = 0, l = this.listHeight.length; i < l; i++) {
let menuHeight_fir = this.listHeight[i] // 当前menu 子块区域的 高度
let menuHeight_sec = this.listHeight[i + 1] // 下一个menu 子块区域的 高度
// 当滑到底部时,menuHeight_sec 为 underfined,
// 需要确定滑到俩个高度区间
if (!menuHeight_sec || (this.scrollY > menuHeight_fir && this.scrollY < menuHeight_sec)) {
return i;
}
}
},
}
获取到 i 后,, 然后通过设置一个 class 来做样式切换变化 :class="{'current':currentIndex === index}", 当 currentIndex 和 menu-item 对应的 index 相等时,设置 current 的样式.这样就可以实现左右联动了.
<li v-for='(item,index) in goods' class="menu-item" :class="index === currentIndex?'menu-item-selected':'menu-item'">
...
在样式里提前设好 选中和正常的样式
5. 最后实现左侧点击的功能.在左侧的 li 下绑定一个 selectMenu 的点击事件,并传入索引值,这样我们就可以知道点击的是哪一个 li
<li v-for='(item,index) in goods' class="menu-item" @click="selectMenu(index,$event)" :class="index === currentIndex?'menu-item-selected':'menu-item'">
...
selectMenu (index, event){ // 点击左侧 ,右侧响应
this.foodScroll.scrollTo(0, -this.listHeight[index], 300)
}
scrollTo(x, y, time, easing)
滚动到某个位置,x,y 代表坐标,time 表示动画时间,easing 表示缓动函数
scroll.scrollTo(0, 500)
参考: vue 使用 better-scroll 的参数和方法
6. 关于在 selectMenu 中点击事件
在 selectMenu 中点击, 在 pc 界面会出现两次事件, 在移动端就只出现一次事件的问题
原因 : better-scroll 会监听事件 (例如 touchmove,click 之类), 并且阻止默认事件 (prevent stop), 并且他只会监听移动端的, pc 端的没有监听
在 pc 页面上 better-scroll 也派发了一次 click 事件, 原生也派发了一次 click 事件
// better-scroll 的事件,有_constructed: true
MouseEvent {isTrusted: false, _constructed: true, screenX: 0, screenY: 0, clientX: 0...}
//pc的事件
MouseEvent {isTrusted: true, screenX: -1867, screenY: 520, clientX: 53, clientY: 400...}
解决 : 针对 better-scroll 的事件, 有_constructed: true, 所以做处理, return 掉非 better-scroll 的事件
selectMenu(index, event) {
if (!event._constructed) { //去掉自带的click事件点击,即pc端直接返回
return;
}
let foodList = this.$refs.foodsWrapper.querySelectorAll(".food-list-hook"); // 获得监听元素
let el = foodList[index]; // 获得 当前 监听元素的高度
this.foodScroll.scrollToElement(el, 300); //类似jump to的功能,通过这个方法,跳转到指定的dom
}
goods 组件到此差不多了!
来源: https://sdk.cn/news/8046