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 相关文章推荐
用js实现控制内容的向上向下滚动效果
Jun 26 Javascript
js中的布尔运算符使用介绍
Nov 20 Javascript
jQuery+json实现动态创建复杂表格table的方法
Oct 25 Javascript
解决vue组件中使用v-for出现告警问题及v for指令介绍
Nov 11 Javascript
详解Vuex管理登录状态
Nov 13 Javascript
vue-cli 脚手架基于Nightwatch的端到端测试环境的过程
Sep 30 Javascript
Vue利用History记录上一页面的数据方法实例
Nov 02 Javascript
JS数组求和的常用方法实例小结
Jan 07 Javascript
js中apply和call的理解与使用方法
Nov 27 Javascript
如何使用JavaScript实现无缝滚动自动播放轮播图效果
Aug 20 Javascript
AJAX XMLHttpRequest对象创建使用详解
Aug 20 Javascript
基于openlayers实现角度测量功能
Sep 28 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
全国FM电台频率大全 - 5 内蒙古自治区
2020/03/11 无线电
php session劫持和防范的方法
2013/11/12 PHP
PHP使用CURL获取302跳转后的地址实例
2014/05/04 PHP
PHP框架Laravel学习心得体会
2015/10/28 PHP
php+redis实现注册、删除、编辑、分页、登录、关注等功能示例
2017/02/15 PHP
php实现多维数组排序的方法示例
2017/03/23 PHP
Yii 访问 Gii(脚手架)时出现 403 错误
2018/06/06 PHP
yii2 url重写并隐藏index.php方法
2018/12/10 PHP
服务器安全设置的几个注册表设置
2007/07/28 Javascript
JavaScript中创建类/对象的几种方法总结
2013/11/29 Javascript
js实现的GridView即表头固定表体有滚动条且可滚动
2014/02/19 Javascript
js、jquery图片动画、动态切换示例代码
2014/06/03 Javascript
用C/C++来实现 Node.js 的模块(一)
2014/09/24 Javascript
JavaScript中的Repaint和Reflow用法详解
2015/07/27 Javascript
理解 JavaScript Scoping &amp; Hoisting(二)
2015/11/18 Javascript
JS中BOM相关知识点总结(必看篇)
2016/11/22 Javascript
jquery dataview数据视图插件使用方法
2016/12/23 Javascript
通过命令行生成vue项目框架的方法
2017/07/12 Javascript
详解刷新页面vuex数据不消失和不跳转页面的解决
2018/01/30 Javascript
vue自定义一个v-model的实现代码
2018/06/21 Javascript
[49:08]Secret vs VP 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
跟老齐学Python之list和str比较
2014/09/20 Python
详解在Python和IPython中使用Docker
2015/04/28 Python
浅谈MySQL中的触发器
2015/05/05 Python
Python实现合并两个有序链表的方法示例
2019/01/31 Python
Python跳出多重循环的方法示例
2019/07/03 Python
详解python opencv、scikit-image和PIL图像处理库比较
2019/12/26 Python
如何在python中实现线性回归
2020/08/10 Python
python自动化测试三部曲之request+django实现接口测试
2020/10/07 Python
python3字符串输出常见面试题总结
2020/12/01 Python
求职简历自荐信范文
2013/10/21 职场文书
财务简历的自我评价
2014/03/05 职场文书
2014年财政局工作总结
2014/12/09 职场文书
成品仓管员岗位职责
2015/04/01 职场文书
药房管理制度范本
2015/08/06 职场文书
nginx配置之并发频次限制
2022/04/18 Servers