vue3-自定义指令-图片懒加载
1. 效果展示
2. 类型文件 - types/lazyImage.ts
import type { ObjectDirective } from 'vue'
export type LazyImageDirective = ObjectDirective
export interface MapItem {
intersectionObserver: IntersectionObserver
callback: (...args: any) => void
}
export type IntersectionObserverOptions = {
root: Element | null
rootMargin: string
threshold: number
}
export type ExtraOptions = {
loadingSrc?: string
loadingTime?: number
show: (...args: any) => void
hide: (...args: any) => void
}
export type BindingValue = ExtraOptions & {
src: string
options?: IntersectionObserverOptions
}
export type LazyImageOptions = ExtraOptions & {
options: IntersectionObserverOptions
}
3. 全局配置 config/index.ts
import { LazyImageOptions} from '@/types/lazyImage'
export interface VDConfig {
lazyImage?: LazyImageOptions
}
const config: VDConfig = {
lazyImage: {
loadingTime: 500,
show: () => {},
hide: () => {},
options: {
root: null,
rootMargin: '0px 0px 0px 0px',
threshold: 0.0
}
}
}
export default config
4. 指令文件 - lazyImage.ts
import { LazyImageDirective, BindingValue, MapItem } from '@/types/lazyImage'
import config from '@/config'
import message from '@/message'
// 懒加载
const lazyImageEls = new Map<HTMLElement, MapItem>()
const copyDirective: LazyImageDirective = {
mounted(el: HTMLElement, binding: any) {
if (lazyImageEls.has(el)) return
const arg = binding.arg || 'image'
let bindingValue: BindingValue = binding.value
const bindingValueType = Object.prototype.toString.call(bindingValue)
const { once } = binding.modifiers
let tagName = el.tagName.toLowerCase() || ''
let src = ''
if (bindingValueType === '[object String]') {
src = bindingValue as unknown as string
} else if (bindingValueType === '[object Object]') {
src = bindingValue.src
bindingValue = { ...config.lazyImage, ...bindingValue }
} else {
return message.warning('binding 只能绑定字符串或者对象')
}
// setSrc
const setSrc = (rSrc: string) => {
if (arg === 'image') {
;(el as HTMLImageElement).src = rSrc
} else if (arg === 'background') {
el.style.background = `url(${rSrc}) 100%/cover`
}
}
// 替换src
const replaceSrc = () => {
setSrc(bindingValue.loadingSrc!)
const startTime = performance.now()
let time = bindingValue.loadingTime || 0
time = isNaN(Number(time)) ? 500 : Number(time)
const img = new Image()
img.src = src
img.onload = () => {
const endTime = performance.now()
let useTime = endTime - startTime
const loadingTime = useTime > time ? 0 : time - useTime
setTimeout(() => {
setSrc(src)
}, loadingTime)
}
}
// 回调
const callback = (entries: Array<IntersectionObserverEntry>) => {
const isIntersecting = entries[0].isIntersecting
if (isIntersecting) {
if (once) {
lazyImageEls.get(el)!.intersectionObserver.unobserve(el)
}
if (bindingValue.loadingSrc) {
replaceSrc()
} else {
setSrc(src)
}
bindingValue.show && bindingValue.show()
} else {
bindingValue.hide && bindingValue.hide()
}
}
// value
if (arg !== 'image' && arg !== 'background') {
return message.warning('仅支持 image 和 background')
} else if (arg === 'image' && tagName !== 'img') {
return message.warning('image 仅配合 img 标签使用')
} else {
const intersectionObserver = new IntersectionObserver(callback, bindingValue.options)
intersectionObserver.observe(el)
lazyImageEls.set(el, {
intersectionObserver,
callback
})
}
},
beforeUnmount(el: HTMLElement) {
lazyImageEls.get(el)!.intersectionObserver.unobserve(el)
}
}
export default copyDirective
5. 对外暴露 - v-directives/index.ts
import type { App } from 'vue'
import lazyImage from '@/directives/lazyImage'
type Directive = 'lazyImage'
type Directives = {
[key in Directive]: Object
}
// 指令
const directives: Directives = { lazyImage }
// 注入函数
const useD = (app: App, directives: UseDirectivesObj) => {
Object.keys(directives).forEach(key => {
app.directive(key, directives[key as keyof UseDirectivesObj] as any)
})
}
// 注入
const vDirectives = {
install(app: App) {
useD(app, directives)
}
}
export default vDirectives
6. 指令引入 - main.ts
import { vDirectives } from '@/v-directives/index.ts'
// ...
app.use( vDirectives )
// ...
7. 指令使用
基础语法:v-lazyImage:支持的图片用途.修饰符='选项'
修饰符:
- once:元素与可视窗口交叉后移除监听该元素
选项:
- 字符串:src地址
- 对象:
- src:src地址
- loadingSrc:图片加载完成前的替代图片
- loadingTime:图片加载完成到替换的的最大缓冲时间
- show:可视范围外到可视范围内时的回调 () => {}
- hide:可视范围内到可视范围外时的回调 () => {}
- options:具体查看 - IntersectionObserver
- root:视口元素
- rootMargin:一个在计算交叉值时添加至根的边界盒,可以有效的缩小或扩大根的判定范围从而满足计算需要,语法和 CSS 中的margin 属性等同
- threshold:规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.0 到 1.0 之间的数组
支持的图片用途:
- image
- background