本集定位 :
第一部分: 骨架屏模板
第二部分: 圖片的懶加載組件css
一: 骨架屏模板
中心思想就是作出幾個樣子的模板, 而後用戶每一個頁面選個模板就行, 那麼須要作的就是這個模板的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
思路:
// 處理該不應顯示的問題 // 這裏涉及的比較多, 先看個人思路, 而後再逐一解釋每一個工具類方法 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 這兩種屬性來作的,能夠這麼說他們都錯了!!
想要知道爲何錯以及有什麼坑咱們逐一探索.
坑點:
具體實現代碼以下
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
至此才把懶加載寫完, 真實累;
不本身作一遍, 本身測一遍各類狀況, 真的不知道居然這麼麻煩, 但也學到了不少收穫滿滿.
最後仍是但願與各位同窗一塊兒進步, 早日成爲真正的大牛, 實現本身的價值!!!
謝謝您的觀看,一塊兒加油吧💦