第七集: 從零開始實現一套pc端vue的ui組件庫( 懶加載v-lazy )與'骨架屏模板' 組件

第七集: 從零開始實現( 懶加載v-lazy )與'骨架屏模板' 組件

本集定位 :
第一部分: 骨架屏模板
第二部分: 圖片的懶加載組件css

  1. 爲何說是'骨架屏模板', 上一集我有過一些思考, 總的來講, 骨架屏在pc端畢竟只是一個緩衝手段, 不必爲他消耗太多, 什麼100%還原之類的, 本套ui並無這方面的追求, 只是作到儘量的優化便可,因此本ui只是提供模板, 簡單使用便可, 更多精力放在業務上.
  2. 其實本集最主要的是第二部分, 懶加載如今已是項目不可或缺的優化了, 而對於這種有大部分實現方案的技術有必要本身寫一份麼?答案是很是有必要, 就拿本人來講, 經過這個組件的書寫發現了網上大部分人的作法完徹底全是錯的, 可能他們都是copy的某我的的😁, 經過對其的書寫能加強對dom元素的更深理解, 並且能夠由此組件推導出更多工程上可用的優化方案, 歸根結底咱們都是愛學習的仔! 知其然固然也要知其因此然, 那麼此次就讓咱們一塊兒來探索這兩個組件吧🌹.

一: 骨架屏模板
中心思想就是作出幾個樣子的模板, 而後用戶每一個頁面選個模板就行, 那麼須要作的就是這個模板的dom儘量的少, 還有就是要有流光劃過的效果, 以及漸隱的動畫, 出現不必有動畫.vue

第一步: 畫橫線
一條一條的條紋, 如圖所示.
圖片描述node

初學者可能會使用循環div生成條紋, 而工做過的人都有體會, dom是很吃性能的, 這裏選擇box-shadow屬性, 不瞭解這個屬性有多神奇的同窗, 能夠去看張鑫旭的css世界這個本書.git

第二步: 畫公共的圖形github

好比圓形, 方形 這裏最開始使用的僞類來作, 可是很不方便動態的配置各類屬性, 可能會致使組件的可擴展性下降不少, 因此最後沒有選擇使用僞類.設計模式

第三步: 畫金屬光澤瀏覽器

這個原本個人想法是, 三個元素重疊, 第一個元素爲底色, 第二個元素在左側, 一點點變寬, 第三個元素在右側一點點變窄, 反覆重複就會出現條形的漏出第一個元素, 可是這個方案在性能上並不高, 並且能作到的事情不少但都不適合這套組件, 最後否決了這個想法.
如今採用的是一個dom元素, 從左下角傾斜45°的飛向右上角閉包

具體動效,可去觀看個人我的網站
我的網站
圖片描述架構

奉上代碼
這裏有個頗有趣的bug, 就是:style裏面無法使用{}的形式定義box-shadow這個屬性, 因此只能選擇行間的形式.dom

<template>
  <transition name="leave">
    <div class="cc-ske">
      <div class="cc-ske__box">
        <div class="cc-ske__base"
             :style="`box-shadow: ${myShadow}; height: ${height}px;`">
          <!-- 模式一: 單圓 -->
          <template v-if="type === 1">
            <div class="cc-ske__round" />
          </template>
          <!-- 模式二: 多圓 -->
          <template v-else-if="type === 2">
            <div class="cc-ske__round"
                 v-for="i in distanceList"
                 :key='i'
                 :style="{top:i}" />
          </template>
          <!-- 模式三: 表格 -->
          <template v-else-if="type === 3">
            <div class="cc-ske__rec--big" />
          </template>
          <!-- 模式四: 複雜表格 -->
          <template v-else-if="type === 4">
            <div class="cc-ske__round"
                 v-for="i in distanceList"
                 :key='i'
                 :style="{left:i}" />
            <div class="cc-ske__rec" />
          </template>
        </div>
      </div>
      <div class="across" />
    </div>
  </transition>
</template>
props: {
    type: {  // 容許用戶選擇模式, 也就是樣子
      type: Number,
      default: 1
    },
    height: { // 灰色條紋的高度, 由於有的用戶可能須要很細的條紋
      type: Number,
      default: 30
    }
  },

因爲條紋可配置, 因此他的box-shaow屬性就是動態生成的

initClass() {
      let myShadow = "";
      for (let i = 0; i < 30; i++) {
        let h = (this.height + 20) * i;
        // 每次生成一組屬性
        myShadow += `0px ${h}px 0 0 #F6F6F6,`;
      }
      // 去掉','
      this.myShadow = myShadow.slice(0, -1) + ";";
}

好比說模式4 須要多個圓形, 那就作一個圓, 給這個圓shadow屬性

distanceList() {
      let n = this.n,
         result = [];
      while (n--) {
        result.unshift(n * 180 + "px");
      }
      return result;
    }

具體的css代碼
vue-cc-ui/src/style/Ske.scss

@import './common/var.scss';
@import './common/mixin.scss';
@import './common/extend.scss';

// 父級只負責被色的底與定位
@include b(ske) {
    background-color: white;
    @include position(fixed);
    @include e(box) {
    // 裏面爲了與父級有必定的間隙
        overflow: hidden;
        @include position(absolute, 30px);
    }
    @include e(base) {
        background-color: #F6F6F6;
        width: 100%;
        z-index: -1; // 爲了僞類可以被擋住
    }
    @include e(round) {
        display: flex;
        position: absolute;
        align-items: center;
        justify-content: center;
        background-color: white;
        left: 0px;
        width: 180px;
        height: 180px;
        &::after {
            content: '';
            position: absolute;
            background-color: #F6F6F6;
            width: 70%;
            height: 70%;
            border-radius: 50%;
        }
    }

    @include e(rec) {
        position: absolute;
        background-color: #F6F6F6;
        left: 0px;
        bottom: 0;
        right: 0px;
        height: 300px;
        @at-root {
            @include m(big) {
                position: absolute;
                background-color: #F6F6F6;
                border-right: 20px solid white;
                top: 0px;
                left: 0px;
                width: 260px;
                height: 100%;
            }
        }
    }
    .across {
        // 透明的白色, 驚豔了
        background-color: white;
        animation: pass 2s infinite linear;
        width: 30px;
        opacity: 0.8;
        height: 2000px;
    }
}

@keyframes pass {
    0% {
        transform: rotate(-45deg) translate(0px);
    }

    100% {
        transform: rotate(-45deg) translate(2000px);
    }
}

模式2,3,4的展現
圖片描述
圖片描述
圖片描述

二. 重頭戲'懶加載'
這個組件與其餘的組件不一樣, 他只有指令的形式, 沒有dom固然也就沒有style一說,
他的調用必需要use而不能 統一調用, 由於他要傳入配置項.
從怎麼配置他入手
time: 圖片出現多久開始加載;
error:加載失敗時的圖片
loadingImg: 加載時的圖片
有了這些配置咱們纔可以把這個組件作出來

Vue.use(lazy, {
    time: 200,
    error:'xxx.png',
    loadingImg:'xxx.png'
  });

仍是來樣子, 結構仍是與以前的一個套路
圖片描述

vue-cc-ui/src/components/Lazy/main/lazy.js
很簡易的搭建個殼子
分析: 這裏面確定要儲存下有指令的函數, 這就確定須要閉包, 涉及到事件綁定與檢測dom距離body距離等等的方法函數, 因此很適合以類的形式去作, 語義化好, 符合設計模式.

// 先把架構搭好
class Lazy {
// 接收傳過來的參數
  install(Vue, options) {
    this.vm = Vue;
    this.list = new Set(); // 容納全部被指令綁定的元素
    this.timeEl = ''; // 延時器的實例id載體
    this.error = options.error;
    this.time = options.time;
    this.loadingImg = options.loadingImg;
    this.initDirective();
    this.initScroll();
  }
// 固然在初始化的階段要設置這個全局指令;
  initDirective() {
    this.vm.directive('lazy', {
    // 指令怎麼使用或是參數的意義不懂的同窗能夠去官網查閱,很詳細.
      bind: (el, data) => {
      // 若是用戶配了加載圖片, 那麼統一改爲加載狀態
        if(this.loadingImg){
        // 涉及到屬性的修改我的比較喜歡setAttribute 而不是直接賦值, 更語義化.
            el.setAttribute('src', this.loadingImg);
        }
        // 把綁定事件的dom放到組裏面
        // vue-lazy源碼裏面是把value放在屬性上, 而我這裏是分開放的
        this.list.add({ oImg: el, path: data.value });
      }
    });
  }
  initScroll() {
    // 無論怎麼樣, 默認先把body監控起來把
    // 先觸發一次, 第一屏
    this.whetherHandle();
    // 默認狀況下只是綁定監控body的滾動, 這裏面別忘了bind一下, 否則this會改變
    window.addEventListener('scroll', this.whetherHandle.bind(this), false);
  }
  // 具體的渲染相關在這裏作
  whetherHandle(){}
}

export default new Lazy(); // 不傳參的話這個()能夠省略;

何時出發加載, 加載什麼樣的img?

whetherHandle函數的完善

// 並非每次滾動都判斷加載圖片, 而是滾動中止後.
// 圖片在規定時間內一直出如今用戶眼前才加載.
clearTimeout(this.timeEl);
    this.timeEl = setTimeout(() => {
    // 具體的執行我放在下一個函數裏面, 爲了單一職責
      this.handleScroll();
    }, this.time);

handleScroll
挑出真正還在加載中的元素,進行下一步操做;

handleScroll() {
// 要循環遍歷咱們綁定lazy的元素
    for (let item of this.list) {
      // 判斷是否是加載中
      if (this.isNoLoading(item.oImg)) {
      // 只要不是加載中, 通通剔除出Set.
        this.list.delete(item);
      } else {
       // 只有仍是loading中的元素纔會進行真正的判斷
        this.handleSrc(item);
      }
    }
  }
// 工具類,判斷是否是loading狀態
  isNoLoading(item){
    if(!item)return false
    if(item && item.src === this.loadingImg) return false
    return true
  }

handleSrc
思路:

  1. 取得當前body的滾動偏移量, 與高度寬度;
  2. 取得目標元素距離body的距離( 這個很重要, 網上基本上大部分作的都不對 );
  3. 計算當前元素是否出如今視口上
  4. 賦值src
// 處理該不應顯示的問題
// 這裏涉及的比較多, 先看個人思路, 而後再逐一解釋每一個工具類方法
  handleSrc(item) {
    let { oImg, path } = item,
      { top: top1, left: left1 } = getHTMLScroll(oImg),
      { top: top2, left: left2 } = getScrollOffset(),
      { width, height } = getViewportSize(),
      // 漏出一半就開始加載他
      height2 = oImg.offsetHeight / 2,
      width2 = oImg.offsetWidth / 2;
    if (top1 - top2 + height2 > 0 && top1 - top2 + height2 < height) {
      if (left1 - left2 + width2 > 0 && left1 - left2 + width2 < width) {
        oImg.onerror = ()=>{
            oImg.setAttribute('src', this.error);   
        }
        oImg.setAttribute('src', path);
      }
    }
  }

utils裏面的家庭成員
getScrollOffset: 獲取body的上下左右滾動距離.兼容性很好.

function getScrollOffset() {
  if (window.pageXOffset) {
    return {
      left: window.pageXOffset,
      top: window.pageYOffset
    };
  } else {
    // 問題: 爲何要相加
    // 由於這兩個屬性只有一個有用, 另外一個確定是0, 索性直接相加
    return {
      left: document.body.scrollLeft + document.documentElement.scrollLeft,
      top: document.body.scrollTop + document.documentElement.scrollTop
    };
  }
}

getViewportSize: 獲取視口的寬高
兼容是否是'怪異模式'
'怪異模式'這個知識點有興趣能夠去查查

function getViewportSize() {
  if (window.innerHeight) {
    return {
      width: window.innerWidth,
      height: window.innerHeight
    };
  } else {
    if (document.compatMode === 'BackCompat') {
      return {
        width: document.body.clientWidth,
        height: document.body.clientHeight
      };
    } else {
      return {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      };
    }
  }
}

重頭戲!!
getHTMLScroll: 獲取元素到body的距離
網上有不少這方面的文章, 大多隻是用offsetParent與offsetTop 這兩種屬性來作的,能夠這麼說他們都錯了!!
想要知道爲何錯以及有什麼坑咱們逐一探索.

  1. offsetParent是什麼?
    與當前元素最近的通過定位(position不等於static)的父級元素
    也就是說, 他並非元素的父級, 而是第一個有定位的父級, 有個大坑下面會講.
  2. offsetTop與offsetLeft
    若是父元素不是body元素且設置了position屬性時,offsetLeft爲元素邊框外側到父元素邊框內側的距離

坑點:

  1. 看起來貌似很完美, 一個找最近的定位父級a, 一個獲取這個元素到a的距離, 可是他們全都忽略了滾動!! 好比你這個元素原本不在'視野'內, 可是他的父級滾動的時候把它暴露了出來, 那他也算是出如今視口內, 上面的方式就徹底作不到這一點了, 並且還要顧及到多層父級都有滾動屬性, 有的父級有滾動沒定位, 有的有滾動有定位
  2. 這個offsetParent的一個坑, 不多有人兼容他, 說明囫圇吞棗得人不少, 在元素定位爲fixed的時候, 瀏覽器會把它脫離定位, 也就是他是沒有父級這一說的, 因此他的offsetParent是個null, 也就是永遠找不到body身上.

具體實現代碼以下

export function getHTMLScroll(node) {
  if (!node) return;// 啥也沒傳就別玩了
  let result = { top: 0, left: 0 },
    parent = node.offsetParent||node.parentNode,// 獲取第一個定位元素,防止img自己就是fiexd定位元素
    children = node; // 記錄下子集
  let task = son => {
// 真正獲取的元素是父級,而不是定位父級 !!!
    let dom = son.parentNode;
    if (!dom) return; // 沒有就別玩了
    // 這裏是關鍵---當本次獲取的父級是第一個定位父級時
    if (parent === dom) {
    // 拿到父級的滾動偏移量
      let domScrollTop = dom.scrollTop || 0,
        domScrollLeft = dom.scrollLeft || 0;
        // 用子集距離第一個定位父級的距離減去父級的滾動偏移
      result.top += children.offsetTop - domScrollTop;
      result.left += children.offsetLeft - domScrollLeft;
      // 賦予新的子集
      children = parent;
      // 賦予新的定位父級
      parent = dom.offsetParent; // 下一個父級
    } else {
    // 這裏是關鍵---當本次獲取的父級是否是定位父級時
      let domScrollTop = dom.scrollTop || 0,
        domScrollLeft = dom.scrollLeft || 0;
        // 不用子集的offsetTop 這裏不涉及定位距離的計算
      result.top -= domScrollTop;
      result.left -= domScrollLeft;
    }
     // 碰到body就結束了
    if (dom.nodeName !== 'BODY') {
      task(dom);
    }
  };
  task(node);
  return result;
}

初版忘寫了, 自定義父級監聽
不少時候並非要監聽body, 而是監聽指定的父級的scroll事件
用戶在dom上寫上指令 v-lazy-box, 就能夠監聽這個元素了,

this.vm.directive('lazy-box', {
      bind: el => {
      // 觸發第一次監控, 由於可能dom是v-if狀態, 不知道何時出現;
        this.whetherHandle();
        // 與以前相同
        el.addEventListener('scroll', this.whetherHandle.bind(this), false);
      }
    });

end
至此才把懶加載寫完, 真實累;
不本身作一遍, 本身測一遍各類狀況, 真的不知道居然這麼麻煩, 但也學到了不少收穫滿滿.
最後仍是但願與各位同窗一塊兒進步, 早日成爲真正的大牛, 實現本身的價值!!!
謝謝您的觀看,一塊兒加油吧💦

我的學習博客:我的網站
github:github

相關文章
相關標籤/搜索