目录
项目中用到了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 传值