自定义指令

目录

一、自定义指令

二、v-lazy

注册和使用

vue3-lazy插件实现

插件注册

指令实现

相关链接

三、v-loading

功能分析

具体实现

1.loading.vue

2.创建loading指令对象(directive.js)

3.注册并使用v-loading


项目中用到了v-lazy指令和v-loading自定义指令,笔者在学习的时候非常困惑这到底是什么鬼,所以查阅资料整理了下思路,发现自定义指令真的太好用了!

一、自定义指令

目的:对底层的DOM进行操作,可复用(注意最好数据不会发生改变)

IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

Vue3 自定义指令文档: https://v3.cn.vuejs.org/guide/custom-directive.html

二、v-lazy

        懒加载,当图片出现在可视范围内的时候再进行加载

        使用vue3-lazy插件

注册和使用

import lazyPlugin from 'vue3-lazy'

createApp(App).use(lazyPlugin, {
  loading: require('@/assets/images/default.png')  //默认加载的图片
})
<img v-lazy="item.pic">

vue3-lazy插件实现

插件注册

        为了实现复用,将其做成插件。

const lazyPlugin = {
  install (app, options) {
    app.directive('lazy', {
      // 指令对象
    })
  }
}

export default lazyPlugin

        引入并挂载插件

import { createApp } from 'vue'
import App from './App.vue'
import lazyPlugin from 'vue3-lazy'

createApp(App).use(lazyPlugin, {
  // 添加一些配置参数
})

        当app实例use时,则会执行插件中的install()函数,函数内部通过app.directive来注册全局指令

指令实现

(1)图片的管理(ImageManager 类)

  • 管理图片的 DOM、真实的 src、预加载的 url、加载的状态以及图片的加载。(初始化)
  • 把它对应的 img 标签的 src 执行加载中的图片 loading,这就相当于默认加载的图片
const State = {
  loading: 0,
  loaded: 1,
  error: 2
}

export class ImageManager {
  constructor(options) {
    this.el = options.el
    this.src = options.src
    this.state = State.loading
    this.loading = options.loading
    this.error = options.error
    
    this.render(this.loading)
  }
  render() {
    this.el.setAttribute('src', src)
  }
  load(next) {
    //判断图片加载的状态,如果仍正在加载,则会加载其真实的src
    if (this.state > State.loading) {
      return
    }
    this.renderSrc(next)
  }
  renderSrc(next) {
    loadImage(this.src).then(() => {
      this.state = State.loaded
      this.render(this.src)
      next && next()
    }).catch((e) => {
      this.state = State.error
      this.render(this.error)
      console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)
      next && next()
    })
  }
}
  • loadImage:图片预加载技术实现请求src图片,成功之后再替换src修改状态
export default function loadImage (src) {
  return new Promise((resolve, reject) => {
    const image = new Image()

    image.onload = function () {
      resolve()
      dispose()
    }

    image.onerror = function (e) {
      reject(e)
      dispose()
    }
    //成功之后替换src标签
    image.src = src
    //修改状态
    function dispose () {
      image.onload = image.onerror = null
    }
  })
}

(2)可视区的判断(Lazy 类

判断图片是否进入可视区域以及对多个图片的管理器的管理

IntersectionObserver API:当一个 IntersectionObserver 对象被创建时,其被配置为监听根中一段给定比例的可见区域

当图片元素绑定v-lazy指令,且在 mounted 钩子函数执行的时候,就会执行add方法

  • 参数:
    • el:图片对应的 DOM 元素对象
    • binding:指令对象绑定的值(item.pic)
      <img class="avatar" v-lazy="item.pic">
add(el, binding) {
    const src = binding.value
    const manager = new ImageManager({
      el,      
      src,
      loading: this.loading,
      error: this.error
    })
    this.managerQueue.push(manager)
    this.observer.observe(el)
  }

(3)完整代码

const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

export default class Lazy {
  constructor(options) {
    this.managerQueue = []
    this.initIntersectionObserver()
    
    this.loading = options.loading || DEFAULT_URL
    this.error = options.error || DEFAULT_URL
  }
  add(el, binding) {
    const src = binding.value
    const manager = new ImageManager({
      el,      
      src,
      loading: this.loading,
      error: this.error
    })
    this.managerQueue.push(manager)
    this.observer.observe(el)
  }
  initIntersectionObserver() {
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const manager = this.managerQueue.find((manager) => {
            return manager.el === entry.target
          })
          if (manager) {
            if (manager.state === State.loaded) {
              this.removeManager(manager)
              return
            }
            manager.load()
          }
        }
      })
    }, {
      rootMargin: '0px',
      threshold: 0
    })
  }
  removeManager(manager) {
    const index = this.managerQueue.indexOf(manager)
    if (index > -1) {
      this.managerQueue.splice(index, 1)
    }
    if (this.observer) {
      this.observer.unobserve(manager.el)
    }
  }
}

const lazyPlugin = {
  install (app, options) {
    const lazy = new Lazy(options)

    app.directive('lazy', {
      mounted: lazy.add.bind(lazy)
    })
  }
}

(4)指令优化

在实现图片的真实 url 的加载过程中,我们使用了 loadImage 做图片预加载,那么显然对于相同 url 的多张图片,预加载只需要做一次即可。

为了实现上述需求,我们可以在 Lazy 模块内部创建一个缓存 cache:

export default class Lazy {
  constructor(options) {
    // ...
    this.cache = new Set()
  }
}

然后在创建 ImageManager 实例的时候,把该缓存传入:

const manager = new ImageManager({
  el,
  src,
  loading: this.loading,
  error: this.error,
  cache: this.cache
})

然后对 ImageManager 做如下修改:

export default class ImageManager {
  load(next) {
    if (this.state > State.loading) {
      return
    }
    if (this.cache.has(this.src)) {
      this.state = State.loaded
      this.render(this.src)
      return
    }
    this.renderSrc(next)
  }
  renderSrc(next) {
    loadImage(this.src).then(() => {
      this.state = State.loaded
      this.render(this.src)
      next && next()
    }).catch((e) => {
      this.state = State.error
      this.cache.add(this.src)
      this.render(this.error)
      console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)
      next && next()
    })  
  }
}

在每次执行 load 前从缓存中判断是否已存在,然后在执行 loadImage 预加载图片成功后更新缓存。

通过这种空间换时间的手段,就避免了一些重复的 url 请求,达到了优化性能的目的。

相关链接

参考:Vue3 实现图片懒加载的自定义指令 v-lazy - 知乎 (zhihu.com)

IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

vue3-lazy: https://github.com/ustbhuangyi/vue3-lazy

《Vue3 开发高质量音乐 Web app》:https://coding.imooc.com/class/503.html

Vue3 自定义指令文档: https://v3.cn.vuejs.org/guide/custom-directive.html

三、v-loading

功能分析

        页面在数据异步请求等待的时候会出现短暂的白屏,可以添加一个loading效果(如加载转圈图)提高用户体验,这个效果被封装在loading组件当中。而v-loading实际上就是将这个loading组件加载到某个<div>上,使得在在没有拿到数据的时候呈现loading组件中<template>中的效果,拿到数据之后就正常呈现<div>中的效果。在这里我们可以true/false来判断显示哪个效果

具体实现

1.loading.vue

<template>
  <div class="loading">
    <div class="loading-content">
      <img width="24" height="24" src="./loading.gif">
      <p class="desc">{{title}}</p>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'loading',
    data() {
      return {
        title: '正在载入...'
      }
    },
    methods: {
      setTitle(title) {
        this.title = title
      }
    }
  }
</script>

2.创建loading指令对象(directive.js)

分析

        模块化:将longding指令对象封装成一个函数来进行调用。这样可以通过createLoadingLikeDirective函数传入不同的值来实现加载不同的loading效果组件(很6)

        对象中主要包括mounted和updated两个生命周期函数,其均有两个参数值:

  • el:指令要绑定到的元素,可以用于直接操作dom对象
  • binding:对象,包含一些属性
    {
        args:loadingText    // 传递给指令的参数,这里注意传的是动态参数[loadingText]
        value:loading       //传递给指令的值
    }

具体实现:

(1)mounted初始化和挂载

creatApp创建vue实例,并将根组件设置为comp

const app = createApp(Comp)

动态创建div,并将app挂载到div上去

const instance = app.mount(document.createElement('div'))
el.instance = instance

注意这里并不是body中的dom,因为它最终是需要挂载到el的dom上,而挂载需要判断传入的boolean值

if (binding.value) {
        append(el)
      }

append函数中实现挂载

el.appendChild(el[name].instance.$el)
  • father.appendChild(son) 方法将节点(元素)作为最后一个子元素添加到元素
  • el.instance:创建的挂载到div上的、根组件为comp(loading组件)的vue实例
  • el[name].instance.$el:vue实例对象(loading组件)对应的的dom对象
  • el:loading组件作用的dom上

(2)updated

根据是否拿到数据进行添加或移除操作。true:append  false:remove(和append逻辑相反)

if (binding.value !== binding.oldValue) {
        binding.value ? append(el) : remove(el)
      }
el.removeChild(el[name].instance.$el)

(3)优化

设置loading转圈图实现在当前页面水平居中(或者任意位置),提高自定义指令的通用性

动态拿到要挂在的dom上的style

const style = getComputedStyle(el)

判断style中的position是否具有三者之一的样式,如果没有则添加一个relative样式

if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
    addClass(el, relativeCls)
}

(5)传参

使用binding.arg属性,这个可以用于传入具体的加载文字(根据需求进行调整)

const title = binding.arg
      if (typeof title !== 'undefined') {
        instance.setTitle(title)
      }

(6)完整代码

import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'

const relativeCls = 'g-relative'

export default function createLoadingLikeDirective(Comp) {
  return {
    mounted(el, binding) {
      const app = createApp(Comp)
      const instance = app.mount(document.createElement('div'))
      const name = Comp.name
      if (!el[name]) {
        el[name] = {}
      }
      el[name].instance = instance
      const title = binding.arg
      if (typeof title !== 'undefined') {
        instance.setTitle(title)
      }
      if (binding.value) {
        append(el)
      }
    },
    updated(el, binding) {
      const title = binding.arg
      const name = Comp.name
      if (typeof title !== 'undefined') {
        el[name].instance.setTitle(title)
      }
      if (binding.value !== binding.oldValue) {
        binding.value ? append(el) : remove(el)
      }
    }
  }

  function append(el) {
    const name = Comp.name
    const style = getComputedStyle(el)
    if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
      addClass(el, relativeCls)
    }
    el.appendChild(el[name].instance.$el)
  }

  function remove(el) {
    const name = Comp.name
    removeClass(el, relativeCls)
    el.removeChild(el[name].instance.$el)
  }
}

3.注册并使用v-loading

createApp(App).directive('loading', loadingDirective).mount('#app')
<div v-loading:[loadingText]="loading"></div>

loading值为true,显示loading;false,正常显示

:[loadingText]    动态传参

loading   传值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值