使用 Web component 封装一个简易懒加载图片组件

WebComponent 是 HTML5 推出的新特性,借助 WebComponent 可以在原生 js 代码基础上可以实现组件功能,通常情况前端开发是借助如 Vue、React 等框架,在此框架基础可以轻易实现组件功能,但是这些组件往往在不同的框架下是不通用的,但是 WebComponent 是

原生js实现的所以使用 WebComponent 封装的组件基本是各框架通用的。

 一、Web components 主要包含三个规范

  • Custom Elements:自定义元素
  • Shadow DOM:影子DOM
  • HTML Templates:模板 插槽

 1、创建一个自定义元素 (文件名 LazyImage.js)(LazyImage 继承 HTMLElement 所有属性和方法)

class LazyImage extends HTMLElement {
    constructor() {
    super()
  }
}

2、影子DOM 用于封装自定义组件内部样式,防止被外部样式污染作用类似 scope

      创建影子DOM

class LazyImages extends HTMLElement {
  constructor() {
    super()
    // 创建影子 dom 
    // mode:open 外界js可以通过影子节点的父元素parentDom.shadowRoot方法获得
    // mode:closed 时,无法被外界js获取
    const shadowRoot = this.attachShadow({ mode: "open" })
  }
}

     认识组件函数

class LazyImage extends HTMLElement {
  constructor() {
    super()
    const shadowRoot = this.attachShadow({ mode: "open" })
  }
  /* 元素被插入到文档时触发,等价于 vue 的 mounted */
  connectedCallback() {

  }
  /* 元素从文档中移除时触发,等价于 vue 的 beforeDestory / destoyed */
  disconnectedCallback() {

  }
  /* 自定义元素的属性被添加、移除或更改时触发,等价于 vue 的 beforeUpdate / updated */
  attributeChangedCallback(attributeName, oldValue, newValue) {

  }
}

3、注册组件

class LazyImage extends HTMLElement {
  constructor() {
    super()
    const shadowRoot = this.attachShadow({ mode: "open" })
  }
  /* 元素被插入到文档时触发,等价于 vue 的 mounted */
  connectedCallback() {

  }
  /* 元素从文档中移除时触发,等价于 vue 的 beforeDestory / destoyed */
  disconnectedCallback() {

  }
  /* 自定义元素的属性被添加、移除或更改时触发,等价于 vue 的 beforeUpdate / updated */
  attributeChangedCallback(attributeName, oldValue, newValue) {

  }
}
// 注册组件
customElements.define('lazy-image', LazyImage)

4、附上组件代码

class LazyImage extends HTMLElement {
  /**
   * @description 监听元素 Attribute 需配合 attributeChangedCallback 函数使用
   * @returns {*}
   */
  static get observedAttributes() {
    // 指定要监听的元素的属性数组
    const shape = ['width', 'height']
    const style = ['style', 'class']
    const value = ['src', 'alt', 'title', 'crossorigin', 'ismap']
    return [...shape, ...style, ...value]
  }
  constructor() {
    super()
    // this 就是注册的 <lazy-image></lazy-image> 标签本身 console.log(this, 'this')
    const shadowRoot = this.attachShadow({ mode: "open" })
    this.intersectionObserver = null
    this.STYLE = document.createElement('style')
    this.IMG = document.createElement('IMG')
    this.setILazyImageStyle(0, 0)
    shadowRoot.appendChild(this.STYLE)
    shadowRoot.appendChild(this.IMG)
  }
  /* 元素被插入到文档时触发,等价于 vue 的 mounted */
  connectedCallback() {
    const { clientHeight, clientWidth } = this
    this.setILazyImageStyle(clientHeight, clientWidth)
  }
  /* 元素从文档中移除时触发,等价于 vue 的 beforeDestory / destoyed */
  disconnectedCallback() {
    this.intersectionObserver = null
  }
  /* 元素的属性被添加、移除或更改时触发,等价于 vue 的 beforeUpdate / updated */
  attributeChangedCallback(attributeName, oldValue, newValue) {
    this.lazyLoad(attributeName, newValue)
  }
  /* 设置标签属性 */
  setAttributes(attributeName, newValue) {
    this.IMG.setAttribute(attributeName, newValue)
  }
  /* 获取外部样式给 LazyImage 的设置的样式,并设置给 LazyImage
     便于获取 LazyImage { clientHeight, clientWidth }
    */
  setILazyImageStyle(height, width) {
    // 给 LazyImage 配置 display: inline-block 保证外部给 LazyImage 设置的样式生效
    this.STYLE.textContent = `:host { display: inline-block }`
    this.IMG.setAttribute('height', height)
    this.IMG.setAttribute('width', width)
  }
  /* 懒加载入口方法 */
  lazyLoad(attributeName, newValue) {
    // 检测浏览器是否支持 loading="lazy" api
    const supportLazyLoading = 'loading' in HTMLImageElement.prototype
    if (supportLazyLoading) {
      this.supportLoading(attributeName, newValue)
      return
    }
    // 检测浏览器是否支持 IntersectionObserver api
    const supportObserver = 'IntersectionObserver' in window
    if (supportObserver) {
      this.supportIntersectionObserver(attributeName, newValue)
      return
    }
    // 以上不支持时,按原生 img 自定义
    this.nextSupport(attributeName, newValue)
  }
  supportLoading(attributeName, newValue) {
    // 支持 loading="lazy" api 时设置属性
    this.IMG.setAttribute('loading', 'lazy')
    // 主要赋值 src 可自动扩展 ...
    this.setAttributes(attributeName, newValue)
  }
  supportIntersectionObserver(_, newValue) {
    this.intersectionObserver = new IntersectionObserver((entries) => {
      // intersectionRatio === 0 时代表监听元素不在可视范围
      if (entries[0].intersectionRatio <= 0) return
      this.setAttributes('src', newValue)
      this.intersectionObserver.unobserve(this.IMG)
      // IntersectionObserver懒加载 => 加载失败处理函数
      this.IMG.onerror = (e) => { throw new Error('loaded error true') }
    })
    // 监听
    this.intersectionObserver.observe(this.IMG)
  }
  nextSupport(attributeName, newValue) {
    this.setAttributes(attributeName, newValue)
  }
}
// 若要设置组件 lazy-image 样式使用 style 设置
customElements.define('lazy-image', LazyImage)

5、在 Vue 中使用组件

<template>
  <div class="home">
      <div class="home-mian">
        <lazy-image
          v-for="(url, index) in urlArr"
          :key="index"
          :src="url"
          style="border-radius: 8px;height: 200px;width: 200px;"
        ></lazy-image>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts" name="Home">

import { ref, onMounted } from 'vue'
// 在main.js 中引入或者在使用页面引入
import './LazyImage.js'

const urlArr = ref<string[]>([
  'https://img2.baidu.com/it/u=3430387650,1694029532&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500',
  'https://img2.baidu.com/it/u=1530370139,2858963280&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=500',
  'https://img0.baidu.com/it/u=350592823,3182430235&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800',
  'https://img1.baidu.com/it/u=1179199327,1946315836&fm=253&fmt=auto&app=138&f=JPEG?w=1364&h=800',
  'https://img1.baidu.com/it/u=2603934083,3021636721&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
])

onMounted(() => {})

</script>

<style lang="scss" scoped>
.home {
  width: 100%;
  &-main {
    display: flex;
    flex-direction: column;
    height: 400px;
    width: 400px;
    overflow: auto;
    border: 1px solid #aaa;
  }
}
</style>

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值