摘要:這篇文章將介紹如何使用原生主要使用語法實現全屏滾動插件,兼容手機觸屏,觸摸板優化,支持自定義頁面動畫,壓縮后文件只有。
這篇文章將介紹如何使用原生 JS (主要使用 ES6 語法)實現全屏滾動插件,兼容 IE 10+、手機觸屏,Mac 觸摸板優化,支持自定義頁面動畫,壓縮后 gzip 文件只有 2.15KB。完整源碼在這 pure-full-page,點這查看 demo。
1)前面的話現在已經有很多全屏滾動插件了,比如著名的 fullPage,那為什么還要自己造輪子呢?
現有輪子有以下問題:
首先,最大的問題是最流行的幾個插件都依賴 jQuery,這意味著在使用 React 或者 Vue 的項目中使用他們是一件十分蛋疼的事:我只需要一個全屏滾動功能,卻還需要把 jQuery 引入,有種殺雞使用宰牛刀的感覺;
其次,現有的很多全屏滾動插件功能往往都十分豐富,這在前幾年是優勢,但現在(2018-5)可以看作是劣勢:前端開發已經發生了很大變化,其中很重要的一個變化是 ES6 原生支持模塊化開發,模塊化開發最大的特點是一個模塊最好只專注做好一件事,然后再拼成一個完整的系統,從這個角度看,大而全的插件有悖模塊化開發的原則。
對比之下,通過原生語言造輪子有以下好處:
使用原生語言編寫的插件,自身不會受依賴的插件的使用場景而影響自身的使用(現在依賴 jQuery 的插件非常不適合開發單頁面應用),所以使用上更加靈活;
搭配模塊化開發,使用原生語言開發的插件可以只專注一個功能,所以代碼量可以很少;
最后,隨著 JS/CSS/HTML 的發展以及瀏覽器不斷迭代更新,現在使用原生語言編寫插件的開發成本越來越低,那為什么不呢?
2)實現原理及代碼架構 2.1 實現原理實現原理見下圖:容器及容器內的頁面取當前可視區高度,同時容器的父級元素 overflow 屬性值設為 hidden,通過更改容器 top 值實現全屏滾動效果。
2.2 代碼架構代碼編寫的思路是通過 class 定義全屏滾動類,使用時通過 new PureFullPage().init() 使用。
/** * 全屏滾動類 */ class PureFullPage { // 構造函數 constructor() {} // 原型方法 methods() {} // 初始化函數 init() {} }3)html 結構
鑒于上述實現原理,對于 html 的結構有特定要求,如下:頁面容器為 #pureFullPageContainer,所有的頁面為其直接子元素,這里為了方便,直接取 body 為其直接父元素。
4)css 設置
首先,容器及容器內的頁面取當前可視區高度,為每次切換都顯示一個完整的頁面做準備;
第二,容器的父級元素(此處是 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";5)監控滾動/滑動事件
這里的滾動/滑動事件包括鼠標滾動、觸摸板滑動以及手機屏幕上下滑動。
5.1 PC 端PC 端主要解決的問題是獲取鼠標滾動或觸摸板滑動方向,觸摸板上下滑動和鼠標滾動綁定的是同一個事件:
firefox 是 DOMMouseScroll 事件,對應的滾輪信息(向前滾還是向后滾)存儲在 detail 屬性中,向前滾,這個屬性值是 3 的倍數,反之,是 -3 的倍數;
firefox 之外的其他瀏覽器是 mousewheel 事件,對應的滾輪信息存儲在 wheelDelta 屬性中,向前滾,這個屬性值是 -120 的倍數,反之, 120 的倍數。
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 是頁面滾動的邏輯代碼,需要特別說明的是必須 判斷滾動邊界,保證容器中顯示的始終是頁面內容:
上邊界容易確定,為 1 個頁面(也即可視區)的高度,即如果容器當前的上外邊框距離整個頁面頂部的距離(這里此值正是容器的 offsetTop 值的絕對值,因為它父元素的 offsetTop 值都是 0)大于等于當前可視區高度時,才允許向上滾動,不然,就證明上面已經沒有頁面了,不允許繼續向上滾動;
下邊界為 n - 2(n 表示全屏滾動的頁面數) 個可視區的高度,當容器的 offsetTop 值的絕對值小于等于 n - 2 個可視區的高度時,表示還可以向下滾動一個頁面。
具體代碼如下:
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); }5.2 移動端
移動端需要判斷是向上還是向下滑動,可以結合 touchstart(手指開始接觸屏幕時觸發) 和 touchend(手指離開屏幕時觸發) 兩個事件實現判斷:分別獲取兩個事件開始觸發時的 pageY 值,如果觸摸結束時的 pageY 大于觸摸開始時的 pageY,表示手指向下滑動,對應頁面向上滾動,反之亦然。
此處我們需要觸摸事件跟蹤觸摸的屬性:
touches:當前跟蹤的觸摸操作的 Touch 對象的數組,用于獲取觸摸開始時的 pageY 值;
changeTouches:自上次觸摸以來發生了改變的 Touch 對象的數組,用于獲取觸摸觸摸結束時的 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(); });6)PC 端滾動事件性能優化 6.1 防抖函數和截流函數介紹
優化主要從兩方便入手:
更改頁面大小時,通過防抖動(debounce)函數限制 resize 事件觸發頻率;
滾動/滑動事件觸發時,通過截流(throttle)函數限制滾動/滑動事件觸發頻率。
既然都是限制觸發頻率(都通過定時器實現),那這兩者有什么區別?
首先,防抖動函數工作時,如果在指定的延遲時間內,某個事件連續觸發,那么綁定在這個事件上的回調函數永遠不會觸發,只有在延遲時間內,這個事件沒再觸發,對應的回調函數才會執行。防抖動函數非常適合改變窗口大小這一事件,這也符合 拖動到位以后再觸發事件,如果一直拖個不停,始終不觸發事件 這一直覺。
而截流函數是在延遲時間內,綁定到事件上的回調函數能且只能觸發一次,這和截流函數不同,即便是在延遲時間內連續觸發事件,也不會阻止在延遲時間內有一個回調函數執行。并且截流函數允許我們指定回調函數是在延遲時間開始時還是結束時執行。
鑒于截流函數的上述兩個特性,尤其適合優化滾動/滑動事件:
可以限制頻率;
不會因為滾動/滑動事件太靈敏(在延遲時間內不斷觸發)導致注冊在事件上的回調函數無法執行;
可以設置在延遲時間開始時觸發回調函數,從而避免用戶感到操作之后的短暫延時。
這里不介紹防抖動函數和截流函數的實現原理,感興趣的可以看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 延遲函數, // 這里沒有提供是在延遲時間開始還是結束的時候執行回調函數的選項, // 直接在延遲時間開始的時候執行回調 throttle(method, context, delay) { let wait = false; return function() { if (!wait) { method.apply(context, arguments); wait = true; setTimeout(() => { wait = false; }, delay); } }; },
《JavaScript 高級程序設計 - 第三版》 22.33.3 節中介紹的 throttle 函數和此處定義的不同,高程中定義的 throttle 函數對應此處的 debounce 函數,但網上大多數文章都和高程中的不同,比如 lodash 中定義的 debounce。6.2 改造 PC 端滾動事件
通過上述說明,我們已經知道截流函數可以通過限定滾動事件觸發頻率提升性能,同時,設置在延遲時間開始階段立即調用滾動事件的回調函數并不會犧牲用戶體驗。
截流函數上文已經定義好,使用起來就很簡單了:
// 設置截流函數 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 實例。
7)其他 7.1 導航按鈕為了簡化 html 結構,導航按鈕通過 js 創建。這里的難點在于如何實現點擊不同按鈕實現對應頁面的跳轉并更新對應按鈕的樣式。
解決的思路是:
頁面跳轉:頁面個數和導航按鈕的個數一致,所以點擊第 i 個按鈕也就是跳轉到第 i 個頁面,而第 i 個頁面對應的容器 top 值恰好是 -(i * this.viewHeight)
更改樣式:更改樣式即先刪除所有按鈕的選中樣式,然后給當前點擊的按鈕添加選中樣式。
// 創建右側點式導航 createNav() { const nav = document.createElement("div"); nav.className = "nav"; this.container.appendChild(nav); // 有幾頁,顯示幾個點 for (let i = 0; i < this.pagesNum; i++) { nav.innerHTML += "7.2 自定義參數 "; } 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); }7.3 窗口尺寸改變時更新數據
瀏覽器窗口尺寸改變的時候,需要重新獲取可視區、頁面元素高度,并重新確定容器當前的 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));7.4 兼容性
這里的兼容性主要指兩個方面:一是不同瀏覽器對同一行為定義了不同 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, }); } },
引用自:MDN-Object.assign()
因為本插件只兼容到 IE10,所以不打算對事件做兼容處理,畢竟 IE9 都支持 addEventListener 了。7.5 通過惰性載入進一步優化性能
在 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; } },
完整源碼在這 pure_full_page,點這查看 demo。
參考資料純 JS 全屏滾動 / 整屏翻頁
Throttling and Debouncing in JavaScript
Debouncing and Throttling Explained Through Examples
JavaScript Debounce Function
Simple throttle in js
Simple throttle in js - jsfiddle
Viewport height is taller than the visible part of the document in some mobile browsers
MDN-Object.assign()
Babel 編譯出來還是 ES 6?難道只能上 polyfill?- Henry 的回答
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107730.html
摘要:由此,我們可以完全屏蔽的默認滾動觸發,改用方法控制全屏滾動,解除了全屏滾動與鼠標滾輪事件的強耦合。此外,通過定時器延時秒設置的值,將用戶的鼠標滾輪操作強制分為兩步,最終實現了目的。 需求分析 剛進公司產品提出一個需求:在全屏頁面中滾動鼠標滾輪更新文本,回滾再恢復原文本,同時不影響全屏頁面的正常切換: 初始狀態 showImg(https://segmentfault.com/img/b...
原文鏈接:https://github.com/AlloyTeam/AlloyTouch/wiki/AlloyTouch-FullPage-Plugin 先驗貨 showImg(https://segmentfault.com/img/remote/1460000007885626?w=280&h=280); 插件代碼可以在這里找到。 注意,雖然是掃碼體驗,但是AlloyTouch.FullPag...
摘要:轉載來源包管理器管理著庫,并提供讀取和打包它們的工具。能構建更好應用的客戶端包管理器。一個整合和的最佳思想,使開發者能快速方便地組織和編寫前端代碼的下一代包管理器。很棒的組件集合。隱秘地使用和用戶數據。 轉載來源:https://github.com/jobbole/aw... 包管理器管理著 javascript 庫,并提供讀取和打包它們的工具。?npm – npm 是 javasc...
摘要:轉載來源包管理器管理著庫,并提供讀取和打包它們的工具。能構建更好應用的客戶端包管理器。一個整合和的最佳思想,使開發者能快速方便地組織和編寫前端代碼的下一代包管理器。很棒的組件集合。隱秘地使用和用戶數據。 轉載來源:https://github.com/jobbole/aw... 包管理器管理著 javascript 庫,并提供讀取和打包它們的工具。?npm – npm 是 javasc...
閱讀 1619·2021-11-11 10:59
閱讀 2624·2021-09-04 16:40
閱讀 3650·2021-09-04 16:40
閱讀 2979·2021-07-30 15:30
閱讀 1615·2021-07-26 22:03
閱讀 3164·2019-08-30 13:20
閱讀 2225·2019-08-29 18:31
閱讀 439·2019-08-29 12:21