前端必備自定義滾動庫——iScroll

做者真的很懶,這篇文章從想法開始到如今都幾個月了。😊javascript

iScroll是什麼

不少場景,若是使用body的滾動會很不方便,這時候,就會使用某個元素的局部滾動,噁心的事情就會發生了。css

  1. pc端web,windows系統的瀏覽器會出現醜陋的滾動條。(其實也有最新的css樣式能夠解決,兼容性欠佳)
  2. 手機移動端,ios瀏覽器沒法慣性和彈性滑動(默認使用瀏覽器滾動,非ios系統原生滾動),若是加上-webkit-overflow-scrolling: touch;使用系統原生滾動,兼容性方面欠佳,bug不是一個兩個😭。
  3. 不利於實現一些個性化需求,例如加載,刷新,貼合滾動等。

巧了,iScroll解決了這些問題。html

iScroll的做者是位國際友人,他的github飛機票在此vue

遺憾的是,做者幾乎再也不維護這個iScroll插件,網上的相關中文文檔不多,可是這並不影響咱們拿着這個插件處處浪。java

下面看看iScroll怎麼運做的ios

iScroll採用了css3的transform動畫模擬了慣性和彈性滾動的效果,效果和性能完美接近原生的滾動效果。同時提供了諸多功能包括自定義滾動條,指定滾動到元素等功能,還能夠輕鬆實現下拉刷新,上拉加載css3

iScroll的基本使用

首先確定是先安裝了
npm install iscroll

yarn add iscroll
複製代碼
而後引用
import IScroll from 'iscroll/build/iscroll'; // 普通版
import IScroll from 'iscroll/build/iscroll-probe'; // 複雜版
import IScroll from 'iscroll/build/iscroll-infinite';
複製代碼

iscroll有幾中不一樣js文件,分別是普通本,複雜版,無限滾動版。這裏經常使用的是複雜版,是支持實時監聽滾動的位置的,若是不須要實時監聽,能夠用普通版。git

初始化使用

這裏以vue框架爲例github

<template>
  <div class="wrap">
    <div class="scroll-area">
      <div v-for="n in 50" class="item">{{ n }}</div>
    </div>
  </div>
</template>
<script> import IScroll from 'iscroll/build/iscroll-probe'; export default { data() { scroll: null, }, mounted() { // 提示,由於transform是對dom操做,因此須要在這個生命週期操做 this.scroll = new IScroll('.wrap', { mouseWheel: true, // 容許鼠標滾輪 }); // 第一個參數是dom選擇器,建議使用惟一性的id,這裏以class爲例 // 第二個參數爲參數對象,是iscroll的一些配置 // 參數配置能夠參考 http://wiki.jikexueyuan.com/project/iscroll-5/ } } </script>
<style> .wrap{ height: 400px; overflow: hidden; /* 給滾動區域固定可滾動高度,而且超出隱藏 */ } </style>
複製代碼

以上代碼就完成了簡單的iscroll初始化使用,能夠看下效果web

效果1

iScroll刷新

注意點,因爲滾動內容多是異步獲取並加載dom,若是不刷新iscroll,那麼滾動功能可能會受到影響,因此當異步內容加載後,須要調用刷新方法,刷新iscroll,刷新方式以下

<template>
  <div ref="scroll" class="wrap">
    <div class="scroll-area">
      <div v-for="n in 50" class="item">{{ n }}</div>
    </div>
  </div>
</template>
<script> import IScroll from 'iscroll/build/iscroll-probe'; export default { data() { scroll: null, }, mounted() { const el = this.$refs.scroll; this.scroll = new IScroll('.wrap', { ... }); // ① 異步數據刷新 getData().then(_=>{ this.scroll.refresh(); }) // ② 首次滑動時刷新 el.addEventListener('touchstart', _=>this.scroll.refresh()); } } </script>
複製代碼

監聽位置

this.scroll = new IScroll('.wrap', {
  probeType: 3, // 滾動監聽級別 有3檔,3是像素級監聽
});
// 用iscroll實例註冊scroll事件
this.scroll.on('scroll', e => {
  // 此處不用箭頭函數能夠用this.x和this.y訪問實時位置,用了箭頭函數須要從實例上訪問
  // this.scroll.x
  // this.scroll.y
})
複製代碼

多的不說,看效果

效果2

注意取值的正負,監聽取得的值是transform的值,確認好正負值所對應的方向。

滾動到指定元素位置

這裏須要使用iscroll的貼合功能

this.scroll = new IScroll('.wrap', {
  snap: '.item',
});
// 當設置snap屬性爲true時,iscroll會把容器可視區域分割爲一個page
// 當設置snap屬性爲元素選擇器時,iscroll會把對應的元素設置爲一個page
// 這裏咱們設置爲'.item'
複製代碼

而後使用iscroll的goToPage方法,跳到對應元素

this.scroll.goToPage(0, 30, 1000);
// 參數分別爲x, y, 動畫時間,
// 注意x,y是傳入索引,第一個是0,類推
複製代碼

也可使用prevnext方法跳上一個或者下一個

this.scroll.prev();
this.scroll.next();
複製代碼

效果3

配置滾動條

若是想要滾動條呢,也很簡單

this.scroll = new IScroll('.wrap', {
  scrollbars: true, // 開啓滾動條
  shrinkScrollbars: 'scale', // 超出滾動時,縮放滾動條
});
複製代碼
/* 由於iscroll的滾動條是定位實現,因此容器須要加一個相對定位 */
.wrap{
  position: relative;
}
複製代碼

效果4

點擊事件

iscroll默認禁用了click事件,若是須要也能夠開啓

this.scroll = new IScroll('.wrap', {
  click: true,
});
複製代碼

而且iscroll很人性化的內置了tap事件,只要開啓tap,就能夠在元素上響應tap

this.scroll = new IScroll('.wrap', {
  tap: true,
});
複製代碼
<template>
  <div ref="scroll" class="wrap">
    <div class="scroll-area">
      <div v-for="n in 50" class="item" @tap="onTap">{{ n }}</div>
    </div>
  </div>
</template>
複製代碼

iScroll的sticky

在基於dom元素的原生滾動中,是能夠給內容添加position: sticky來實現吸頂效果的。

吸頂: 在父元素的滾動過程當中,若是子元素含有position: stickytop: 0樣式,那麼該內容滾到頂部時,會吸附在父元素的頂部,不會繼續向上滾動。(橫向滾動同理)

沒玩過position: sticky的,去試下就知道啦。固然這個css並非無敵的,緣由就是兼容性不過關。戳這裏

好了,咱們來講iscroll如何實現sticky,由於iscroll使用了transform實現滾動,因此容器設置了overflow: hidden,因此沒辦法用css的sticky實現,那麼既然是父元素transform滾動,那麼到達吸頂位置的時候,子元素反向transform是否是就能夠了呢?

仔細看下面的代碼呢,很重要,認真看註釋️

// 這段代碼能夠理解爲是對iscroll類的擴展
// 這裏的參數爲iscroll類
export const extendSticky = (iScroll) => {
  let m = Math;
  // 這裏是爲了兼容性配置的瀏覽器css前綴,網絡上有不少寫法呢
  let vendor = (/webkit/i).test(navigator.appVersion) ? 'webkit' :
      (/firefox/i).test(navigator.userAgent) ? 'Moz' :
        'opera' in window ? 'O' : '',
    has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix(),
    trnOpen = 'translate' + (has3d ? '3d(' : '('),
    trnClose = has3d ? ',0)' : ')';
  
  /** * 這裏開始拓展iscroll類 * @param selector 須要sticky的對象集合,包含元素和sticky的位置 * @return { iScrollStickyHeaders } */
   // 在iscroll原型上添加 enableStickyHeaders 方法
  iScroll.prototype.enableStickyHeaders = function (selector) {
    return new iScrollStickyHeaders(this, selector); // 拓展方法採用新的類並傳參
  };

  // 參數,iscroll實例,須要sticky的元素集合
  let iScrollStickyHeaders = function (iscroll, selector) {
    if (!iscroll.options.useTransform) {
      return;
    }
    this.iscroll = iscroll;
    this.selector = selector;
    this.initialize(); // 初始化
  };
  iScrollStickyHeaders.prototype = {
    headers: [], // 存儲須要sticky的對象集合
    initialize() {
      let that = this;
      this._augment();
      this.iscroll.on('refresh', function() {
        that._refresh() // 每次iscroll刷新,sticky方法也刷新
      });
      this.iscroll.refresh()
    },
    _refresh() { // 初始化或者刷新
      let elms = this.selector;
      this.headers = [ // 深拷貝對象集合
        ...elms,
      ]
      // 此處對象集合的格式爲 { el: 元素, top: 須要sticky的位置 }
      // 此處能夠根據習慣和喜歡自行定義格式和邏輯代碼
      this._translate(00); // 初始化
    },
    _augment() { // 初始化函數
      let that = this;
      this.iscroll.on('scroll', function() {
        that._translate(this.x, this.y) // iscroll滾動時,觸發主函數
      });
      this.iscroll.on('beforeScrollStart', function() {
        that._translate(this.x, this.y) // iscroll即將滾動時,觸發主函數
      });
      this.iscroll.on('scrollStart', function() {
        that._translate(this.x, this.y) // iscroll開始滾動時,觸發主函數
      });
    },
    _translate(x, y) { // 主函數,到達sticky位置後,反向transform
      let absY = m.abs(y); // 獲取y軸滾動的絕對值
      this.headers.forEach((stickyObj) => { // 遍歷sticky對象
        let translateY = 0; // sticky的反向transform默認爲0
        let yy = m.abs(absY - stickyObj.el.offsetTop); // 計算iscroll的y軸滾動值-當前元素距離父級的值
        // stickyObj.el.offsetTop爲固定值
        // yy即爲當前元素距離容器頂部的位置
        // absY < stickyObj.el.offsetTop說明該元素還沒到達頂部
        // yy <= stickyObj.top 判斷元素是否到達須要sticky的位置
        // ① 當元素還沒到達容器頂部時,默認爲0,再判斷是否到達指定sticky位置
        // ② 若是沒到達指定sticky,依然爲0
        // ③ 若是達到指定sticky位置,那麼就計算超過sticky位置後,須要反向transform的距離
        // ④ 這裏默認指定位置是小於元素初始位置的,指定位置大於初始位置的,我想會很奇葩吧。
        if (absY - stickyObj.el.offsetTop > 0 || yy <= stickyObj.top) {
          // 這個公式須要反覆理解一下
          // 當容器往上滾動時,容器的transform是負值,因此咱們反向是正值
          // 容器向上滾動值absY不斷變大,咱們sticky就不斷向下transform
          // stickyObj.el.offsetTop - stickyObj.top 即爲容器滾動多少範圍纔會讓元素到達指定sticky位置
          // 計算iscroll容器的滾動值 - (初始位置 - 指定位置)
          // 當滾動值等於初始位置和指定位置之差時,恰好等於0
          // 隨着滾動值愈來愈大,超過0的部分,即爲須要反向transform的值
          translateY = absY - (stickyObj.el.offsetTop - stickyObj.top);
        } else {
          translateY = 0;
        }
        // 最後拼接瀏覽器前綴,完成css賦值
        stickyObj.el.style[vendor + 'Transform'] = trnOpen + ('0, ' + translateY + 'px') + trnClose;
      });
    },
  };
};
export default extendSticky;
複製代碼

爲了便於理解,我就秀一下Axure的功力。

效果5

好了,上面的iscroll-sticky.js工具已經完成,下面開始使用。

<template>
  <div ref="scroll" class="wrap">
    <div class="scroll-area">
      <div v-for="n in 20" class="item">{{ n }}</div>
      <div ref="sticky" class="sticky" :top="20">21</div>
      <div v-for="n in 20" class="item">{{ n+20 }}</div>
    </div>
  </div>
</template>
<script> import IScroll from 'iscroll/build/iscroll-probe'; import enableSticky from 'path/to/iscroll-sticky.js'; enableSticky(IScroll); // 這一步是將sticky方法掛載到iscroll原型上 export default { data() { scroll: null, }, mounted() { const el = this.$refs.scroll; this.scroll = new IScroll('.wrap', { ... }); const stickyEl = this.$refs.sticky; // 容許元素對象集合sticky this.scroll.enableStickyHeaders([ { el: stickyEl, top: stickyEl.getAttribute('top') // 此處我把top值配置在了原生prop } ]); } } </script>
複製代碼

看效果吧。

效果6

上面的iscroll-sticky.js是個靈活的js,能夠根據本身的需求自行配置修改。

下拉刷新

事實上iscroll自己沒有下拉刷新功能,可是能夠本身實現。

export default {
  data() {
    scroll: null,
    status: 0, // 用一個變量記錄iscroll滾動狀態,默認爲0
    txt: '下拉刷新', // 記錄刷新文本,默認
  },
  watch: {
    status() {
      // 每次iscroll的狀態碼變化時,就要刷新iscroll,以便iscroll從新計算dom元素
      this.iscroll.refresh();
    }
  }
}
複製代碼

而後添加一個刷新文本(或者動畫)

<template>
  <div ref="scroll" class="wrap">
    <div class="scroll-area">
      <div :class="{hide: status===0}" class="refresh">{{ txt }}</div>
      <div v-for="n in 50" class="item">{{ n }}</div>
    </div>
  </div>
</template>
複製代碼
.refresh{
  width: 100%;
  height: 50px;
  line-height: 50px;
  text-align: center;
  &.hide{
    /* 當status爲0默認時,隱藏刷新文本,經過定位到容器外面 */
    position: absolute;
    left: 0;
    top: -50px;
  }
}
複製代碼

接下來監聽下拉iscroll

// ...
this.scroll.on('scroll', e => {
  const y = this.iscroll.y; // 監聽下拉的y值,下拉是正值
  if (y >= 50) { // 當下拉距離>=刷新文本高度時,
    this.status = 1; // 狀態碼變爲1, 表示準備好刷新了
  }
})
複製代碼

這個時候,status的值變爲1,那麼以前被咱們hide的刷新文本,已經變爲正常的內容載入iscroll了,這裏的dom變化須要理解清楚的,關鍵就在於狀態改變後iscroll的刷新,咱們手指並無釋放,因此目前是準備刷新狀態,這個時候須要一個新的監聽,去監聽手指離開而且滾動中止。

this.scroll.on('scroll', e => {
  const y = this.iscroll.y; // 監聽下拉的y值,下拉是正值
  if (y >= 50) { // 當下拉距離>=刷新文本高度時,
    this.txt = '釋放刷新';
    this.status = 1; // 狀態碼變爲1, 表示準備好刷新了
  } else if (y > 0) { // 若是返回了,又不想刷新了,恢復status爲0
	this.txt = '下拉刷新';
	this.status = 0;
  }
})
this.scroll.on('scrollEnd', e => {
  if (status === 1) { // 滾動中止時,若是是準備刷新狀態
    this.txt = '刷新中。。。';
    this.status = 2; // 改變狀態碼,開始刷新
    this.scroll.disable(); // 刷新過程禁止滾動,這個禁用方法視需求而定。
    this.updateData(); // 假設有一個更新數據的method
  }
})
複製代碼
export default {
  methods: {
    updateData() {
      getData().then(_=>{
        // 數據更新完成
        this.txt = '刷新完成';
        // 延遲1秒後繼續隱藏刷新文本
        setTimeout(_=>{
          this.txt = '下拉刷新';
          this.status = 0; // 狀態重置爲0
          this.scroll.enable();
        }, 1000);
      })
    }
  }
}
複製代碼

看下demo的效果:

效果7

這裏附上一個我平時作的一個貓眼電影demo:

效果8

上拉加載

這個也是要本身實現,不過這個很簡單了,判斷滾動觸底便可。

this.scroll.on('scroll', e => {
  // 此處scrollEl是容器高度,contentEl是內容高度,由於y是負值,因此用scrollEl - contentEl
  if (this.scroll.y <= scrollEl.offsetHeight - contentEl.offsetHeight) {
    // do something 上拉加載
  }
});
複製代碼

總結

iscroll是個很靈活的庫,能夠根據本身想要的效果,自由配置。

若是對模塊化比較熟悉,能夠嘗試將sticky,下拉刷新,上拉加載封裝到一個組件中。

評論有人提到better-scroll,沒什麼問題,喜歡哪一個用哪一個。

歡迎點贊收藏,後續和iscroll相關的會及時更新進來。

相關文章
相關標籤/搜索