来自哪里。本文来自本人开源组件库fighting-design[2]中Avatar头像[3]组件的load-image[4]类。相比其他静态组件,像图片加载这样的组件,我做了很多内部优化。对于图像加载和错误处理,我尽力考虑每一个可能的结果。对于每个不确定的结果做出相应的提示,以提高用户体验。设计思路我的设计思路是:通过一个loading类传入dom元素、props和emit。首先创建一个虚拟图像元素并尝试加载它。如果加载成功或失败,则进入下一个函数,并进行相应的处理逻辑。初步设计首先,类中有一个加载方法loadCreateImg,代码如下:classLoad{constructor(node,props,emit){this.node=nodethis.props=propsthis.emit=emit}//loadsrcloadCreateImg=()=>{constnewImg=newImage()//创建一个新的虚拟imgnewImg.src=this.props.src//将传入的src赋值给虚拟节点//src加载失败newImg.addEventListener('error',(evt)=>{//加载失败的处理})//src加载成功newImg.addEventListener('load',(evt)=>{//加载成功的处理})}}复制代码首先我创建了一个Load加载类需要传入node参数作为最终要渲染的dom节点,props是传入的组件内部的props参数,里面包含了要加载图片的src路径,emit包括一些回调参数。类内部有一个loadCreateImg方法,调用它可以创建一个虚拟的Image元素,直接赋值加载传入的props.src。通过监听上面的error和load事件,可以监控图片是否加载成功,从而做出不同的状态。成功和失败对于成功或失败的处理,我添加了onerror和onload方法来处理加载成功和失败后的不同处理状态classLoad{constructor(node,props,emit){this.node=nodethis.props=propsthis.emit=emit}loadCreateImg=()=>{constnewImg=newImage()newImg.src=this.props.srcnewImg.addEventListener('error',(evt)=>{this.onerror(evt)//添加})newImg.addEventListener('load',(evt)=>{this.onload(evt)//添加})}//加载成功onload=(evt)=>{this.node.src=this.props.src}//加载失败onerror=(evt)=>{//...}}复制代码加载成功,处理方式是直接将传入的真实dom节点赋值给传入的在props.src中完成加载。加载失败对于加载失败的处理,格斗设计内部做了很多处理。比如可以传入err-src的备用路径进行加载。src加载失败后,如果err-src存在,则需要加载err-src。接下来继续完善类方法:首先判断onerror方法中是否有err-src。如果有err-src,那么就需要调用loadCreateImg重新加载,但是目前的代码显然不能满足需求,所以loadCreateImg需要接收一个可选的errSrc的参数为errSrc,因为需要再次调用该方法来传递inerr-src只是加载失败后,所以该方法可以根据err-src是否存在做不同的处理:emit=emit}loadCreateImg=(errSrc?:string)=>{constnewImg=newImage()//如果errSrc存在则尝试加载它if(errSrc){newImg.src=errSrc}else{newImg.src=this.props.src}newImg.addEventListener('error',(evt)=>{this.onerror(evt)})newImg.addEventListener('load',(evt)=>{this.onload(evt)})}onload=(evt)=>{this.node.src=this.props.src}//加载失败onerror=(evt)=>{//如果存在errSrc将继续尝试加载if(this.props.errSrc){//将errSrc传递给loadCreateImg方法返回this.loadCreateImg(this.props.errSrc)}//否则返回失败回调this.emit('error',evt)}}复制上面代码但生成代码有两个问题:首先,我们发现在`onload`加载成功的方法中,真正的`dom`总是赋值给`src`:onload=(evt)=>{//alwaysassignedtoprops.srcthis.node.src=this.props.src}复制代码但是src并不能一直加载成功,所以还是要动态的把真正加载成功的src传给onload方法,然后是加载成功的src实际上加载成功是在load方法中并且还添加了一个成功的emit。其次,在处理加载失败的`onerror`方法中,因为判断`errSrc`存在,所以会继续调用`loadCreateImg`加载方法重新加载。问题是,如果传入errSrc,那么if(this.props.errSrc)实际上总是为真,从而导致无限循环,重复调用加载函数。onerror=(evt)=>{//判断一直为真if(this.props.errSrc){returnthis.loadCreateImg(this.props.errSrc)}//否则返回失败回调this.emit('error',evt)}复制代码所以需要给它一个变成false的机会,那么修复的方法是:传给loadCreateImg方法后,清除errSrc,这样加载一次就判断为false,这样就完成了代码是:classLoad{constructor(node,props,emit){this.node=nodethis.props=propsthis.emit=emit}loadCreateImg=(errSrc?:string)=>{constnewImg=newImage()//如果errSrc存在尝试加载errSrcif(errSrc){newImg.src=errSrc}else{newImg.src=this.props.src}newImg.addEventListener('error',(evt)=>{this.onerror(evt)})newImg.addEventListener('load',(evt)=>{this.onload(evt,newImg.src)//将加载成功的src传给onload函数})}//添加src属性onload=(evt,src:string)=>{this.node.src=src//分配真正的domsrc到传入的srcthis.emit('load',evt)//添加}onerror=(evt)=>{if(this.props.errSrc){this.loadCreateImg(this.props.errSrc)this.props.errSrc=''//清除errSrc避免重复调用死循环return}this.emit('error',evt)}}复制代码回调函数有时候,我们也需要传递一个布尔值判断图片是否加载成功,或者做其他判断FightingDesign内部处理图片加载失败时,会以一种特殊的方式来提示用户,所以需要一个布尔值和v-if来显示不同的状态,这就涉及到了该类的第四个参数,它是一个可选的callback这样,callback函数可以在加载成功或失败时返回一个布尔值来判断加载是否成功。代码如下:classLoad{constructor(node,props,emit,callback){this.node=nodethis.props=propsthis.emit=emitthis.callback=callback//添加回调参数}loadCreateImg=(errSrc?:string)=>{constnewImg=newImage()if(errSrc){newImg.src=errSrc}else{newImg.src=this.props.src}newImg.addEventListener('error',(evt)=>{this.onerror(evt)})newImg.addEventListener('load',(evt)=>{this.onload(evt,newImg.src)})}onload=(evt,src:string)=>{this.node.src=srcthis.emit('load',evt)//如果有回调,加载成功返回trueif(this.callback){this.callback(true)}}onerror=(evt)=>{if(this.props.errSrc){this.loadCreateImg(this.props.errSrc)this.props.errSrc=''返回}this.emit('error',evt)//如果有callback,加载失败返回falseif(this.callback){this.callback(false)}}}复制上面代码实现是否加载需求当然,你可以发挥你的想象力,用回调函数做更多的事情,这里只是一些用法。图片的懒加载懒加载也是图片加载的必备功能。这里我使用内置的IntersectionObserver[5]接口。对于该方法,这里不再赘述。你可以从MDN[6]中学习。对于懒加载,因为这是一个可选属性,并不是每次都需要,所以我实现了一个和懒加载分离的Lazy类,然后把Lazy类继承给Load类。代码如下:classLazyextendsLoad{constructor(img,props,emit,callback){//super关键字调用super(img,props,emit,callback)}observer=()=>{constobserver=newIntersectionObserver((arr):void=>{//如果进入可见区域if(arr[0].isIntersecting){//开始加载图片并调用父类this.loadCreateImg()observer.unobserve(this.node)}},/***rootMargin是触发器延迟加载的距离通过props传入*https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin*/{rootMargin:this.props.rootMargin})returnobserver}//执行懒加载lazyCreateImg=():void=>{//IntersectionObserver内部方法,需要将dom节点传入this.observer().observe(this.node)}}复制代码IntersectionObserver接口可以判断dom元素是否进入可见区域通过内置方法进入可见区域后,执行父类加载,从而实现延迟加载。对外接口对于Load类和Lazy类,FightingDesign并没有直接暴露出来使用,而是暴露了一个全新的loadImage函数,让它根据是否懒加载来实例化不同的类,然后调用加载method://导出对外接口exportconstloadImage=(node,prop,emit,callback)=>{/***如果传入lazy,执行懒加载类*否则执行普通加载类*/if(prop.lazy){constlazy=newLazy(node,prop,emit,callback)returnlazy.lazyCreateImg()}constload=newLoad(node,prop,emit,callback)load.loadCreateImg()}复制代码测试使用写好的函数测试看一下:asunknownasHTMLImageElement)//模拟道具constprops={src:'https://tianyuha2o.cn/images/auto/my.jpg',errSrc:'https://tianyuhao.cn/images/auto/4.jpg',lazy:true}onMounted(()=>{loadImage(myImg.value,props)})
