當頁面比較長內容較多的時候,會使用導航欄,給用戶提供方便跳轉到頁面某一模塊的功能。因爲導航欄須要監聽頁面的滾動事件,在小程序中,很容易出現性能問題,須要時刻注意滾動監聽中 setData 的次數。javascript
本文將介紹頁面滾動條操做相關的微信 API,並利用這些 API 實現一個通用的導航欄組件。html
導航組件效果以下圖:java
實現滾動到頁面目標位置的功能,須要「滾動操做」和」目標位置「。
小程序
將頁面滾動到頁面中的目標位置,可使用 wx.pageScrollTo 這個微信提供的 API。該方法能夠接收一個對象做爲參數,對象中能夠指定:微信小程序
滾動的目標位置 scrollTop,單位爲 px;api
滾動動畫的時長 duration,單位爲 ms;微信
選擇器 selector,但支持的基礎庫版本是從2.7.3;函數
其實,直接使用選擇器能夠方便地完成咱們想要的效果,但遺憾的是,咱們的小程序大約只有60%用戶的基礎庫是2.7.3以上。若是隻有這些用戶享受到新功能,用戶量稍微少了些,不過咱們可使用 scrollTop 直接指定目標位置。性能
首先,須要建立節點查詢對象 selectorQuery,建立方法以下:優化
wx.createSelectorQuery() // 返回selectorQuery對象
複製代碼
selectorQuery 對象能夠利用選擇器選擇匹配的節點,使用 selectAll 方法:
wx.createSelectorQuery().selectAll('.nav-target') // 返回 NodesRef複製代碼
NodesRef 可使用 fields 方法獲取到節點的信息,好比大小、dataset等,使用 boundingClientRect 能夠獲取節點的位置信息,如上邊界座標等,最後調用 exec 方法才能執行:
wx.createSelectorQuery().selectAll('.nav-target').fields({
dataset: true, // 指定返回節點 dataset 的信息
size: true, // 指定返回節點大小信息
}, rects => {
rects.forEach(rect => {
rect.dataset;
rect.width;
rect.height;
})
}).boundingClientRect(rects => {
rects.forEach(rect => {
rect.dataset;
rect.top;
})
}).exec() // 最後要加 exec 才能執行
複製代碼
導航欄組件的實現,大體須要以下準備工做:
獲取錨點的信息,組成導航欄按鈕文案;
獲取錨點的位置信息,以便點擊導航滾動到對應位置;
此外,還須要兩個特性:
點擊導航欄,讓頁面滾動到對應位置;
當頁面滾動時,導航欄對應錨點的按鈕須要改變active狀態;
咱們能夠約定,全部錨點都須要加上:
class: nav-target;
data-label:導航欄中顯示的文本;
data-key:做爲錨點標識;
因此,一個錨點元素可能會編寫成以下形式:
<view class="nav-target" data-key="overview" data-label="概覽">...</view>複製代碼
有了class,咱們就能夠利用前文提到的selectorQuery取到這些錨點,進而利用boundingClientRect方法取到錨點上的dataset,關鍵代碼以下:
wx.createSelectorQuery().selectAll('.nav-target').boundingClientRect(res => {
this.setData({
navList: res.map(item => item.dataset).filter(Boolean)
})
})
複製代碼
取到了錨點信息後,存入navList,其中的label做爲導航欄的按鈕文案,而key則用於接下來存儲錨點位置。
錨點的位置信息,也能夠經過boundingClientRect獲取,取到位置信息後,存入一個Map中,咱們命名爲positionMap,結合上面獲取錨點信息,_getAllAnchorInfoAndScroll方法代碼以下:
_getAllAnchorInfoAndScroll(selectorIdToScroll) {
wx.createSelectorQuery().selectAll('.nav-target').boundingClientRect(res => {
if (!res || res.length === 0) return
this.setData({
navList: res.map(item => item.dataset).filter(Boolean)
})
// 爲了減小setData傳輸數據量,咱們將視圖層不須要用到的position信息存在Page實例上
res.forEach(item => {
const { top, dataset: { key} } = item
if (top >= 0) {
this.positionMap[key] = Math.max(top - 55, 0) // 向上留55px的空間給導航欄
}
})
// 若是須要作滾動的操做,則在這裏執行
if (selectorIdToScroll) {
wx.pageScrollTo({ scrollTop: this.positionMap[selectorIdToScroll] })
}
}).exec()
}
複製代碼
因爲須要加導航的頁面長度都比較長,咱們一般會對非首屏的模塊使用動態加載技術。而頁面模塊的動態加載意味着,導航組件獲取錨點位置的時機不能簡單地設置在組件的 ready 事件。
很顯然,獲取錨點位置的時機應該設置在全部模塊都加載完成的時候。咱們能夠在模塊(組件)加載完成後,通知導航組件進行錨點信息的更新。
關鍵代碼大體以下:
頁面 page.wxml
<!-- 導航組件 -->
<nav id="nav" />
<!-- 頁面模塊組件 -->
<page-module bindupdate="updateNavList" />
複製代碼
頁面 page.js
Page({
updateNavList() {
this.getNavComponent().updateNavInfo()
},
getNavComponent() {
// 避免屢次調用 selectComponent,將其結果存入變量 _navComponent
if (!this._navComponent) {
this._navComponent = this.selectComponent('#nav')
}
return this._navComponent
},
})
複製代碼
模塊組件 pageModule.js
// 模塊組件中,加載完成時觸發頁面實例的 updateNavList 方法
this.triggerEvent('update')
複製代碼
導航組件 nav.js
Component({
methods: {
...,
updateNavInfo() {
this._getAllAnchorInfoAndScroll()
}
}
})
複製代碼
如此一來,頁面模塊更新後,導航組件也會更新錨點信息和位置,保證導航組件的信息是最新的。最後須要注意若是有懶加載的圖片,須要提早設定好高度,不然等圖片加載完錨點信息就錯亂了。固然,也能夠在圖片加載完成的方法中,調用更新導航信息的 updateNavList 方法,這部分與模塊組件的加載觸發思路一致本文就不贅述。
有了前面兩項準備工做,這個特性實現起來,就簡單多了。導航欄的按鈕有可能一行放不下,應該使用 scroll-view 標籤支持滾動。wxml 代碼以下:
導航組件 nav.wxml
<scroll-view scroll-x>
<view class="scroll-inner" bindtap="bindClickNav">
<view class="nav {{index === currentIndex ? 'nav--active' : ''}}" wx:for="{{navList}}" wx:key="{{index}}" data-key="{{item.key}}" data-index="{{index}}">{{item.label}}</view>
</view>
</scroll-view>複製代碼
其中,currentIndex 記錄當前選中的導航項;bindClickNav 則處理點擊導航項的更新 currentIndex 和頁面滾動邏輯。
導航組件 nav.js
bindClickNav(e) {
const { index, key } = e.target.dataset
this.setData({ currentIndex: index })
if (this.data.positionMap[selectorId] === undefined) {
// 若是點擊時,錨點位置還未取得,則須要先獲取位置並傳入key,在獲取位置以後滾動
this._getAllAnchorInfoAndScroll(key)
return
}
wx.pageScrollTo({ scrollTop: this.positionMap[selectorId] })
},
複製代碼
頁面滾動的監聽函數是 onPageScroll,咱們須要在其中判斷頁面滾動到哪一個錨點。
判斷滾動到哪一個錨點的具體邏輯是在導航組件中的 watchScroll 實現,頁面實例中的 onPageScroll 則傳遞頁面滾動位置給導航組件 watchScroll 方法。
頁面實例 page.js
Page({
onPageScroll({ scrollTop }) {
const navComponent = () => {
if (!this._navComponent) {
this._navComponent = this.selectComponent('#nav')
}
return this._navComponent
}
navComponent && navComponent.watchScroll(scrollTop)
}
})
複製代碼
在導航組件中,應該如何判斷頁面滾動的位置與錨點的關係呢?
如下圖爲例,頁面滾動超過了」模塊1「與」模塊2「的錨點,但未超過」模塊3「的錨點,此時導航欄顯示的」模塊2「應該是 active 態:
總結一下實現思路:按照從上到下的順序遍歷各個模塊,並將各個模塊的錨點位置與頁面的 scrollTop 進行對比,找到最後一個小於 scrollTop 的錨點模塊,該模塊的狀態即爲 active。
因爲「最後一個小於」比較難找,咱們能夠轉換成找「第一個大於」的模塊,該模塊的上一個模塊即爲 active 態的模塊。關鍵代碼以下:
導航組件 nav.js
Component({
...,
methods: {
...,
watchScroll(pageScrollTop) {
// 判斷是否爲空,即初始化還沒有完成
if (isEmpty(this.positionMap)) {
return
}
// 當頁面滾動時,中止更新navIndex
if (_navIndexLock) {
return
}
// 判斷滾動的scrolltop,而後設置 currentIndex
const lastIndex = this.data.navList.length - 1
for (let idx = 0; idx <= lastIndex; idx++) {
const navItem = this.data.navList[idx]
const top = this.positionMap[navItem.key]
const indexToSet = idx === 0 ? idx : idx - 1
// 尋找「第一個大於scrollTop」的模塊,其上一個模塊即爲 active 態的模塊
if (top > pageScrollTop) {
this.data.currentIndex !== indexToSet && this.setData({ currentIndex: indexToSet })
break
}
// 到最後一個tab尚未break,說明已經滾動到了最後tab
if (idx === lastIndex) {
this.data.currentIndex !== lastIndex && this.setData({ currentIndex: lastIndex })
}
}
}
}
})
複製代碼
本文介紹了微信小程序對頁面滾動和元素操做的支持狀況,利用這些特性實現了一個導航組件。這個導航組件支持動態加載的模塊,並可以根據頁面滾動的位置更新導航組件的 active 狀態。
組件中,主要是模塊動態加載完成這個時機比較難捕捉到,這裏利用加載完的事件觸發導航更新,這種方式的優化方案還待思考討論。若是你們有好的建議也歡迎留言討論~