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 分钟前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解8 分钟前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里9 分钟前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱9 分钟前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster28 分钟前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw30 分钟前
UniAPP快速入门教程(一)
前端·uni-app
nameofworld40 分钟前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
帅比九日1 小时前
【HarmonyOS NEXT】实战——登录页面
前端·学习·华为·harmonyos
摇光931 小时前
promise
前端·面试·promise
麻花20132 小时前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习