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>