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 相关文章推荐
对象特征检测法判断浏览器对javascript对象的支持
Jul 25 Javascript
jQuery创建自己的插件(自定义插件)的方法
Jun 10 Javascript
javascript 弹出的窗口返回值给父窗口具体实现
Nov 23 Javascript
将list转换为json失败的原因
Dec 17 Javascript
js加载读取内容及显示与隐藏div示例
Feb 13 Javascript
JavaScript link方法入门实例(给字符串加上超链接)
Oct 17 Javascript
详解JavaScript的while循环的使用
Jun 03 Javascript
浅谈angularJS的$watch失效问题的解决方案
Aug 11 Javascript
vue.js 实现输入框动态添加功能
Jun 25 Javascript
详解vue 项目白屏解决方案
Oct 31 Javascript
Vue组件的使用及个人理解与介绍
Feb 09 Javascript
VSCode 配置uni-app的方法
Jul 11 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
动态生成gif格式的图像要注意?
2006/10/09 PHP
Ajax+PHP 边学边练 之二 实例
2009/11/24 PHP
discuz authcode 经典php加密解密函数解析
2020/07/12 PHP
php中使用exec,system等函数调用系统命令的方法(不建议使用,可导致安全问题)
2012/09/07 PHP
PHP If Else(elsefi) 语句
2013/04/07 PHP
PHP常量使用的几个需要注意的地方(谨慎使用PHP中的常量)
2014/09/12 PHP
PHP通过CURL实现定时任务的图片抓取功能示例
2016/10/03 PHP
JavaScript高级程序设计 错误处理与调试学习笔记
2011/09/10 Javascript
JS中实现简单Formatter函数示例代码
2014/08/19 Javascript
JS实现的仿淘宝交易倒计时效果
2015/11/27 Javascript
总结Javascript中数组各种去重的方法
2016/10/04 Javascript
jquery图片放大镜效果
2017/06/23 jQuery
浅谈原型对象的常用开发模式
2017/07/22 Javascript
Vue 中使用vue2-highcharts实现top功能的示例
2018/03/05 Javascript
Vue + better-scroll 实现移动端字母索引导航功能
2018/05/07 Javascript
使用JavaScript实现node.js中的path.join方法
2018/08/12 Javascript
JavaScript实现的级联算法示例【省市二级联动功能】
2018/12/25 Javascript
微信小程序实现的一键拨号功能示例
2019/04/24 Javascript
express框架中使用jwt实现验证的方法
2019/08/25 Javascript
[01:05:30]VP vs TNC 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
wxPython 入门教程
2008/10/07 Python
Python二叉搜索树与双向链表转换实现方法
2016/04/29 Python
python实现RabbitMQ的消息队列的示例代码
2018/11/08 Python
pandas取出重复数据的方法
2019/07/04 Python
Python 微信爬虫完整实例【单线程与多线程】
2019/07/06 Python
pytorch 加载(.pth)格式的模型实例
2019/08/20 Python
python  ceiling divide 除法向上取整(或小数向上取整)的实例
2019/12/27 Python
Python虚拟环境的创建和包下载过程分析
2020/06/19 Python
Python绘图实现台风路径可视化代码实例
2020/10/23 Python
ASOS英国官网:英国在线时装和化妆品零售商
2017/05/19 全球购物
护士自我鉴定范文
2013/10/06 职场文书
就业自我评价
2014/02/04 职场文书
班干部演讲稿
2014/04/24 职场文书
煤矿安全保证书
2015/02/27 职场文书
python flask框架快速入门
2021/05/14 Python
HTML基础详解(下)
2021/10/16 HTML / CSS