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 相关文章推荐
使用JQuery和s3captche实现一个水果名字的验证
Aug 14 Javascript
Knockoutjs的环境搭建教程
Nov 26 Javascript
JavaScript SetInterval与setTimeout使用方法详解
Nov 15 Javascript
javascript版的in_array函数(判断数组中是否存在特定值)
May 09 Javascript
jQuery多项选项卡的实现思路附样式及代码
Jun 03 Javascript
Jquery中扩展方法extend使用技巧
Aug 24 Javascript
JavaScript常用正则验证函数实例小结【年龄,数字,Email,手机,URL,日期等】
Jan 23 Javascript
canvas实现图片根据滑块放大缩小效果
Feb 24 Javascript
基于vue2.0+vuex+localStorage开发的本地记事本示例
Feb 28 Javascript
bootstrap 通过加减按钮实现输入框组功能
Nov 15 Javascript
redux-saga 初识和使用
Mar 10 Javascript
vue-router结合vuex实现用户权限控制功能
Nov 14 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启动时候提示PHP startup的解决方法
2013/05/07 PHP
php导入csv文件碰到乱码问题的解决方法
2014/02/10 PHP
PHP实现微信发红包程序
2015/08/24 PHP
JavaScript入门之基本函数详解
2011/10/21 Javascript
js仿土豆网带缩略图的焦点图片切换效果实现方法
2015/02/23 Javascript
jquery UI Datepicker时间控件的使用方法(基础版)
2015/11/07 Javascript
几种经典排序算法的JS实现方法
2016/03/25 Javascript
jQuery与JS加载事件用法分析
2016/09/04 Javascript
微信小程序  简单实例(阅读器)的实例开发
2016/09/29 Javascript
自学实现angularjs依赖注入
2016/12/20 Javascript
使用bat打开多个cmd窗口执行gulp、node
2017/02/17 Javascript
vue实现动态数据绑定
2017/04/28 Javascript
Ionic + Angular.js实现验证码倒计时功能的方法
2017/06/12 Javascript
JavaScript实现一个简易的计算器实例代码
2018/05/10 Javascript
详解promise.then,process.nextTick, setTimeout 以及 setImmediate的执行顺序
2018/11/21 Javascript
vue中使用带隐藏文本信息的图片、图片水印的方法
2020/04/24 Javascript
JS深入学习之数组对象排序操作示例
2020/05/01 Javascript
[01:29:46]DOTA2上海特级锦标赛C组资格赛#1 OG VS LGD第二局
2016/02/27 DOTA
python的id()函数介绍
2013/02/10 Python
python自定义类并使用的方法
2015/05/07 Python
Python中DJANGO简单测试实例
2015/05/11 Python
Python正则简单实例分析
2017/03/21 Python
Windows下Anaconda的安装和简单使用方法
2018/01/04 Python
python装饰器-限制函数调用次数的方法(10s调用一次)
2018/04/21 Python
PyQt5 pyqt多线程操作入门
2018/05/05 Python
python进阶之自定义可迭代的类
2019/08/20 Python
python实现超市商品销售管理系统
2019/11/22 Python
Python使用pycharm导入pymysql教程
2020/09/16 Python
python爬虫利用代理池更换IP的方法步骤
2021/02/21 Python
使用phonegap获取位置信息的实现方法
2017/03/31 HTML / CSS
GoPro摄像机美国官网:美国运动相机厂商
2018/07/03 全球购物
教师产假请假条范文
2014/04/10 职场文书
党性分析自查总结
2014/10/14 职场文书
医院合作意向书范本
2015/05/08 职场文书
聘任书格式及范文
2015/09/21 职场文书
vue使用echarts实现折线图
2022/03/21 Vue.js