前言
ES6 注意!!
最近在优化个人博客前端,翻看到了自己图片懒加载/预加载的远古代码(通过watch监听实现的),虽说实际效果勉强还行,但总觉得不够 “Vue”,功能上也有所不足。
考虑到现有的 vue-lazyload 插件将懒加载指令化了,于是我想能不能自己也写一个。搜索一波后发现是可行的,并且还挺简单的,这篇文章给了我很多参考,在此之上我又优化并增添了额外功能,最后也不超一百行代码,奥利给,造TA就完了
问:为什么不用现成的插件?
答:代码还是自己撸的爽,vue-lazyload 据说不支持分别指定占位图,而我实现了
图片懒加载
这里给小白的简单介绍一下,熟知此概念的大佬可以跳过,还是看不懂的小白转百度或谷歌
图片的懒加载是指对于首屏加载后未在视野的图片容器 / 视野之外新增的图片容器,先给容器一个缩略图或者默认占位图(size 很小);待容器进入或即将进入视野时在后台下载原图,原图准备好后才替换。效果如下,左右分别是懒加载完成前后的效果
这样用户在原图到来之前至少还有缩略图看看,一定程度减缓其不耐烦的心情从而优化用户体验;同时按需加载的特性能够节省流量,这对服务器和用户都是一件好事
所以对于小水管服务器和面对网络拥堵时候,懒加载就显得特别有用
实现关键
首先给数组原型加两个自定义方法,后面会用上,这段代码放在指令调用前即可
// 移除数组指定的元素
if (!Array.prototype.remove){
Array.prototype.remove = function (item) {
if (!this.length) return;
let index = this.indexOf(item);
if (index > -1) {
this.splice(index,1);
return this;
}
}
}
// 推入数组当且仅当该数组没有该元素(针对string)
if (!Array.prototype.pushIfNew){
Array.prototype.pushIfNew = function (...item) {
for (let i of item)
if (this.indexOf(i)===-1)
this.push(i);
return this
}
}
位置判断
懒加载关键的之一就是判断该图片容器是否在视野之内,这里要用到节点的 getBoundingClientRect()
方法,返回值是 DOMRect 对象,包含该元素块边框相对于视野左上角的距离,各属性如下
如果视野高度为 screenHeight,结合以上属性,我们很容易判断元素是否在视野之内
let top = el.getBoundingClientRect().top;
let screenHeight = window.innerHeight || document.documentElement.clientHeight;
if (top < screenHeight + 50 && top > -50){
// 不一定要严格地进入视野,可以适当“扩大”视野,能够判断“即将进入”的情况,更符合实际要求
}
后台加载
懒加载关键的之二是后台加载原图,实现起来很简单,当 img 元素的 src
属性被赋值时,加载就会发生,加载成功后执行其 onload
方法,失败时执行 onerror
方法。利用这个特性,当目标进入视野时,可以创建一个临时 img(不用插入document),定义其加载成功和失败的行为,然后给他的 src
赋值即可
let img = new Image();
img.onload = ()=>{
// 成功后替换缩略图
//...
}
img.onerror = ()=>{
// 失败后可以显示 error 图片
// 或什么都不做维持之前的缩略图
// ...
}
img.src = 'original imgSrc'
监听追踪
关键之三就是对目标的监听和追踪了,可以定义两个数组,listenList
存放追踪目标,imgCacheList
存放已加载(已缓存)图片的 src。
当一个 img 元素被新插入文档后,以下操作按序三选一
- 如果其原图在
imgCacheList
中,直接 src 赋值为原图 - 如果该 img 在视野之内,开始触发后台加载,加载成功后其 src 加入
imgCacheList
- 如果该 img 在视野之外,将其加入
listenList
中进行监听
对于 listenList
, 我们会绑定全局滚动事件,窗口一滚动就对 listenList
中的所有目标进行位置判断
- 如果在视野内,触发后台加载,加载成功后其 src 加入
imgCacheList
,同时将目标从listenList
中移除 - 如果在视野外,什么都不干
当然直接绑定滚动时间会超频繁的触发函数,这里可以对函数做防抖处理
指令注册
因为要对 listenList
和imgCacheList
进行共享和管理,所以不能简单地进行全局指令注册 Vue.directive()
,而是要在其之外开辟一个区域存放这些共享的数据,这就要以插件形式进行指令的注册了
同时指令有多个钩子函数,考虑到 img 要插入文档后才能通过 getBoundingClientRect()
获取位置信息,这里选择 inserted
钩子函数
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
虽然跟要求有偏差,但这是最接近要求的钩子函数了,实际使用上又没什么问题,就选他吧
/*-------lazyload.js-------*/
export default (Vue,options)=>{
let listenList = [];
let imgCacheList = [];
//.....
Vue.directive('lazyload',{
inserted:(el,binding)=>{
}
}
}
/*-------main.js-------*/
import LazyLoad from './lazyload';
Vue.use(LazyLoad)
使用
了解了这几个关键点我想最终实现也应该有个大概了,剩下一些细节以注释给出,详看下面的完整代码
// ----lazyload.js----
// 防抖
function throttle(func, wait) {
let context, args;
let previous = 0;
return function() {
let now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
export default (Vue,options={})=>{
//默认设置,可以传入options覆盖
//preloadClass: 占位状态(原图未加载进来)的 class,可以利用他配合 css 加模糊效果
//loadErrorClass: 图片加载失败后赋予的 class
//default: 默认占位图透明
//error: 出错后显示的图片默认透明,要启用错误处理才生效
let init = {
preloadClass:'lazyload-preload',
loadErrorClass:'lazyload-status-fail',
default:'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==',
error:`data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==`,
...options
};
let listenList = [];
let imgCacheList = [];
// 判断图片是否已经缓存
const isAlreadyLoad = (imgSrc)=>{
return imgCacheList.indexOf(imgSrc) > -1;
};
// 如果在视野内,触发后台加载返回true,否则返回false
const tryLoad = (item)=>{
let {el,src} = item;
let top = el.getBoundingClientRect().top;
let screenHeight = window.innerHeight || document.documentElement.clientHeight;
if (top < screenHeight + 50 && top > -50){
let img = new Image();
//后台加载完:进行替换,加入缓存,移除监听,更新class
img.onload = ()=>{
el.src = src;
el.classList.remove(init.preloadClass);
imgCacheList.pushIfNew(src);
listenList.remove(item);
};
//如果出错:更新class,移除监听
img.onerror = ()=>{
if (item.errorHandle){
el.src = init.error;
el.style.objectFit = 'none';
}
el.classList.remove(init.preloadClass);
el.classList.add(init.loadErrorClass);
listenList.remove(item);
};
//出发后台加载
img.src = src;
return true;
}else{
return false;
}
};
//用于标记监听状态,确保只会 addEventListener(由第一张插入的图片触发)
let listenStatus = false;
const listenScroll = ()=>{
if (!listenStatus){
window.addEventListener('scroll',throttle(()=>{
let len = listenList.length;
for (let i = 0; i < len; i++){
tryLoad(listenList[i])
}
},200));
listenStatus = true;
}
};
Vue.directive('lazyload',{
inserted:(el,{value,modifiers})=>{
let imgSrc,placeholder;
// 两种方式传参数
if (typeof value==='string'){
imgSrc = value;
placeholder = init.default;
}else{
imgSrc = value[0];
placeholder = value[1]||init.default;
}
// 如果已经有缓存,直接使用
if (isAlreadyLoad(imgSrc)){
el.src = imgSrc;
return false;
}
let item = {
el:el,
src:imgSrc,
errorHandle:!!modifiers.rude //是否开启错误处理
};
// 先给占位图和占位 class
el.src = placeholder;
el.classList.add(init.preloadClass);
if (tryLoad(item)){
return;
}
// 如果在视野外,加入监听
listenList.pushIfNew(item);
// 第一张插入的图片负责 addEventListener
!listenStatus && listenScroll();
}
})
}
使用上和 Vue 装插件一样
/*-------main.js-------*/
import LazyLoad from './lazyload';
Vue.use(LazyLoad)
/*-------xxx.vue-------*/
// 两种方式传参数,指定原图和占位图/只指定原图,占位图默认
// 启用错误处理
其他
目前该指令只支持 img 标签的懒加载,对于 background-image 这种背景图并未支持(因为自己博客用得少),但我想实现起来也不难 “通过指令的修饰区别两种情况,改一下 tryLoad 函数……” 应该就行了
同时也不支持动态响应的参数(我不知道这样说对不对),也就是如果传入指令的 imgSrc 发生变动,被绑定的元素并不会更新。所以目前该指令只适用于插入一次后不再变更的元素
目前想到的问题就上面两个,如果有什么实用的功能也可以提出来,正好我也想把这个指令做得更精一些
以上