这篇文章将介绍如何使用原生 JS (主要使用 ES6 语法)实现全屏滚动插件,兼容 IE 10+、手机触屏,Mac 触摸板优化,支持自定义页面动画,压缩后 gzip 文件只有 2k。完整源码在这 pure_full_page,点这查看 demo。
现在已经有很多全屏滚动插件了,比如著名的 fullPage,那为什么还要自己造轮子呢?
现有轮子有以下问题:
对比之下,通过原生语言造轮子有以下好处:
实现原理见下图:容器及容器内的页面取当前可视区高度,同时容器的父级元素 overflow 属性值设为 hidden,通过更改容器 top 值实现全屏滚动效果。
代码编写的思路是通过 class 定义全屏滚动类,使用时通过 new PureFullPage().init() 使用。
/**
* 全屏滚动类
*/
class PureFullPage {
// 构造函数
constructor() {}
// 原型方法
methods() {}
// 初始化函数
init() {}
}
鉴于上述实现原理,对于 html 的结构有特定要求,如下:页面容器为 #pureFullPageContainer,所有的页面为其直接子元素,这里为了方便,直接取 body 为其直接父元素。
<body>
<div id="pureFullPageContainer">
<div class="page"></div>
<div class="page"></div>
<div class="page"></div>
</div>
</body>
首先,容器及容器内的页面取当前可视区高度,为每次切换都显示一个完整的页面做准备;
第二,容器的父级元素(此处是 body) overflow 属性值定为 hidden,这样可以保证每次只会显示一个页面,其他页面被隐藏。
经过上述设置,对容器 top 值,每次更改一个可视区高度的距离,便实现了页面间的切换,部分代码如下:
body {
/* body 为容器直接的父元素 */
overflow: hidden;
}
#pureFullPage {
/* 只有当 position 的值不是 static 时,top 值才有效 */
position: relative;
/* 设置初始值 */
top: 0;
}
.page {
/* 此处不能为 100vh,后面详述 */
/* 其父元素,也就是 #pureFullPage 的高度,通过 js 动态设置*/
height: 100%;
}
Notice:
容器的 position 属性值需要设置为 relative,因为 top 只有在 position 属性值不为 static 时才有效;
页面高度需设置为当前可视区高度,但不能直接设置为 100vh,因为 safari 手机浏览器把地址栏算进去计算 100vh,但地址栏下面的不应该算做“可视区”,毕竟实际上是“看不见”的区域。这会导致 100vh 对应的像素值比 document.documentElement.clientHeight 获取的像素值大。这样在切换 top 值时就不是全屏切换了,实际上,这种情况下切换的高度小于页面的高度。
解决 safari 手机浏览器可视区高度问题:既然通过 js 获取的 document.documentElement.clientHeight值是符合预期的可视区高度(不包括顶部地址栏和底部工具栏),那就将该值通过 js 设置为容器的高度,同时,容器内的页面高度设置为 100%,这样就可以保证容器及页面的高度和切换 top 值相同了,也就保证了全屏切换。
// 伪代码
'#pureFullPage'.style.height = document.documentElement.clientHeight + 'px';
这里的滚动/滑动事件包括鼠标滚动、触摸板滑动以及手机屏幕上下滑动。
PC 端主要解决的问题是获取鼠标滚动或触摸板滑动方向,触摸板上下滑动和鼠标滚动绑定的是同一个事件:
macOS 如此,windows 相反?
所以,可以通过 detail 或 wheelDelta 的值判断鼠标的滚动方向,进而控制页面是向上还是向下滚动。在这里我们只关心正负,不关心具体值的大小,为了便于使用,下面基于这两个事件封装了一个函数:如果鼠标往前滚动,返回负数,反之,返回正数,代码如下:
// 鼠标滚轮事件
getWheelDelta(event) {
if (event.wheelDelta) {
return event.wheelDelta;
} else {
// 兼容火狐
return -event.detail;
}
},
有了滚动事件,就可以据此编写页面向上或者向下滚动的回调函数了,如下:
// 鼠标滚动逻辑(全屏滚动关键逻辑)
scrollMouse(event) {
let delta = utils.getWheelDelta(event);
// delta < 0,鼠标往前滚动,页面向下滚动
if (delta < 0) {
this.goDown();
} else {
this.goUp();
}
}
goDown、goUp 是页面滚动的逻辑代码,需要特别说明的是必须 判断滚动边界,保证容器中显示的始终是页面内容:
具体代码如下:
goUp() {
// 只有页面顶部还有页面时页面向上滚动
if (-this.container.offsetTop >= this.viewHeight) {
// 重新指定当前页面距视图顶部的距离 currentPosition,实现全屏滚动,
// currentPosition 为负值,越大表示超出顶部部分越少
this.currentPosition = this.currentPosition + this.viewHeight;
this.turnPage(this.currentPosition);
}
}
goDown() {
// 只有页面底部还有页面时页面向下滚动
if (-this.container.offsetTop <= this.viewHeight * (this.pagesNum - 2)) {
// 重新指定当前页面距视图顶部的距离 currentPosition,实现全屏滚动,
// currentPosition 为负值,越小表示超出顶部部分越多
this.currentPosition = this.currentPosition - this.viewHeight;
this.turnPage(this.currentPosition);
}
}
最后添加滚动事件:
// 鼠标滚轮监听,火狐鼠标滚动事件不同其他
if (navigator.userAgent.toLowerCase().indexOf('firefox') === -1) {
document.addEventListener('mousewheel', scrollMouse);
} else {
document.addEventListener('DOMMouseScroll', scrollMouse);
}
移动端需要判断是向上还是向下滑动,可以结合 touchstart(手指开始接触屏幕时触发) 和 touchend(手指离开屏幕时触发) 两个事件实现判断:分别获取两个事件开始触发时的 pageY 值,如果触摸结束时的 pageY 大于触摸开始时的 pageY,表示手指向下滑动,对应页面向上滚动,反之亦然。
此处我们需要触摸事件跟踪触摸的属性:
相关代码如下:
// 手指接触屏幕
document.addEventListener('touchstart', event => {
this.startY = event.touches[0].pageY;
});
//手指离开屏幕
document.addEventListener('touchend', event => {
let endY = event.changedTouches[0].pageY;
if (endY - this.startY < 0) {
// 手指向上滑动,对应页面向下滚动
this.goDown();
} else {
// 手指向下滑动,对应页面向上滚动
this.goUp();
}
});
为了避免下拉刷新,可以阻止 touchmove 事件的默认行为:
// 阻止 touchmove 下拉刷新
document.addEventListener('touchmove', event => {
event.preventDefault();
});
优化主要从两方便入手:
既然都是限制触发频率(都通过定时器实现),那这两者有什么区别?
首先,防抖动函数工作时,如果在指定的延迟时间内,某个事件连续触发,那么绑定在这个事件上的回调函数永远不会触发,只有在延迟时间内,这个事件没再触发,对应的回调函数才会执行。防抖动函数非常适合改变窗口大小这一事件,这也符合 拖动到位以后再触发事件,如果一直拖个不停,始终不触发事件 这一直觉。
而截流函数是在延迟时间内,绑定到事件上的回调函数能且只能触发一次,这和截流函数不同,即便是在延迟时间内连续触发事件,也不会阻止在延迟时间内有一个回调函数执行。并且截流函数允许我们指定回调函数是在延迟时间开始时还是结束时执行。
鉴于截流函数的上述两个特性,尤其适合优化滚动/滑动事件:
这里不介绍防抖动函数和截流函数的实现原理,感兴趣的可以看Throttling and Debouncing in JavaScript,下面是实现的代码:
// 防抖动函数,method 回调函数,context 上下文,event 传入的时间,delay 延迟函数
debounce(method, context, event, delay) {
clearTimeout(method.tId);
method.tId = setTimeout(() => {
method.call(context, event);
}, delay);
},
// 截流函数,method 回调函数,context 上下文,delay 延迟函数,
// immediate 传入 true 表示在 delay 开始时执行回调函数
throttle(method, context, delay, immediate) {
return function() {
const args = arguments;
const later = () => {
method.tID = null;
if (!immediate) {
method.apply(context, args);
}
};
const callNow = immediate && !method.tID;
clearTimeout(method.tID);
method.tID = setTimeout(later, delay);
if (callNow) {
method.apply(context, args);
}
};
},
《JavaScript 高级程序设计 - 第三版》 22.33.3 节中介绍的 throttle 函数和此处定义的不同,高程中定义的 throttle 函数对应此处的 debounce 函数,但网上大多数文章都和高程中的不同,比如 lodash 中定义的 debounce。
通过上述说明,我们已经知道截流函数可以通过限定滚动事件触发频率提升性能,同时,设置在延迟时间开始阶段立即调用滚动事件的回调函数并不会牺牲用户体验。
截流函数上文已经定义好,使用起来就很简单了:
// 设置截流函数
let handleMouseWheel = utils.throttle(this.scrollMouse, this, this.DELAY, true);
// 鼠标滚轮监听,火狐鼠标滚动事件不同其他
if (navigator.userAgent.toLowerCase().indexOf('firefox') === -1) {
document.addEventListener('mousewheel', handleMouseWheel);
} else {
document.addEventListener('DOMMouseScroll', handleMouseWheel);
}
上面这部分代码是写在 class 的 init 方法中,所以截流函数的上下文(context)传入的是 this,表示当前 class 实例。
为了简化 html 结构,导航按钮通过 js 创建。这里的难点在于如何实现点击不同按钮实现对应页面的跳转并更新对应按钮的样式。
解决的思路是:
// 创建右侧点式导航
createNav() {
const nav = document.createElement('div');
nav.className = 'nav';
this.container.appendChild(nav);
// 有几页,显示几个点
for (let i = 0; i < this.pagesNum; i++) {
nav.innerHTML += '<p><span></span></p>';
}
const navDots = document.querySelectorAll('.nav-dot');
this.navDots = Array.prototype.slice.call(navDots);
// 添加初始样式
this.navDots[0].classList.add('active');
// 添加点式导航点击事件
this.navDots.forEach((el, i) => {
el.addEventListener('click', event => {
// 页面跳转
this.currentPosition = -(i * this.viewHeight);
this.turnPage(this.currentPosition);
// 更改样式
this.navDots.forEach(el => {
utils.deleteClassName(el, 'active');
});
event.target.classList.add('active');
});
});
}
得当的自定义参数可以增加插件的灵活性。
参数通过构造函数传入,并通过 Object.assign() 进行参数合并:
constructor(options) {
// 默认配置
const defaultOptions = {
isShowNav: true,
delay: 150,
definePages: () => {},
};
// 合并自定义配置
this.options = Object.assign(defaultOptions, options);
}
浏览器窗口尺寸改变的时候,需要重新获取可视区、页面元素高度,并重新确定容器当前的 top 值。
同时,为了避免不必要的性能开支,这里使用了防抖动函数。
// window resize 时重新获取位置
getNewPosition() {
this.viewHeight = document.documentElement.clientHeight;
this.container.style.height = this.viewHeight + 'px';
let activeNavIndex;
this.navDots.forEach((e, i) => {
if (e.classList.contains('active')) {
activeNavIndex = i;
}
});
this.currentPosition = -(activeNavIndex * this.viewHeight);
this.turnPage(this.currentPosition);
}
handleWindowResize(event) {
// 设置防抖动函数
utils.debounce(this.getNewPosition, this, event, this.DELAY);
}
// 窗口尺寸变化时重置位置
window.addEventListener('resize', this.handleWindowResize.bind(this));
这里的兼容性主要指两个方面:一是不同浏览器对同一行为定义了不同 api,比如上文提到的获取鼠标滚动信息的 API Firefox 和其他浏览器不一样;第二点就是 ES6 新语法、新 API 的兼容处理。
对于 class、箭头函数这类新语法的转换,通过 babel 就可完成,鉴于本插件代码量很小,都处于可控的状态,并没有引入 babel 提供的 polyfill 方案,因为新 API 只有 Object.assign() 需要做兼容处理,单独写个 polyfill 就好,如下:
// polyfill Object.assign
polyfill() {
if (typeof Object.assign != 'function') {
Object.defineProperty(Object, 'assign', {
value: function assign(target, varArgs) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
let to = Object(target);
for (let index = 1; index < arguments.length; index++) {
let nextSource = arguments[index];
if (nextSource != null) {
for (let nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true,
});
}
},
因为本插件只兼容到 IE10,所以不打算对事件做兼容处理,毕竟 IE9 都支持 addEventListener 了。
在 5.1 中写的 getWheelDelta 函数每次执行都需要检测是否支持 event.wheelDelta,实际上,浏览器只需在第一次加载时检测,如果支持,接下来都会支持,再做检测是没必要的。
并且这个检测在页面的生命周期中会执行很多次,这种情况下可以通过 惰性载入 技巧进行优化,如下:
getWheelDelta(event) {
if (event.wheelDelta) {
// 第一次调用之后惰性载入,无需再做检测
this.getWheelDelta = event => event.wheelDelta;
// 第一次调用使用
return event.wheelDelta;
} else {
// 兼容火狐
this.getWheelDelta = event => -event.detail;
return -event.detail;
}
},
纯 JS 全屏滚动 / 整屏翻页
Throttling and Debouncing in JavaScript
Debouncing and Throttling Explained Through Examples
JavaScript Debounce Function
Viewport height is taller than the visible part of the document in some mobile browsers
MDN-Object.assign()
Babel 编译出来还是 ES 6?难道只能上 polyfill?- Henry 的回答
来源:https://xiaogliu.github.io/2018/04/28/develop-full-page-scroll-by-es6/
这篇文章主要讲述通过js来实现html页面的全屏显示,以及退出全屏展示的方法。分享给大家查考,具体代码实现如下
fullPage.js 是一个基于 jQuery 的插件,它能够很方便、很轻松的制作出全屏网站。 主要功能有:支持鼠标滚动、支持前进后退和键盘控制...
写前端网页或者做前端小游戏的时候,为了提高用户体验,需要请求网页全屏显示(这点对于手机端的页游很重要),这个需求可以通过JavaScript实现
js让浏览器全屏模式的方法launchFullscreen,HTML 5中的full screen,目前可以在除IE和opera外的浏览器中使用 ,有的时候用来做全屏API,游戏呀,等都很有用。浏览器全屏模式的启动函数requestFullscreen仍然需要附带各浏览器的js方言前缀
这篇文章主要介绍了JavaScript控制全屏,监听退出全屏。以及ie低版本的全屏,退出全屏都这个方法,ie调用ActiveX控件,需要在ie浏览器安全设置里面把 未标记为可安全执行脚本的ActiveX控件初始化并执行脚本设置为启用
我们知道,浏览器全屏通常按快捷键F11。那么如何通过JS前端实现让浏览器全屏、退出全屏? 在进行进入全屏和退出全屏的操作,需要的朋友可以参考下
页面中使用iframe嵌入PDF.js的viewer.html时,部分浏览器全屏功能错误; 问题出现的浏览器主要有:edge(win10自带),火狐(按钮被屏蔽);解决方案:
最近后台项目需要一个全屏的按钮, github了下, 发现都仅仅支持开启全屏, 而没有切换、监听全屏状态等功能, 首先我发现ts自带的声明中, 对webkit或moz开头的这种api并没有声明类型
javascript可以使用Element.requestFullscreen()实现全屏也可以通过模拟F11快捷键实现全屏。Element.requestFullscreen()方法用于 异步请求使得Element(该元素)全屏显示。
一张清晰漂亮的背景图片能给网页加分不少,我们既不想图片因为不同分辨率图片变形,也不希望当在大屏的情况下,背景有一块露白,简而言之,就是实现能自适应屏幕大小又不会变形的背景大图,而且背景图片不会随着滚动条滚动而滚动。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!