本集定位 :
第一部分: 骨架屏模板
第二部分: 图片的懒加载组件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
至此才把懒加载写完, 真实累;
不本身作一遍, 本身测一遍各类状况, 真的不知道居然这么麻烦, 但也学到了不少收获满满.
最后仍是但愿与各位同窗一块儿进步, 早日成为真正的大牛, 实现本身的价值!!!
谢谢您的观看,一块儿加油吧💦