Vue 自定义指令directive实现v-lazy

Vue 指令相关文章:

利用Vue 的自定义指令实现,v-show, v-if

数据接口准备:

  1. 新建一个后端工程 server, 用Node.js express
  2. 数据准备:
json 复制代码
[
  {
    "id": 1,
    "title": "Web 开发技术",
    "img" : "http://localhost:3000/images/1.webp"
  },
  {
    "id": 2,
    "title": "深入浅出 Vue.js",
    "img" : "http://localhost:3000/images/2.webp"
  },
  {
    "id": 3,
    "title": "JavaScript 高级程序设计 第四版",
    "img" : "http://localhost:3000/images/3.webp"
  },
  {
    "id": 4,
    "title": "JavaScript + jQuery",
    "img" : "http://localhost:3000/images/4.webp"
  },
  {
    "id": 5,
    "title": "javascript 前端开发 程序设计 ",
    "img" : "http://localhost:3000/images/5.webp"
  },
  {
    "id": 6,
    "title": "Web 前端开发技术 HTML CSSJ JAVSCRIPT",
    "img" : "http://localhost:3000/images/6.webp"
  },
  {
    "id": 7,
    "title": "Web 前端开发精品课 HTML 与CSS进阶",
    "img" : "http://localhost:3000/images/7.webp"
  },
  {
    "id": 8,
    "title": "Web 前端开发实战教程",
    "img" : "http://localhost:3000/images/8.webp"
  },
  {
    "id": 9,
    "title": "Web 前端开发技术",
    "img" : "http://localhost:3000/images/9.webp"
  },
  {
    "id": 10,
    "title": "Web 前端技术",
    "img" : "http://localhost:3000/images/10.webp"
  },
  {
    "id": 11,
    "title": "零基础 html + css 从入门到精通",
    "img" : "http://localhost:3000/images/11.webp"
  },
  {
    "id": 12,
    "title": "javascript 高级程序设计第四版",
    "img" : "http://localhost:3000/images/12.webp"
  },
  {
    "id": 13,
    "title": "Javascript 入门经典",
    "img" : "http://localhost:3000/images/13.webp"
  },
  {
    "id": 14,
    "title": "Web前端开发技术",
    "img" : "http://localhost:3000/images/14.webp"
  },
  {
    "id": 15,
    "title": "WebAssembly 原理与核心技术",
    "img" : "http://localhost:3000/images/15.webp"
  },
  {
    "id": 16,
    "title": "互联网前端技术开发",
    "img" : "http://localhost:3000/images/16.webp"
  } 
]

接口编写:

js 复制代码
const express = require('express')
const { resolve } = require('path')
const { readFileSync } = require('fs')

const app = express()

app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-methods', 'POST,GET')
  next()
})

app.get('/images/:filename', (req, res) => {
  res.sendFile(resolve(__dirname, './images/' + req.params.filename))
})

app.get('/book-list', (req, res) => {
  const imageData = readFileSync(resolve(__dirname, './data/imageData.json'), 'utf8')
  console.log(imageData)
  res.send(JSON.parse(imageData))
})

app.listen(3000, () => {
  console.log('http://localhost:3000')
})

开源项目的 v-lazy 的使用

1 新建前端工程 vue-lazy 2. 安装axios , vue-lazyload 3. main.js 引用,并使用use 使用

js 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import VueLazyload  from 'vue-lazyload'

createApp(App).use(VueLazyload, {
  preLoad: 1.3,
  loading: './assets/images/loading.gif',
  error: './images/images/error.webp'
}).mount('#app')
  1. 组件内使用
js 复制代码
<script setup>
import { onMounted, ref } from 'vue'
import axios from 'axios'

const imgList = ref([])
onMounted(async () => {
  const data = await axios('http://localhost:3000/book-list')
  imgList.value = data.data
})
</script>

<template>
  <div class="container">
    <ul class="img-list">
      <li v-for="item in imgList" :key="item.id">
        <img v-lazy="item.img" />
        <p>{{ item.title }}</p>
      </li>
    </ul>
  </div>
</template>

<style scoped>
html,
body,
ul,
p {
  margin: 0;
  padding: 0;
}
.container {
  height: 100%;
  overflow: auto;
}
.img-list li {
  list-style: none;
  display: flex;
  height: 140px;
  margin-bottom: 30px;
  border-bottom: 1px solid #ddd;
  padding-bottom: 20px;
}
.img-list li img {
  width: 120px;
  margin-right: 20px;
}
.img-list li p {
  flex: 1
}
</style>

观察效果:

可以看到控制台之只有5张图片的请求,但是我们的接口是有16条数据的

这就实现了图片的懒加载。

v-lazy 实现

分析v-lazy 做了那些事情:

  1. 只在用户设定的preLoad 区域内加载图片
  2. 滚动的时候还需要判断图片是否在用户设定的区域
  3. 图片有没有被加载过,已经加载过了,再次滚动的时候不再重新执行加载逻辑
  4. 图片还没加载完成时会先加载loading 图片
  5. 加载出错时会显示一个加载错误的图片

针对第一点: 怎么计算出图片位置是否在设定的区域内呢?

我们可以用getBoundingClientRect 获取到距离顶部的距离和用户传进来的preLoad 设定的区域进行对比

针对第二、三点需要做两件事:

(1)根据图片位置查找到带有overflow 属性值为auto 或者scroll的元素

(2)监听查找到的带有overflow 属性值为auto 或者为scroll的元素的scroll事件

(3)给每个图片实例都添加一个loaded 属性, 没加载过标记为false, 加载过了标记为true

针对四、五两点: (1)根据图片的onload 和onerror 回调来判断是现实真实图片还是error 图片

代码结构设计:

  1. 新建modules文件夹
  2. modules 文件夹下新建lazyload
  3. lazyload 文件夹下新建index.js 作为vue-lazyload 的出口文件
  4. 新建Lazyload.js 文件主要用来处理滚动相关逻辑和渲染逻辑
  5. 新建LazyImg.js 用来创建图片实例的类,保存相关状态,状态改变时,调用渲染函数渲染对应状态的图片
  6. 新建utils.js 放置一些工具函数, 比如查找overflow 属性值为auto 或者为scroll的元素

来看下具体代码:

index.js

js 复制代码
import Lazyload from './Lazyload'

const VueLazyload  = {
  install (app, options) {
    const LazyClass = Lazyload(app)
    const lazyLoad = new LazyClass(options)
    app.directive('lazy' ,{
      mounted: lazyLoad.bindLazy.bind(lazyLoad)
    })
  }
}

export default VueLazyload

LazyImg.js

js 复制代码
import { imgLoad } from './utils'
export default class LazyImg {
  constructor ({el, src, options, imageRender}) {
    this.el = el
    this.src = src
    this.options = options
    this.imageRender = imageRender

    this.loaded = false
    this.state = {
      error: false,
      loading: false
    }
  }

  checkVisible () {
    const { top } = this.el.getBoundingClientRect()
    return top < window.innerHeight * (this.options.preLoad || 1.3)
  }

  loadImg () {
    this.imageRender(this, 'loading')
    this.loading = true
    imgLoad(this.src).then(res => {
      this.loading = false
      this.loaded = true
      this.imageRender(this, 'ok')
    }).catch(error => {
      this.loading = false
      this.error = true
      this.loaded = true
      this.imageRender(this, 'error')
    })
  }
}

Lazyload.js

js 复制代码
import { getParentNode } from "./utils"
import LazyImg from './LazyImg'

function Lazyload (app) {
  return  class Lazy {
    constructor (options) {
      this.options = options
      this.isAddEvnt = false
      this.imgPool = []
    }
    bindLazy (el, binding, vnode) {
      const scrollParent = getParentNode(el)

      if (scrollParent && !this.isAddEvnt)  {
        this.isAddEvnt = true
        scrollParent.addEventListener('scroll', this.handleScroll.bind(this), false)
      }

      const lazyImg = new LazyImg({
        el,
        src: binding.value,
        options: this.options,
        imageRender: this.imageRender.bind(this)
      })
      console.log(lazyImg)
      this.imgPool.push(lazyImg)
      this.handleScroll()
    }

    imageRender (lazy, state) {
      const { el, options } = lazy
      const { loading, error } = options
      let src = ''
      switch (state) {
        case 'loading':
          src = loading
          break
        case 'error':
          src = error
          break
        default:
          src = lazy.src
      }
      el.setAttribute('src', src)
    }
    handleScroll () {
      this.imgPool.forEach(item => {
        if (!item.loaded) {
          const isVisible = item.checkVisible()
          isVisible && item.loadImg()
        }
        
      })
    }

  }
}

export default Lazyload

utils.js

js 复制代码
export function getParentNode (el) {
  var _parent = el.parentNode

  while(_parent) {
    const _styleOverflow = getComputedStyle(_parent).overflow

    if (/(scroll)|(auto)/.test(_styleOverflow)) {
      return _parent
    }

    _parent = _parent.parentNode
  }
}

export function imgLoad (src) {
  return new Promise((resolve, reject) => {
    const oImg = new Image()
    oImg.src = src
    oImg.onload = resolve()
    oImg.onerror = reject()
  })
}

将main.js 种vue-lazyload的引用改为我们自己写的vue-lazyload

js 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import VueLazyload  from './modules/vue-lazyload'

createApp(App).use(VueLazyload, {
  preLoad: 1.3,
  loading: './assets/images/loading.webp',
  error: './images/images/error.webp'
}).mount('#app')

来看看效果:

可以看到,我们也实现了和开源项目一样的效果,第一屏只加载了5张图片。

我们再往下滚动观察效果:

又多加载了两张图片, 符合预期。

到此Vue 自定义实现v-lazy 就分享到这里了,感谢收看,一起学习一起进步。

Vue 指令相关文章:

利用Vue 的自定义指令实现,v-show, v-if

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax