vue3+typescript实现图片懒加载插件


Posted in Javascript onOctober 26, 2020

github项目地址: github.com/murongg/vue…

求star 与 issues

我文采不好,可能写的文章不咋样,有什么问题可以在留言区评论,我会尽力解答

本项目已经发布到npm

安装:

$ npm i vue3-lazyload
# or
$ yarn add vue3-lazyload

需求分析

  • 支持自定义 loading 图片,图片加载状态时使用此图片
  • 支持自定义 error 图片,图片加载失败后使用此图片
  • 支持 lifecycle hooks,类似于 vue 的生命周期,并同时在 img 标签绑定 lazy 属性,类似于
<img src="..." lazy="loading">
<img src="..." lazy="loaded">
<img src="..." lazy="error">

并支持:

img[lazy=loading] {
  /*your style here*/
 }
 img[lazy=error] {
  /*your style here*/
 }
 img[lazy=loaded] {
  /*your style here*/
 }

支持使用 v-lazy 自定义指令,指定可传入 string/object ,当为 string 时,默认为需要加载的 url,当为 object 时,可传入

  • src: 当前需要加载的图片 url
  • loading: 加载状态时所用到的图片
  • error: 加载失败时所用到的图片
  • lifecycle: 本次 lazy 的生命周期,替换掉全局生命周期

目录结构

- src
---- index.ts 入口文件,主要用来注册插件
---- lazy.ts 懒加载主要功能
---- types.ts 类型文件,包括 interface/type/enum 等等
---- util.ts 共享工具文件

编写懒加载类

懒加载主要通过 IntersectionObserver对象实现,可能有些浏览器不支持,暂未做兼容。

确定注册插件时传入的参数

export interface LazyOptions {
 error?: string; // 加载失败时的图片
 loading?: string; // 加载中的图片
 observerOptions?: IntersectionObserverInit; // IntersectionObserver 对象传入的第二个参数
 log?: boolean; // 是否需要打印日志
 lifecycle?: Lifecycle; // 生命周期 hooks
}

export interface ValueFormatterObject {
 src: string,
 error?: string,
 loading?: string,
 lifecycle?: Lifecycle;
}

export enum LifecycleEnum {
 LOADING = 'loading',
 LOADED = 'loaded',
 ERROR = 'error'
}

export type Lifecycle = {
 [x in LifecycleEnum]?: () => void;
};

确定类的框架

vue3 的 Custom Directives,支持以下 Hook Functions:beforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted,具体释义可以去 vue3 文档查看,目前仅需要用到mountedupdatedunmounted,这三个 Hook。

Lazy 类基础框架代码,lazy.ts

export default class Lazy {
 public options: LazyOptions = {
  loading: DEFAULT_LOADING,
  error: DEFAULT_ERROR,
  observerOptions: DEFAULT_OBSERVER_OPTIONS,
  log: true,
  lifecycle: {}
 };
 constructor(options?: LazyOptions) {
  this.config(options)   
 }
 
 /**
  * merge config
  * assgin 方法在 util.ts 文件内,此文章不在赘述此方法代码,可在后文 github 仓库内查看此代码
  * 此方法主要功能是合并两个对象
  *
  * @param {*} [options={}]
  * @memberof Lazy
  */
 public config(options = {}): void {
  assign(this.options, options)
 }
 
	public mount(el: HTMLElement, binding: DirectiveBinding<string | ValueFormatterObject>): void {} // 对应 directive mount hook
 public update() {} // 对应 directive update hook
 public unmount() {} // 对应 directive unmount hook
}

编写懒加载功能

/**
  * mount
  *
  * @param {HTMLElement} el
  * @param {DirectiveBinding<string>} binding
  * @memberof Lazy
  */
 public mount(el: HTMLElement, binding: DirectiveBinding<string | ValueFormatterObject>): void {
  this._image = el
  const { src, loading, error, lifecycle } = this._valueFormatter(binding.value)
  this._lifecycle(LifecycleEnum.LOADING, lifecycle)
  this._image.setAttribute('src', loading || DEFAULT_LOADING)
  if (!hasIntersectionObserver) {
   this.loadImages(el, src, error, lifecycle)
   this._log(() => {
    throw new Error('Not support IntersectionObserver!')
   })
  }
  this._initIntersectionObserver(el, src, error, lifecycle)
 }
 
 /**
  * force loading
  *
  * @param {HTMLElement} el
  * @param {string} src
  * @memberof Lazy
  */
 public loadImages(el: HTMLElement, src: string, error?: string, lifecycle?: Lifecycle): void {
  this._setImageSrc(el, src, error, lifecycle)
 }

 /**
  * set img tag src
  *
  * @private
  * @param {HTMLElement} el
  * @param {string} src
  * @memberof Lazy
  */
 private _setImageSrc(el: HTMLElement, src: string, error?: string, lifecycle?: Lifecycle): void {    
  const srcset = el.getAttribute('srcset')
  if ('img' === el.tagName.toLowerCase()) {
   if (src) el.setAttribute('src', src)
   if (srcset) el.setAttribute('srcset', srcset)
   this._listenImageStatus(el as HTMLImageElement, () => {
    this._log(() => {
     console.log('Image loaded successfully!')
    })
    this._lifecycle(LifecycleEnum.LOADED, lifecycle)
   }, () => {
    // Fix onload trigger twice, clear onload event
    // Reload on update
    el.onload = null
    this._lifecycle(LifecycleEnum.ERROR, lifecycle)
    this._observer.disconnect()
    if (error) el.setAttribute('src', error)
    this._log(() => { throw new Error('Image failed to load!') })
   })
  } else {
   el.style.backgroundImage = 'url(\'' + src + '\')'
  }
 }

 /**
  * init IntersectionObserver
  *
  * @private
  * @param {HTMLElement} el
  * @param {string} src
  * @memberof Lazy
  */
 private _initIntersectionObserver(el: HTMLElement, src: string, error?: string, lifecycle?: Lifecycle): void {  
  const observerOptions = this.options.observerOptions
  this._observer = new IntersectionObserver((entries) => {   
   Array.prototype.forEach.call(entries, (entry) => {
    if (entry.isIntersecting) {
     this._observer.unobserve(entry.target)
     this._setImageSrc(el, src, error, lifecycle)
    }
   })
  }, observerOptions)
  this._observer.observe(this._image)
 }

 /**
  * only listen to image status
  *
  * @private
  * @param {string} src
  * @param {(string | null)} cors
  * @param {() => void} success
  * @param {() => void} error
  * @memberof Lazy
  */
 private _listenImageStatus(image: HTMLImageElement, success: ((this: GlobalEventHandlers, ev: Event) => any) | null, error: OnErrorEventHandler) {
  image.onload = success 
  image.onerror = error
 }

 /**
  * to do it differently for object and string
  *
  * @public
  * @param {(ValueFormatterObject | string)} value
  * @returns {*}
  * @memberof Lazy
  */
 public _valueFormatter(value: ValueFormatterObject | string): ValueFormatterObject {
  let src = value as string
  let loading = this.options.loading
  let error = this.options.error
  let lifecycle = this.options.lifecycle
  if (isObject(value)) {
   src = (value as ValueFormatterObject).src
   loading = (value as ValueFormatterObject).loading || this.options.loading
   error = (value as ValueFormatterObject).error || this.options.error
   lifecycle = ((value as ValueFormatterObject).lifecycle || this.options.lifecycle)
  }
  return {
   src,
   loading,
   error,
   lifecycle
  }
 }

 /**
  * log
  *
  * @param {() => void} callback
  * @memberof Lazy
  */
 public _log(callback: () => void): void {
  if (!this.options.log) {
   callback()
  }
 }

 /**
  * lifecycle easy
  *
  * @private
  * @param {LifecycleEnum} life
  * @param {Lifecycle} [lifecycle]
  * @memberof Lazy
  */
 private _lifecycle(life: LifecycleEnum, lifecycle?: Lifecycle): void {      
  switch (life) {
  case LifecycleEnum.LOADING:
   this._image.setAttribute('lazy', LifecycleEnum.LOADING)
   if (lifecycle?.loading) {
    lifecycle.loading()
   }
   break
  case LifecycleEnum.LOADED:
   this._image.setAttribute('lazy', LifecycleEnum.LOADED)
   if (lifecycle?.loaded) {
    lifecycle.loaded()
   }
   break
  case LifecycleEnum.ERROR:
   this._image.setAttribute('lazy', LifecycleEnum.ERROR)   
   if (lifecycle?.error) {
    lifecycle.error()
   }
   break
  default:
   break
  }
 }

编写 update hook

/**
  * update
  *
  * @param {HTMLElement} el
  * @memberof Lazy
  */
 public update(el: HTMLElement, binding: DirectiveBinding<string | ValueFormatterObject>): void {  
  this._observer.unobserve(el)
  const { src, error, lifecycle } = this._valueFormatter(binding.value)
  this._initIntersectionObserver(el, src, error, lifecycle)
 }

编写 unmount hook

/**
  * unmount
  *
  * @param {HTMLElement} el
  * @memberof Lazy
  */
 public unmount(el: HTMLElement): void {
  this._observer.unobserve(el)
 }

在 index.ts 编写注册插件需要用到的 install 方法

import Lazy from './lazy'
import { App } from 'vue'
import { LazyOptions } from './types'

export default {
 /**
  * install plugin
  *
  * @param {App} Vue
  * @param {LazyOptions} options
  */
 install (Vue: App, options: LazyOptions): void {
  const lazy = new Lazy(options)

  Vue.config.globalProperties.$Lazyload = lazy
  // 留着备用,为了兼容$Lazyload
  // 选项api,可以通过this.$Lazyload获取到Lazy类的实例,组合api我还不知道怎么获取
  // 所以通过 provide 来实现此需求
  // 使用方式 const useLazylaod = inject('Lazyload')
  Vue.provide('Lazyload', lazy)
  Vue.directive('lazy', {
   mounted: lazy.mount.bind(lazy),
   updated: lazy.update.bind(lazy),
   unmounted: lazy.unmount.bind(lazy)
  })
 }
}

使用插件

import { createApp } from 'vue'
import App from './App.vue'
import VueLazyLoad from '../src/index'

const app = createApp(App)
app.use(VueLazyLoad, {
 log: true,
 lifecycle: {
  loading: () => {
   console.log('loading')
  },
  error: () => {
   console.log('error')
  },
  loaded: () => {
   console.log('loaded')
  }
 }
})
app.mount('#app')

App.vue:

<template>
 <div class="margin" />
 <img v-lazy="'/example/assets/logo.png'" alt="Vue logo" width="100">
 <img v-lazy="{src: errorlazy.src, lifecycle: errorlazy.lifecycle}" alt="Vue logo" class="image" width="100"> 
 <button @click="change">
  change
 </button>
</template>

<script>
import { reactive } from 'vue'
export default {
 name: 'App',
 setup() {
  const errorlazy = reactive({
   src: '/example/assets/log1o.png',
   lifecycle: {
    loading: () => {
     console.log('image loading')
    },
    error: () => {
     console.log('image error')
    },
    loaded: () => {
     console.log('image loaded')
    }
   }
  })
  const change = () => {
   errorlazy.src = 'http://t8.baidu.com/it/u=3571592872,3353494284&fm=79&app=86&size=h300&n=0&g=4n&f=jpeg?sec=1603764281&t=bedd2d52d62e141cbb08c462183601c7'
  }
  return {
   errorlazy,
   change
  }
 }
}
</script>

<style>
.margin {
 margin-top: 1000px;
}
.image[lazy=loading] {
 background: goldenrod;
}
.image[lazy=error] {
 background: red;
}
.image[lazy=loaded] {
 background: green;
}
</style>

以上就是vue3+typescript实现图片懒加载插件的详细内容,更多关于vue3 图片懒加载的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
利用webqq协议使用python登录qq发消息源码参考
Apr 08 Javascript
JavaScript访问字符串中单个字符的两种方法
Jul 03 Javascript
JavaScript实现模仿桌面窗口的方法
Jul 18 Javascript
深入理解事件冒泡(Bubble)和事件捕捉(capture)
May 28 Javascript
用move.js库实现百叶窗特效
Feb 08 Javascript
十大热门的JavaScript框架和库
Mar 21 Javascript
JavaScript Drum Kit 指南(纯 JS 模拟敲鼓效果)
Jul 23 Javascript
微信小程序 数据绑定及运算的简单实例
Sep 20 Javascript
微信小程序分享功能onShareAppMessage(options)用法分析
Apr 24 Javascript
layui递归实现动态左侧菜单
Jul 26 Javascript
微信小程序网络请求实现过程解析
Nov 06 Javascript
vue项目配置同一局域网可使用ip访问的操作
Oct 23 Javascript
原生js实现简单轮播图
Oct 26 #Javascript
vue项目开启Gzip压缩和性能优化操作
Oct 26 #Javascript
Vue检测屏幕变化来改变不同的charts样式实例
Oct 26 #Javascript
解决ant design vue中树形控件defaultExpandAll设置无效的问题
Oct 26 #Javascript
vue下载二进制流图片操作
Oct 26 #Javascript
JavaScript如何操作css
Oct 24 #Javascript
javascript实现多边形碰撞检测
Oct 24 #Javascript
You might like
php数组函数序列之array_values() 获取数组元素值的函数与方法
2011/10/30 PHP
PHP取进制余数函数代码
2012/01/19 PHP
编写安全 PHP应用程序的七个习惯深入分析
2013/06/08 PHP
php检测文本的编码
2015/07/26 PHP
thinkPHP5项目中实现QQ第三方登录功能
2017/10/20 PHP
匹配任意字符的正则表达式写法
2010/04/29 Javascript
javascript 闭包疑问
2010/12/30 Javascript
推荐30个新鲜出炉的精美 jQuery 效果
2012/03/26 Javascript
javascript匿名函数应用示例介绍
2014/03/07 Javascript
js监控IE火狐浏览器关闭、刷新、回退、前进事件
2014/07/23 Javascript
Bootstrap导航条可点击和鼠标悬停显示下拉菜单
2016/11/25 Javascript
基于canvas的二维码邀请函生成插件
2017/02/14 Javascript
jQuery实现百度登录框的动态切换效果
2017/04/21 jQuery
解决Vue大括号字符换行踩的坑
2020/11/09 Javascript
[40:05]DOTA2上海特级锦标赛A组小组赛#1 EHOME VS MVP.Phx第一局
2016/02/25 DOTA
在Python中操作列表之list.extend()方法的使用
2015/05/20 Python
Python字典,函数,全局变量代码解析
2017/12/18 Python
python Pexpect 实现输密码 scp 拷贝的方法
2019/01/03 Python
python turtle库画一个方格和圆实例
2019/06/27 Python
python PyAutoGUI 模拟鼠标键盘操作和截屏功能
2019/08/04 Python
python采集百度搜索结果带有特定URL的链接代码实例
2019/08/30 Python
python和pywin32实现窗口查找、遍历和点击的示例代码
2020/04/01 Python
Python基于Socket实现简易多人聊天室的示例代码
2020/11/29 Python
python中常用的数据结构介绍
2021/01/12 Python
html5与css3小应用
2013/04/03 HTML / CSS
班主任经验交流会主持词
2014/04/01 职场文书
公司募捐倡议书
2014/05/14 职场文书
电气工程及其自动化专业求职信
2014/06/23 职场文书
地理信息科学专业推荐信
2014/09/08 职场文书
2014教育局对照检查材料思想汇报
2014/09/23 职场文书
2014副镇长民主生活会个人对照检查材料思想汇报
2014/09/30 职场文书
2014个人年度工作总结
2014/12/15 职场文书
陪护人员误工证明
2015/06/24 职场文书
2016年小学端午节活动总结
2016/04/01 职场文书
Python 统计序列中元素的出现频度
2022/04/26 Python
MySql数据库触发器使用教程
2022/06/01 MySQL