本篇文章爲基於better-scroll
的 vue 移動端項目踩坑記錄,主要圍繞better-scroll
這個插件在項目中所帶來的問題進行記錄。javascript
固然最後還會有部分章節爲其餘插件踩坑和一下技巧運用的內容。css
詳情能夠參考下方目錄。html
better-scroll 概要
java
具體踩坑node
其實一開始的核心是爲了解決:ios
微信內置瀏覽器下拉顯示網頁信息這個特色,所致使頁面動效錯亂git
的這個問題。github
如圖所示,微信瀏覽器還真的是奇奇怪怪(還有好比說 ios 的底部導航條)。因而我就很好奇爲何會有一個默認的下拉行爲,他的意義是什麼,我就馬上去網上查了查。
然而根據查詢的結果好像並沒獲得詳細合理的解釋,大概就是由於微信瀏覽器是內嵌 webview,而 webview 在小程序應用中須要有這個下拉刷新的默認行爲,雖然微信瀏覽器不須要下拉刷新,但仍然是保留了這個機制。。。
Emm...好吧! 也許還有更加合理的理由來解釋保留這個機制的緣由,儘管我尚未找到,但項目要繼續完成,因而開始着手解決。
固然要解決這個問題,直接去禁用頁面的 touchmove
事件顯然不是一個穩妥的方案,因此...
web
其實早在 bs(簡稱)以前就瞭解到 iscroll 滾動庫,但其已經中止維護。而 better-scroll 是基於 iscroll 重寫的一款解決移動端滾動場景的插件,api 兼容 iscroll,另作了優化。體積小巧功能強大。
先放一張官網上的圖,會更加直觀清晰。
滾動原理大體能夠理解爲wrapper
是父容器,content
爲子容器。當子容器的高度超過了父容器,就能夠開啓滾動。
這個滾動原理很是重要,我就是事先沒有特別注意這裏的前導,致使我直接使用的時候發現怎麼不管滾動也沒有效果,再倒回來看。
具體的底層原理是基於
requestAnimationFrame
這個 api 來實現動畫,不深刻闡述。
<div class="wrapper" ref="wrapper">
<div class="content">
<router-view />
</div>
</div>
複製代碼
import Bscroll from "@better-scroll/core";
export default {
mounted() {
let scroll = new BScroll(this.$refs.wrapper, {});
},
};
複製代碼
初始化後沒法滾動會有不少緣由,首先很是重要的一點是:實例化的時機。
BScroll
的實例化必定要放在$nextTick
的回調函數中。mounted() {
this.$nextTick(() => {
let scroll = new BScroll(this.$refs.wrapper, {})
})
}
複製代碼
這是由於咱們要在 DOM 已經渲染完畢後,才能確保獲取到須要添加滾動的元素,以及滾動父元素和子元素的高度,來計算是否要添加滾動。
content
dom 必須是wrapper
的第一子元素。這是官網使用方法給出的規定,遵照便可。
這一點很好理解,很簡單也很重要。這也就是爲何沒有讀前導的我發現滾動無效的根本緣由。
必定要給父元素固定的高度,由於一般父元素的高度會由子元素撐開,而滾動開啓的條件是當子元素的高度超出父元素,因此父元素的固定高度是關鍵。
.wrapper {
weight: 100vw;
height: 100vh;
}
複製代碼
此時當子元素content
的內容高度超出一屏時,天然就可以滾動,而 wrapper 之外的任何元素都不會滾動。
那麼至此,不管頁面是否能夠滾動,微信內置瀏覽器的下拉行爲已經阻止,然而踩坑還在繼續。
理解了前導的滾動原理,那麼這個問題也基本上不算是問題。
其實一開始看到個人 tab 頁的表現是懵的,心想我好好的 tab 頁滾動怎麼出了問題,煩躁。。
冷靜想想也很簡單,無非就是切換 tab 頁面的時候動態地改變了子元素的高度,然而插件人家並不知道啊。剛進頁面的時候,插件獲取了頁面的高度,此時一切都正常。tab 頁就是對幾個 div 進行 display 的切換,切換 tab 頁改變了內容高度,那麼讓插件從新獲取一次便可。 尋找 api ->
ps:若是是 layout 級別的應用,記得把實例化的
scroll
存儲在 store 裏面。
export default {
computed: {
...mapState({
scroll: (state) => state.layout.scroll,
}),
},
methods: {
tabChange() {
this.$nextTick(() => {
this.scroll.refresh();
});
},
},
};
複製代碼
這裏一樣須要在$nextTick
的回調中調用refresh
,理由一模一樣。
整個項目我一共有三個頁面使用到了 Tabs 組件,然而在初期製做第一個 Tab 頁面的時候,這個問題沒有出現過。可是當我全部頁面所有制做完成後,第一個製做的 Tab 頁面開始做妖了。。。
具體表現爲切換 tab 頁後,沒法滾動至頂部,而且預設動效時間變的緩慢,好像被中途中止了同樣。
export default {
methods: {
tabChange() {
this.scroll.scrollTo(0, 0, 500); // 參數:x座標 y座標 動效時間 動畫函數
this.$nextTick(() => {
this.scroll.refresh();
});
},
},
};
複製代碼
我思考滾動至頂部這個動做和計算頁面高度並無任何關聯,那麼滾動的動做理論上來講能夠在scroll.refresh()
以前執行,沒毛病。
儘管如此,我屢次嘗試改變scrollTo
和refresh
的執行順序依舊沒有效果。但有一點很神奇的是,別的 tab 頁面沒有出現 bug,另外若是我刪除動效時間這個參數,那麼 bug 會消失。
this.scroll.scrollTo(0, 0); // 沒有出現bug
複製代碼
我思來想去,期間我只作過一件事情,那就是修改了實例化better-scroll
時的配置選項:
{
useTransition: false, // 我添加了這一條屬性
}
複製代碼
然而我其實是必須須要這條配置的,後面的章節會提到。這個選項大體是將頁面滾動的效果從 css 的 transition 切換到了 js 來執行,本意是以默認的 css 配置來優化頁面滾動的性能。
我猜測,也許是修改配置後執行的scrollTo
方法和refresh
方法有衝突,滾動被refresh
中斷等等之類瞎猜的緣由,因此我將目標放在了:如何讓scrollTo
方法在refresh
後執行。
tabChange() {
this.$nextTick(() => {
this.scorll.refresh();
this.scorll.scrollTo(0, 0, 500); // 無效
});
};
複製代碼
tabChange() {
this.$nextTick(async () => {
this.scorll.refresh();
await this.scorll.scrollTo(0, 0, 500); // 無效
});
};
複製代碼
最後,我想起來曾經看到過
$nextTick()
方法不傳回調函數返回一個 promise
相似的語句,猜測$nextTick
方法掛起的回調函數應該是一個micro task
,那麼我建立一個macro task
讓scrollTo
方法在$nextTick
的回調函數結束後在執行,是否會成功呢。
tabChange() {
setTimeout(() => {
this.scroll.scrollTo(0, 0, 500)
}, 0)
this.$nextTick(() => {
this.scroll.refresh()
})
}
複製代碼
結果是成功了
最終雖然解決了問題,可是目前仍然沒有探究明白是什麼緣由致使scrollTo
方法的失效,以及個人猜測是否正確。
從思考的角度來講,切換 tab 頁面,應當先執行refresh
函數來從新定義BS
實例所須要的頁面信息,而後再執行scrollTo
函數,讓頁面滾動到合適的位置。產生該 bug 最大的可能性應該是,因爲執行順序不正確,頁面正在處於滾動狀態的時候觸發refresh
函數才使得滾動失效。
是否有更加優雅的解決方案,還有對 bug 的探究等往後補充了。
在項目前期我根本沒有注意到這個問題,直到我總覽整個項目的各個頁面的時候,發現這個頁面滾動的效果不理想,老是以爲很奇怪,但又說不上來。
因而退出到手機 home 界面打開了設置,開始滑動。那個流暢的感受...再切回來滑動就很快發現了問題 —— 人們滑動頁面的時候並不會等上一次滑動動畫結束後才進行下一次的滑動,一般會進行連續的點擊和滑動,此時頁面再安卓端沒有任何問題。可是在 ios 上面,則變現爲滑動期間觸屏會使頁面滾動高度抖動一次。連續的滑動就會形成連續的抖動,這個問題就很嚴重了。。。
查詢了官網的 issue,給出了以下建議:
{
useTransition: true; // 使用該項配置
}
複製代碼
行吧。。。全用 css 畢竟還使有缺陷的,我們不能要了性能丟了體驗,因而滑動流暢多了。
這個問題解決起來仍是比較簡單,關鍵是在於如何查找問題。簡單的 google 或者 baidu 有時並不會給你滿意的答案,這個時候 官網的 issue 或者一些國外的平臺(stack overflow) 也許會十分有幫助,且高效。
import BScroll from "@better-scroll/core";
export default {
mounted() {
this.$nextTick(() => {
let scroll = new BScroll(this.$refs.wrapper, {
// 發現頁面觸發不了點擊事件加上他們
click: true,
tap: true,
// * 觸發滾動事件的類型,3爲實時觸發
probeType: 3,
// 容許y方向的滾動
scrollY: true,
// 頁面滾動到兩端時的過量回彈效果,默認是 true
bounce: false,
// 使用js實現滾動
useTransition: false,
// * 容許觸發默認事件的標籤名正則
preventDefaultException: {
tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO|VIDEO|IMG)$/,
},
});
});
},
};
複製代碼
probeType
這個屬性,簡單來講就是觸發監聽事件的頻率。在項目中我有需求去實時獲取滑屏的方向,來製做頂部導航 slideDown 的效果。那麼一開始我發現監聽scroll
事件沒有任何反應,我還覺得是 api 更新了。看了 issue 才發現當probeType
值爲1
的時候scroll
事件是不會觸發的。。。
那麼具體所對應的值有什麼效果,能夠去 api 文檔去查看,很少贅述。但一般來講,想要完成項目複雜的需求,大多數狀況下仍是須要probeType
爲3
。
preventDefaultException
字面意思,這是個正則表達式。在配置中有一個preventDefault
屬性默認值是true
。該屬性禁用了大部分的默認行爲,官方也不建議修改此選項。可是咱們能夠經過preventDefaultException
屬性來恢復部分標籤的默認行爲。這一點也是比較重要的,官方默認值爲
{ tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }
複製代碼
這些。因爲項目裏面我使用到了 img
和 video
標籤。圖片須要長按識別,而視頻的默認控件須要默認行爲的支持,因此直接將標籤名放入這個正則表達式便可。
一般項目中會有些奇奇怪怪的設計,現有的 ui 庫拿來無法直接用。例如個人項目中標籤頁的設計,他們說叫沉浸式。
正常的標籤頁是這樣子的:
標題 | 標籤頁 | 內容
三個區域互不干擾,卻又有聯動。
而咱們的設計是這樣的:
沒錯,在某一個標籤的展現中: 內容的一半 | 標籤頁 | 標題 擁有一個沉浸式地背景。
這樣一來只能從新設計整個標籤頁的結構,那麼新的結構必須是這樣的:
如此,就能夠自定義整塊標題+內容區域的樣式。
通過一點點的修改和嘗試,終於完成了對樣式的修改。(同時還要注意標籤底部的條動效位置要正確),代碼以下:
/deep/ .van-tabs__wrap {
position: absolute;
width: 90vw;
height: 42px;
top: 100px + 48px;
transform: translate(5vw);
}
/deep/ .van-hairline--top-bottom::after {
border: 0;
border-bottom: 1px solid rgba(51, 51, 51, 0.1);
}
/deep/ .van-tabs__nav {
justify-content: space-between;
}
/deep/ .van-tab {
width: 60px;
flex: 0;
flex-basis: 70px;
}
複製代碼
那麼重點來了,依此法修改的組件樣式,最好應用於項目中全部的標籤頁,以防止之後其餘頁面出現相同需求。那麼這一串樣式代碼在每一個組件中去複製顯然是比較冗餘的。
能夠巧妙的運用 less 封裝的能力將這一串代碼放入命名空間。
// global.less
#custom-vant {
.absolute_tab {
/deep/ .van-tabs__wrap {
position: absolute;
width: 90vw;
height: 42px;
top: 100px + 48px;
transform: translate(5vw);
}
/deep/ .van-hairline--top-bottom::after {
border: 0;
border-bottom: 1px solid rgba(51, 51, 51, 0.1);
}
/deep/ .van-tabs__nav {
justify-content: space-between;
}
/deep/ .van-tab {
width: 60px;
flex: 0;
flex-basis: 70px;
}
}
}
複製代碼
那麼在使用的時候,樣式代碼會很是地簡潔:
@import url("../../assets/css/global.less");
.tabs {
#custom-vant.absolute_tab();
}
複製代碼
記得運用的時候要引入全局 less 文件。一樣的方法,能夠定義全局變量,例如主題顏色等。
// global.less
#theme_color {
@purple: rgba(73, 56, 141, 1);
@gray: rgba(153, 153, 153, 1);
// ...
}
複製代碼
@import url("../../assets/css/global.less");
.box {
background: #theme_color[ @purple ];
}
複製代碼
關於 less 的使用,文檔上面已經很是的詳細了,那麼更多的使用技巧歡迎你們一塊兒來討論學習。
swiper 的輪播圖的確很是強大,可以實現的效果然的很是多。可是文檔真的不是很好用。。。
最快的方法應該是翻官網上的示例的代碼,直接拿過來用。但就這樣也不必定就能成功,況且設計可能還有些出入。總之項目有一塊輪播的需求是這樣的:
咱們的需求和官網的例子還不是徹底同樣,由於他先後兩個 banner 圖並非徹底顯示的,有一半藏在外面。這倒也好辦,直接把 div 拉出屏幕再居中就行了。
問題來了,輪播圖須要無限循環。當我加入 loop 這個屬性後,第一張圖的前 n 張圖加載失敗了。
也就是隻能看見傳入的四張圖片。我本來覺得是由於引入方式的關係,我將圖片地址換爲靜態路徑,線上地址都沒有用。
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item, index) in swiperList" :key="index">
<VanImage class="swiper-img" :src="..." fit="cover" />
</div>
</div>
</div>
複製代碼
export default {
mounted() {
this.$nextTick(() => {
new Swiper(".swiper-container", {
slidesPerView: 3, // 能夠看見3張banner圖
spaceBetween: 8, // 圖片之間的間隙
centeredSlides: true, // 是否居中顯示
loop: true, // 循環顯示
});
});
},
};
複製代碼
最後我發現了一下奇怪的細節,沒法顯示的圖片,顯示的 vant 組件 Image
的默認圖片,而圖片有一絲很是細的邊框,就好像底下有圖片,被默認 alt
屬性中的圖片遮蓋住了。這很神奇。
後來我棄用了 vant 的組件,改爲原生的img
組件,bug 就解決了。也許是由於實現無限循環,重複的圖片是拷貝出來的 html 片斷,沒有 js 代碼,而 Vant 組件並無接受到圖片的地址,因此默認貼上了alt
屬性的圖片。才致使看起來圖片加載失敗了。
看來 不一樣的組件組合使用的時候,極可能會出現不兼容的狀況。 因此在選擇使用的時候仍是須要當心一些。
順帶提一句,輪播圖裏item
的樣式也是要本身寫的。。直接去看官網上面的例子,樣式直接拿來用便可。
// 周邊的banner圖片進行縮放0.8,是在css中寫出來的
.swiper-slide {
// ....
/* Center slide text vertically */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: 300ms;
transform: scale(0.8);
}
.swiper-slide-active,
.swiper-slide-duplicate-active {
transform: scale(1);
}
複製代碼
另外在 vue 項目中使用 swiper 的時候,須要手動引入 swiper 的樣式,這也是一個樣式沒法生效的常見緣由。
// less
@import url("../../../node_modules/swiper/css/swiper.min.css");
複製代碼
最根本的問題其實就是
andiord
和ios
端微信瀏覽器的內核不同所致使的一系列噁心的問題。
ios 端表現其實十分良好,重點就在於安卓端的兼容處理。
先重中之重!千萬不要加這條屬性:
<video x5-video-player-type="h5" />
複製代碼
千萬不要!這是個大坑,不要嘗試在這條屬性基礎上進行兼容。這條屬性意味聲明啓用同層 h5 播放器。
若是你們有基於同層 h5 播放器好的兼容方案,能夠討論一下。
其他就是一些實現行內播放的兼容屬性,經常使用的屬性大體爲以下代碼
<video preload="auto" x5-video-orientation="portraint" x5-video-player-fullscreen="true" x5-playsinline="" playsinline="" webkit-playsinline="" x-webkit-airplay controls="controls" class="video" ref="video" src="https://hxy-web1.oss-cn-hangzhou.aliyuncs.com/meiye/wuqihua_vlog_v5.mp4" style="object-fit:fill" />
複製代碼
增長了默認控制組件,豎屏處理,全屏播放,還有行內播放的兼容。
還有一點須要注意的是,爲了用戶體驗更加一致,一般不建議使用
poster
。直接在同位置定位一張圖片和按鈕,點擊後直接進行視頻播放。
最後還有一個沒有解決的問題:
安卓端微信瀏覽器的視頻會出現莫名的黑邊,根據寬高比會出如今上下,或者左右,粗細也不一樣,但始終不會消失。
查詢了社區,也沒有找到合理的回覆解釋。不知道你們有沒有好的方法可以解決這個問題。
文章沒有過高的技術深度,基本上是以很是普通的話術來描述將一個技術點運用到項目中遇到的坑。會有許多不夠嚴謹和成熟的地方,也歡迎你們一塊兒討論。此文也貢獻給正在使用 better-scroll
的同事們,遇到相同的問題能夠來借鑑討論一下。
若有轉發請告知 (固然確定是沒有的了,我飄了...)
另:文章首發於 個人博客,儘管仍在建設中,但我須要您鼓勵和支持,對您有幫助的話去點個贊吧。
下次見。