第七集: 从零开始实现一套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

相关文章
相关标签/搜索