vue-lazyload除了图片赖加载还能够做什么

1,442 阅读3分钟

众所周知,vue-lazyload是一款支持vue2.x和vue1.x的图片赖加载库,不过在写这篇文章的时候,社区相关开发者已经fork了vue3的版本,可以直接用V3了。本文就如何实现做一下讲解,以及除了图片赖加载我们还能做什么。

目录

  • vue-lazyload如何实现
  • vue-lazyload如何实现埋点需求
  • 如何实现vue-lazyload的vue3版本

如何实现

其库大概实现流程如下

image.png

  • 绑定指令
  • 添加当前元素监听队列
    • 监听元素滚动模式
    • 使用MutationObserver来监听元素
  • 加载过程中设置图片状态 loadingSrc loadedSrc loaderrorSrc 包括vue-lazyload中的filter,adapter等也是在初始化lisener.js中的ReactiveListener实例中或者执行其load的时候去执行的。关键代码如下
  load (onFinish = noop) {
    if ((this.attempt > this.options.attempt - 1) && this.state.error) {
      if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`)
      onFinish()
      return
    }
    if (this.state.rendered && this.state.loaded) return
    if (this._imageCache.has(this.src)) {
      this.state.loaded = true
      this.render('loaded', true)
      this.state.rendered = true
      return onFinish()
    }

    this.renderLoading(() => {
      this.attempt++

      this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options)
      this.record('loadStart')

      loadImageAsync({
        src: this.src,
        cors: this.cors
      }, data => {
        this.naturalHeight = data.naturalHeight
        this.naturalWidth = data.naturalWidth
        this.state.loaded = true
        this.state.error = false
        this.record('loadEnd')
        this.render('loaded', false)
        this.state.rendered = true
        this._imageCache.add(this.src)
        onFinish()
      }, err => {
        !this.options.silent && console.error(err)
        this.state.error = true
        this.state.loaded = false
        this.render('error', false)
      })
    })
  }

实现埋点需求

上面我们已经讲述了vue-lazyload的实现,可知vue-lazyload针对图片实现的主要功能有两点

  • 对图片的懒加载,只包含img标签,且只读写img标签的src属性
  • 对组件赖加载,判定组件再视图区域,然后加载(或者开启MutationObserver API)

日常开发中,埋点大体分为两类

  • 点击埋点
  • 曝光埋点 对于点击埋点和页面曝光很好处理,但对于元素曝光,就显得不是那么好处理了。其实处理元素曝光的解决方案再HTML5规范里有一个mutationObserve的api,大概使用方法如下
      <div ref="observe" class="observe-li"></div>
      ...
      import 'intersection-observer'
      const observes = []
      Vue.$nextTick(() => {
        const obserRef = new IntersectionObserver(entries => {
          entries.forEach(v => {
            const { item } = v.target.dataset
            const itemData = JSON.parse(item)
            if (observes.has(itemData.id)) {
              obserRef.unobserve(v.target)
            } else if (v.isIntersecting) {
              // @todo 做埋点
            }
          })
        })
        const items = document.querySelectorAll('.observe-li')??[]
        items.forEach(item => obserRef.observe(item))
     })

但其缺点也很明显:

  • 第一:每次都要往dom标签里也埋点值
  • 第二:有元素曝光的地方,每次都要写这一套东西,就很烦
    那么我们能不能利用vue-lazyload的特性把埋点功能给集成进去呢。答案是可以的,我们可以对其业务场景做一个简单的实现。
步骤如下
  • 注册自定义的指令, 曝光事件v-view自定义方法为@view
  • 添加一个listenner的实例,我们可以模仿lazyImage来写一个lazyDom文件来监听视图元素是否进入视图中,上报相应的埋点数据
  <div @view="uploadTracking({id:2})" v-view></div>
  ...
  methods:{
      uploadTracking(options){
          @TODO 埋点
      }
  }

lazyDom如下

    import { CustomEvent } from './util'
export default function (lazy, { preLoad = 1.3, preLoadTop = 0 ,domTypes= []}) {
  return class LazyDom {
    constructor({ el, viewParams = {} }) {
      this.eleLoaded = false
      this.state = {
        inited: false,
        loaded: false
      }
      console.log(el)
      this.$el = el
      this.el = el
      this.viewParams = viewParams
      this.viewed = false
      this.options = {
        preLoad: preLoad,
        preLoadTop: preLoadTop,
        domTypes:[]
      }
      this.destroy = false
    }

    addLazyDom() {
      if (!this.state.inited) {
        this.state.inited = true
        // 添加listenner
        lazy.addLazyBox(this)
        lazy.lazyLoadHandler()
      }
    }
    getRect() {
      this.rect = this.$el.getBoundingClientRect()
    }
    update() { }
    checkInView() {
      this.getRect()
      return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) &&
        (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0)
    }
    load() {
      if (!this.state.loaded) {
        this.state.loaded = true
        this.triggerEleView()
      }
    }
    $destroy() {
      this.$el = null
    }
    triggerEleView() {
      if (!this.viewed && this.state.loaded) {
        const event = new CustomEvent('view', {
          detail: {}
        })
        this.$el.dispatchEvent(event)
        this.viewed = true
      }
    }
  }
}

实现vue-lazyload的vue3版本

这里面主要是指令的挂载形式变了,所以我们只需要关注指令的差异即可。差异如下表格

vue2-
bind只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind只调用一次,指令与元素解绑时调用
vue3---
created在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加在普通的 v-on 事件监听器调用前的事件监听器中时,这很有用。
beforeMount当指令第一次绑定到元素并且在挂载父组件之前调用。
mounted在绑定元素的父组件被挂载后调用。
beforeUpdate在更新包含组件的 VNode 之前调用。
updated在包含组件的 VNode 及其子组件的 VNode 更新后调用。
beforeUnmount在卸载绑定元素的父组件之前调用
unmounted当指令与元素解除绑定且父组件已卸载时,只调用一次

可以得出其对应关系

  • bind--->created
  • inserted--->mounted
  • update--->beforeUpdate
  • componentUpdated--->updated
  • unbind--->unmounted

已vue-lazy的v-lazy为例,改成如下即可

 // vue2 版本
 Vue.directive('lazy', {
    bind: lazy.add.bind(lazy),
    update: lazy.update.bind(lazy),
    componentUpdated: lazy.lazyLoadHandler.bind(lazy),
    unbind: lazy.remove.bind(lazy)
 })
 // vue3版本
 Vue.directive('lazy', {
    bind: lazy.add.bind(lazy),
    update: lazy.update.bind(lazy),
    updated: lazy.lazyLoadHandler.bind(lazy),
    unbind: lazy.remove.bind(lazy)
 })

vue3版lazyload源码传送门github.com/lecheng-lc/…