Vue, App与我(三)
Vue, App与我(三)index.html文件的书写完成之后,也就意味着我们的首页展示框已经写好了,接下来需要写一下app.vue的内容。这部分也就是<div id="app"></div>中展示的内容。app.vue: 如果你想简单的进行实现这部分代码,可以写一个测试代码。<template><div><div class="page-topnav">
·
Vue, App与我(三)
index.html
文件的书写完成之后,也就意味着我们的首页展示框已经写好了,接下来需要写一下app.vue
的内容。这部分也就是<div id="app"></div>
中展示的内容。
app.vue
: 如果你想简单的进行实现这部分代码,可以写一个测试代码。
<template>
<div>
<div class="page-topnav">
<div class="com-top-nav ">
<p>首页</p>
<div class="rigth-btn">
<router-link :to="{path:'/search/1/',query:{tab:'hide'}}" class="ico sea"></router-link>
<router-link :to="{path:'/user/'}" class="ico user"></router-link>
</div>
</div>
<div class="h15 dn"></div>
<div class="hbot dn"></div>
</div>
<div class="banner pt45">
<mt-swipe :auto = "3000" class="swipe" v-if="ads">
<template v-for="ad in ads">
<mt-swipe-item class="swipe-item">
<a v-on:click = "changeClick(ad['id'], ad['url'])">
<img v-if="ad['picture']" : src="fullUrl(ad['picture']) + '!640.301'" />
</a>
</mt-swipe-item>
</template>
</mt-swipe>
<div class="icon-mess"><div class="red"></div></div>
</div>
</div>
</template>
- 代码解析:
<template></template>
单文件中用template标签包含html模板内容。但是很多时候会出现这样的误解:
<template>
<template></template>
</template>
- 这样的书写会存在异常报错,
ERROR in ./~/vue-loader/lib/template-compiler.js?id=data-v-556c4717!./~/vue-loader/lib/selector.js?type=template&index=0!./src/components/xxx.vue
。
template syntax error Cannot use <template> as component root element because it may contain multiple nodes: - 组件只能有一个根元素,就是你这了第一个下面必须只有一个根元素, 但是可以用容器元素将它包括起来。
<template>
<div>
<template></template>
</div>
</template>
<router-link></router-link>
: 组件支持用户在具有路由功能的应用中进行导航。通过to
属性指定目标地址,页面加载时默认渲染为<a>
标签元素, 如果你使用过react
进行开发的话,react-router
中的<Link>
也就类似于它。但是router-link
可以通过配置tag
将它转换为其他标签的。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的CSS类名。<router-link>
比起<a>
的好处是什么?
- 无论是Html5 history模式还是hash模式,它的表现行为一致,所以,当你要切换路由模式,或者在IE9以下降级使用hash模式,无须做任何变动。
- 在Html5 history模式下,
router-link
会拦截点击事件, 让浏览器不在重新加载页面。 - 当你在Html5 history模式下使用
base
选项之后, 所有的to
属性都不需要写(基路经)了。
- 至于
<router-link>
的props
: 请参看vue-router-link的论述。
query: {tab : 'hide'}
: query 后的键值被放在url中,形式类似以get,明文可以。params 的键值对在请求头header中可以查看到,不放在url中。
<mt-swipe></mt-swipe>
:
mint-ui
组件库中swipe
组件,目的为了实现常见的轮播图效果。但是它的实现与常见的实现不同。常见的实现方式:通过轮播图的wrapper
来实现item
的切换效果,(也就是修改wrapper
的translate3d
属性来进行实现)。如果支持循环播放,需要在首部插入一个最后一个轮播轮播图的item
的clone
版, 以及在尾部插入一个第一个轮播图的clone
版。swipe
组件实现的方式: 只显示当前显示的轮播图item
, 当切换的时候, 显示处当前item
的前后相邻的两个item; 通过设置三个item的translate3d
来实现切换的效果。- 这两种方式对比:
- 第一种方式, 初始会渲染出来所有的
item
, 通过translate3d
来实现切换和滑动, 这种方式会启动硬件加速提升性能。但是毕竟是在所有轮播图上进行渲染的。 - 第二种方式,通过切换item的display属性来实现对应item的显示和隐藏,虽然会引起回流和重绘,但是每个item的position为absolute,脱离文档流,所以并不会引起其他dom的回流和重绘。每个item的translate3d引发的渲染只是在当前item的基础上。
- 通过上面分析,可以得出: 如果轮播图的数量不多,第一种方式不会引起回流和重绘,并且translate引发渲染的item不多,性能相对好;但是轮播图的数量比较多的话,第二种性能相对比较好。
swipe
接入实例:
<div class="app">
<div class="swipe-wrapper">
<mt-swipe : auto="0" ref="swipeWrapper">
<mt-swipe-item class="swipe-item-1 item">1</mt-swipe-item>
<mt-swipe-item class="swipe-item-2 item">2</mt-swipe-item>
<mt-swipe-item class="swipe-item-3 item">3</mt-swipe-item>
</mt-swipe>
</div>
<div class="button-wrappper">
<button class="prev-button flex-item" @click="prev">prev</button>
<button class="prev-button flex-item" @click="next">next</button>
</div>
</div>
- css代码:
<link rel="stylesheet" href="../css/mint-style.css">
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#app {
width: 100%;
height: 100%;
}
.swipe-wrapper {
width: 100%;
height: 100%;
}
.swipe-item-1 {
background: #ff0000;
}
.swipe-item-2 {
background: #0000ff;
}
.swipe-item-3 {
background: #347C17;
}
.item {
text-align: center;
font-size: 40px;
color: #ffffff;
}
.button-wrapper {
display: flex;
height: 100px;
}
.flex-item {
flex: 1;
display: inline-block;
text-align: center;
height: 100%;
line-height: 100%;
font-size: 40px;
}
.prev-button {
background: #000000;
}
.next-button {
background: #347C17;
}
</style>
- js代码:
<!-- 先引入vue -->
<script src="../js/vue.js"></script>
<!--- 引入组件库 -->
<script src="../js/index.js"></script>
<script>
new Vue ({
el: '#app',
methods: {
prev: function () {
this.$refs.swipeWrapper.prev();
console.log(this.$children);
},
next: function () {
this.$refs.swipeWrapper.next();
}
}
});
</script>
- 原理解析:
- 初始只显示选中的
index
的item
, 将其他item隐藏。 - 当拖动开始的时候, 显示当前的
index
的相邻两个item
。 - 当拖动的时候,计算出手指滑动的距离, 通过设置当前的item和其相关的两个item的
translate3d
来改变他们位置的方式, 来实现切换的效果。 - 自动播放:通过设置定时器,触发上面拖动相同的切换代码,来实现切换。
- 源码解析:
- 首先需要查看子组件的swipe-item组件, 代码阅读起来倒不是很难,如下:
<template>
<div class="mint-swipe-item">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'mt-swipe-item',
mounted () { //页面显示时触发
this.$parent && this.$parent.swipeItemCreated(this); // 内部实现: 调用父组件的`reinit()`业务
},
destroyed() { // item隐藏时触发
this.$parent && this.$parent.swipeItemDestroyed(this); // 内部实现同上
}
};
</script>
- 上面的这一部分源码,需要进行解析一下,其实就是分为
mounted
和destroyed
, 并且这两部分都存在一个共同点,那就都是调用的父组件的实现。实现如下:
swipeItemCreated () {
if (!this.ready) return;
clearTimeout(this.reInitTimer);
this.reInitTimer = setTimeout (()=>{
this.reInitPages();
}, 100);
},
swipeItemDestroyed() {
if (!this.ready) return;
clearTimeout(this.reInitTimer);
this.reInitTimer = setTimeout (() =>{
this.reInitPages();
}, 100);
},
- 父组件
swipe
的props
, 这一类的数据都是允许从外部导入的,而对于数据部分,这里就不再就行阐述了。详细swipe。
swipe
的data
说明:
data() {
return {
ready: false, //当前组件是否mounted
dragging: false, //当前是否在拖动
userScrolling: false, //判定当前用户在上下滚动,就不执行dragg。
animation: false, //当前是否执行动画(也就是自动切换动画)。
index: 0, // 当前所在的item的index
pages: [], // 存储当前child的dom
timer: null, // 自动播放的定时器timerid
reInitTimer: null, // item组件自动触发reInit触发定时器id
noDrag: false, // 存储是否运动拖动的标识
isDone: false, // 当前动画是否执行完成
}
}
- 上面源码中的不存在注释, 注释的内容为分析源码之后,它的功能说明。
- swipe组件的入口函数mounted回调的实现:
mounted () {
this.ready = true;
this.initTimer(); //初始化自动化播放的timer
this.reInitPages(); //初始化drag状态,以及dom节点的样式信息
var element = this.$el;
// 为当前的dom节点,注册touch时间
element.addEventListener('touchstart', (event) => {
if (this.prevent) {
event.preventDefault();
}
if (this.stopPropagation) {
event.stopPropagation;
}
if (this.animation) return; // 如果当前在执行移动动画,直接返回。
this.dragging = true; //设置dragging的状态意识
this.userScrolling = false; //重置userScrolling的状态
this.doOnTouchStart(event);
});
element.addEventListener('touchmove', (event) => {
if(this.userScrolling) {//纵向滚动
this.dragging = false;
this.State = {};
return;
}
if (!this.dragging) {
return;
}
this.initTimer; // 启动自动播放定时器
this.doOnTouchEnd(event);
this.dragging = false; //重置拖动状态
});
}
- 关于初始化自动播放器的定时器代码,最后会进行分析。现在需要先来看一下dom样式的reInitPages函数实现如下:
reInitPages() {
var children = this.$children;
// 设置拖动状态
this.noDrag = children.length === 1 && this.noDragWhenSingle; // 当前只有一个item,并且设置了只有一个不支持拖动
var pages = [];
var intDefaultIndex = Math.floor(this.defaultIndex);
var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) ? intDefaultIndex : 0;
this.index = defaultIndex; // 设置当前显示的索引值
//初始化显示样式, 将当前index的item显示出来,其他的都隐藏
children.forEach(function(child, index) {
pages.push(child.$el);
removeClass(child.$el, 'is-active');
if (index === defaultIndex) {
addClass(child.$el, 'is-active');
}
});
// 设置所有轮播图的item的dom
this.pages = pages;
},
- swipe的
touchstart
事件回调的处理 - 上面已经有了回调的代码,主要看处理的核心函数doOnTouchStart的实现如下:
doOnTouchStart(event) { // 创建dragState, 包括touch事件的信息,当前drag item以及它前后两个item,并将其显示出来
if (this.noDrag) return; // 不支持拖动
var element = this.$el;
var dragState = this.dragState;
var touch = event.touches[0];
// 设置dragstate的信息(也就是当前滑动的信息数据)
dragState.startTime = new Date();
dragState.startLeft = touch.pageX;
dragState.startTop = touch.pageY;
dragState.startTopAbsolute = touch.clientY;
dragState.pageWidth = element.offsetWidth;
dragState.pageHeight = element.offsetHeight;
var prevPage = this.$children[this.index - 1];
var dragPage = this.$children[this.index];
var nextPage = this.$children[this.index + 1];
if (this.continuous && this.pages.length > 1) { // 当前支持循环播放, 并且pages的长度大于1
if (!prevPage) {
prevPage = this.$children[this.$children.length - 1];
}
if (!nextPage) {
nextPage = this.$children[0];
}
}
dragState.prevPage = prevPage ? prevPage.$el : null;
dragState.dragPage = dragPage ? dragPage.$el : null;
dragState.nextPage = nextPage ? nextPage.$el : null;
// 将当前index下的前后两个item显示出来
if (dragState.prevPage) {
dragState.prevPage.style.display = 'block';
}
if (dragState.nextPage) {
dragState.nextPage.style.display = 'block';
}
}
获取当前touchstart状态下面的拖动的状态信息(包括touch的信息,页面宽高,
prev、current、next
三个item
的dom
)。同时将prev、next显示出来。touchmove
事件回调的处理
doOnTouchMove(event) {
if (this.noDrag) return;
var dragState = this.dragState;
var touch = event.touches[0];
dragState.currentLeft = touch.pageX;
dragState.currentTop = touch.pageY;
dragState.currentTopAbsolute = touch.clientY;
//计算滑动的距离
var offsetLeft = dragState.currentLeft - dragState.startLeft;
var offsetTop = dragState.currentTopAbsolute - dragState.startTopAbsolute;
var distanceX = Math.abs(offsetLeft);
var distanceY = Math.abs(offsetTop);
// 判断是 竖向滚动,还是横向滚动
if (distanceX < 5 || (distanceX >= 5 && distanceY >= 1.73 * distanceX)) {
this.userScrolling = true; // 判定当前用户在上下滚动,就不执行drag动作
return;
} else {
this.userScrolling = false;
event.preventDefault(); // 阻止默认事件的触发,也就是点击事件的触发
}
// 设置最大的拖拽距离在当前dom里面
offsetLeft = Math.min(Math.max(-dragState.pageWidth + 1, offsetLeft), dragState.pageWidth - 1);
var towards = offsetLeft < 0 ? 'next' : 'prev'; // 拖动的方向的确定
//prev方向: prev dom移动到指定的位置
if (dragState.prevPage && towards === 'prev') {
this.translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
}
// current dom移动到指定的位置
this.translate(dragState.dragPage, offsetLeft);
// next方向: next dom 移动到指定的位置
if (dragState.nextPage && towards === 'next') {
this.translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
}
}
- 主要确定当前滚动不是竖向滚动,并确定滚动的方向以确定移动prev还是next。
- 下面看translate移动dom的核心函数实现:
/**
* @param element 要移动的dom节点
* @param offset // dom移动的距离
* @param speed 如果传递, 执行动画的移动; 没有,则直接translate执行的距离
* @param callback 处理完成的回调函数
*/
translate(element, offset, speed, callback) {
if (speed) {
this.animating = true; // 当前正在执行动画,此时不能拖拽
element.style.webkitTransition = '-webkit-transform ' + speed + 'ms ease-in-out'; // transition过渡状态
setTimeout(() => {
element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
}, 50);
var called = false;
var transitionEndCallback = () => {
if (called) return;
called = true;
this.animating = false; // 停止动画
element.style.webkitTransition = '';
element.style.webkitTransform = '';
if (callback) {
callback.apply(this, arguments); // 调用回调
}
};
once(element, 'webkitTransitionEnd', transitionEndCallback); // 此事件只执行一次
// 防止低版本android, 无法触发此事件
setTimeout(transitionEndCallback, speed + 100); // webkitTransitionEnd maybe not fire on lower version android.
} else {
element.style.webkitTransition = '';
element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
}
}
- 如果设置了speed,就会执行平滑的动画切换(speed是动画执行的时间);如果没有设置,直接移动到指定的位置,没有过渡效果。
touchend
事件回调的实现- 分析其中核心代码doTouchEnd函数:
doOnTouchEnd() {
if (this.noDrag) return;
var dragState = this.dragState;
var dragDuration = new Date() - dragState.startTime;
var towards = null; // 决定下面进入哪个页面, null: 当前页面, prev: 前一个页面, next: 下一个页面
var offsetLeft = dragState.currentLeft - dragState.startLeft;
var offsetTop = dragState.currentTop - dragState.startTop;
var pageWidth = dragState.pageWidth;
var index = this.index;
var pageCount = this.pages.length;
// 判断当前是否是 tap事件(轻触事件)
if (dragDuration < 300) {
let fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop) < 5;
if (isNaN(offsetLeft) || isNaN(offsetTop)) {
fireTap = true;
}
if (fireTap) {
this.$children[this.index].$emit('tap'); // 当前轮播图item发送给外部的tab事件
}
}
// 触发时长小于300ms,并且没有执行touchmove事件, 不处理
if (dragDuration < 300 && dragState.currentLeft === undefined) return;
if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
towards = offsetLeft < 0 ? 'next' : 'prev';
}
if (!this.continuous) { // 当前不支持循环, 向前或向后 都回到当前页面
if ((index === 0 && towards === 'prev') || (index === pageCount - 1 && towards === 'next')) {
towards = null;
}
}
if (this.$children.length < 2) {
towards = null;
}
// 动画的方式切换到指定的item
this.doAnimate(towards, {
offsetLeft: offsetLeft,
pageWidth: dragState.pageWidth,
prevPage: dragState.prevPage,
currentPage: dragState.dragPage,
nextPage: dragState.nextPage
});
this.dragState = {};// 清空dragState
}
- 判断当前是否是tap事件,并且确定下面要切换到哪个item。
- 下面来看
doAnimate
动画的方式切换的实现:
doAnimate(towards, options) {
if (this.$children.length === 0) return;
if (!options && this.$children.length < 2) return;
var prevPage, nextPage, currentPage, pageWidth, offsetLeft;
var speed = this.speed || 300;
var index = this.index;
var pages = this.pages;
var pageCount = pages.length;
if (!options) { // 没有options,是 自动播放或手动触发切换页面的处理
pageWidth = this.$el.clientWidth;
currentPage = pages[index];
prevPage = pages[index - 1];
nextPage = pages[index + 1];
if (this.continuous && pages.length > 1) {
if (!prevPage) {
prevPage = pages[pages.length - 1];
}
if (!nextPage) {
nextPage = pages[0];
}
}
// 将 prevPage 和 nextPage 定位到应该的位置(也就是开始执行切换页面的位置)
if (prevPage) {
prevPage.style.display = 'block'; // 显示出来
this.translate(prevPage, -pageWidth); // 移到当前index的前面
}
if (nextPage) {
nextPage.style.display = 'block';
this.translate(nextPage, pageWidth); // 移到当前index的后面
}
} else {
prevPage = options.prevPage;
currentPage = options.currentPage;
nextPage = options.nextPage;
pageWidth = options.pageWidth;
offsetLeft = options.offsetLeft;
}
// 确定 要切换的item的索引
var newIndex;
var oldPage = this.$children[index].$el;
if (towards === 'prev') {
if (index > 0) {
newIndex = index - 1;
}
if (this.continuous && index === 0) {
newIndex = pageCount - 1;
}
} else if (towards === 'next') {
if (index < pageCount - 1) {
newIndex = index + 1;
}
if (this.continuous && index === pageCount - 1) {
newIndex = 0;
}
}
var callback = () => { // 动画完成的回调: 重置dom的样式信息
if (newIndex !== undefined) {
// 重置dom的样式信息
var newPage = this.$children[newIndex].$el;
removeClass(oldPage, 'is-active'); // is-active 设置当前item的display:block
addClass(newPage, 'is-active');
this.index = newIndex;
}
if (this.isDone) { // 切换了页面,向外部发送切换页面完成的事件
this.end();
}
// 在touchStart 时设置的style中的display清空, 也就是使用class里面的display:none隐藏属性
if (prevPage) {
prevPage.style.display = '';
}
if (nextPage) {
nextPage.style.display = '';
}
};
setTimeout(() => {
if (towards === 'next') { // 切换到下一页
this.isDone = true;
this.before(currentPage); // 执行切换页面之前,向外部发送事件
this.translate(currentPage, -pageWidth, speed, callback);
if (nextPage) {
this.translate(nextPage, 0, speed);
}
} else if (towards === 'prev') { // 切换到上一页
this.isDone = true;
this.before(currentPage);
this.translate(currentPage, pageWidth, speed, callback);
if (prevPage) {
this.translate(prevPage, 0, speed);
}
} else { // 回到当前页面,不切换页面
this.isDone = false; // 当前没有进入到前一个页面和后一个页面, 还是回到当前页面
this.translate(currentPage, 0, speed, callback);
if (typeof offsetLeft !== 'undefined') {
if (prevPage && offsetLeft > 0) {
this.translate(prevPage, pageWidth * -1, speed);
}
if (nextPage && offsetLeft < 0) {
this.translate(nextPage, pageWidth, speed);
}
} else {
if (prevPage) {
this.translate(prevPage, pageWidth * -1, speed);
}
if (nextPage) {
this.translate(nextPage, pageWidth, speed);
}
}
}
}, 10);
}
- 此函数代码比较,但是不难,结合上面的注释,应该很容易读懂。实现:如果没有options(手动触发切换页面),会生成options中的信息(也就是下面处理需要用到的数据),并把prev和next两个dom定位到指定的位置;然后执行切换页面的操作,并且在结束的回调中重置相应dom的样式以及当前选中的index。
- 下面来看 手动触发的切换页面的代码
next() { // 切换到下一个页面
this.doAnimate('next');
},
prev() { // 切换到上一个页面
this.doAnimate('prev');
}
- 最后,来看自动播放的代码:
initTimer() {
if (this.auto > 0) {
this.timer = setInterval(() => {
// 如果不支持循环播放,并且当前播放到了 末尾位置, 停止定时器
if (!this.continuous && (this.index >= this.pages.length - 1)) {
return this.clearTimer();
}
if (!this.dragging && !this.animating) { // 没有在拖动, 也没有执行动画
this.next(); // 播放下一个item
}
}, this.auto);
}
}
- 读懂了
doAnimate
函数了,就很简单了。
- 总结:
- 1、了解了一种新的轮播图的实现方式
- 2、两个轮播图的实现方式的差别以及性能的比较
- 3、
touch
事件边界条件的处理,比如tap事件的判断,横纵向滚动的判断
Jackdan9 Thinking
更多推荐
已为社区贡献3条内容
所有评论(0)