从零开始认识Web Components

前言

自从各大框架百花齐放以来,各大框架的各种组件也是让大家应接不暇。比如element-ui,你会发现针对不同框架又不同版本,再比如公司内部开发一个vue组件库,有一天突然说大家要开始把技术栈调整为react,相应的组件库也要调整,这必然会导致耗费人力时间,开发大量重复的代码

Web Components诞生

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web 应用中使用它们。

相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。目前,它还在不断发展,但已经可用于生产环境。

我们熟知html元素,div,p,ul,复杂的比如video,audio等等,那么是不是可以允许用户自己定义元素呢

比如商品页卡,自定义一个sku-card元素 <sku-card img="xxx" name="xxx" price="xxx"></sku-card>,就可以直接渲染成下面这样,是不是很神奇

Web Components 的组成

Web Components不是单一的规范,而是一系列的技术组成,包括

  • Custom Element 自定义元素
  • Shadow DOM 影子DOM
  • HTML templates HTML模板

接下来我们就挨个看看不同技术的实现

Custom Element

Custom Elements 是网页组件化的基础,也是其核心。主要分为两类

  • 独立元素,它不继承其他内建的 HTML 元素,直接以HTML标签形式在页面使用,比如<sku-card></sku-card>
  • 继承元素,在创建时继承自其他HTML元素,比如<div is="sku-card"></div>

在开发时选择哪种创建方式自己选择

CustomElementRegistry对象

创建自定义元素需要用到CustomElementRegistry对象,要获取它的实例,使用 window.customElements 属性,主要作用就是

  • 给页面注册自定义标签
  • 获取已注册自定义标签元素的相关信息

其中我们只需要着重看一下CustomElementRegistry.define()这个方法就好了,具体使用如下

js 复制代码
customElements.define(name, constructor, options);

参数解析:

  • name自定义标签名。注意:它不能是单个单词,且其中必须要是lisp-case连接符式命名法,比如'sku-card',否则会抛出异常的
  • constructor 自定义元素构造器,它可以控制元素的表现形式、行为和生命周期等
  • options 一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。

下面看一下两种元素的创建方式

独立元素
js 复制代码
class SkuCard extends HTMLElement  {
  constructor() {
    super()
    
    console.log(this)
    this.addEventListener('click', () => {
      console.log('我被点击了')
    })
    
    // 都会报错
    // this.setAttribute('aaaa', 3333)
    // this.innerHTML = '<div slot="aaa">我是slot</div>'
  }
}

window.customElements.define('sku-card', SkuCard)

// 页面中使用就是<sku-card></sku-card>

其实你测试会发现,即便你不在js中定义这个自定义元素,仍然可以直接在dom中使用,并且使用document.getElementsByTagName('xxx')也会返回相应的自定义元素相应信息,这是因为浏览器在解析的时候自动做了处理

这里需要需要注意以下几点

  • 创建独立元素必须继承自HTMLElement,否则会报错
  • 使用了继承super函数不能丢
  • 打印一下this,你会得到自定义组件的dom元素,但是这个元素不能被填充子元素,甚至不能设置属性,否则就会报错,但是可以创建shadow dom这个后面会讲
  • 可以添加事件绑定
继承元素

使用方式会有区别

js 复制代码
class SkuCard1 extends HTMLDivElement  {
  constructor() {
    super()
  }
}

window.customElements.define('sku-card1', SkuCard1, {extends: 'div'})

// 页面中使用<div is="sku-card1"></div>

生命周期

我们在使用前端框架页面或者组件都是有生命周期的,同样的Custom Elements也有自己的生命周期,在生命周期中执行相应的代码逻辑,可以使我们的组件变的异常强大

  • ConnectedCallback:当 Custom Elements 首次被插入文档 DOM 时,这个回调函数会被调用
  • DisconnectedCallback:当 Custom Elements 从文档 DOM 中删除时,这个回调函数会被调用。
  • AdoptedCallback:当 Custom Elements 被移动到新的文档时,这个回调函数会被调用。
  • AttributeChangedCallback:当 Custom Elements 增加、删除、修改自身属性时,这个回调函数会被调用。

我们来看一个例子,模拟一下组件的props,改变Custom Element的value属性,在Custom Element内部监听,然后做出一些响应

这里一定要注意监听的值要使用observedAttributes显示的声明出来

html 复制代码
<input class="test-input" id="testVal" oninput="testValChange()"/>
<sku-card value="1">
</sku-card>
js 复制代码
function testValChange () {
  let testVal = document.getElementById('testVal')
  // console.log(testVal.value)
  let el = document.getElementsByTagName('sku-card')
  el[0].setAttribute('value', testVal.value)
}

class SkuCard extends HTMLElement  {
  constructor() {
    super()
    this.shadow = this.attachShadow({
      mode: 'open'
      // mode: 'closed'
    })
    let child = document.createElement('div')
    child.innerHTML = `<div>我是原始dom</div>`

    this.shadow.appendChild(child)

  }
  static get observedAttributes() {
    return ['value'];
  }

  connectedCallback() {
    console.log("挂载到页面");
  }
  disconnectedCallback() {
    console.log("custom-square 从页面被移除");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (newValue!= oldValue) {
      console.log("属性值被改变");
      let newChild = document.createElement('div')
      newChild.innerHTML = `<div>我是新增的dom,我的值是${newValue}</div>`
      this.shadow.appendChild(newChild)
    }
    
  }
}

window.customElements.define('sku-card', SkuCard)

shadow DOM

来看一件比较神奇的事情,在我们的认识里,html元素好像都是一些内置的基础元素

按以下步骤打开设置

这时我们再来看看我们的原生html元素,这里其实就是shadom-dom,一个video竟然可以这么复杂

Shadow DOM 能在 Web Components 体系中占据重要的地位,是因为其具有良好的密封性,主要表现在:

  • 隐藏标记、样式和行为
  • 保持代码隔离,保证页面的干净整洁,各组件内部代码互不影响
  • 隐藏实现细节,便于使用更强大的语法或功能

这就意味着,使用了Shadow DOM,内部不管写了什么,都不会影响页面其他部分。这也是实现css沙箱的一种方案

Shadow DOM 结构

Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中,它以 Shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样

具体使用

js 复制代码
class SkuCard extends HTMLElement  {
  constructor() {
    super()

    let shadow = this.attachShadow({
      mode: 'open'
      // mode: 'closed'
    })

    // 像操作其他普通dom一样操作shadow元素
    let child = document.createElement('div')
    child.innerHTML = '我是shadow DOM 中的子元素'

    shadow.appendChild(child)

  }
}

window.customElements.define('sku-card', SkuCard)

语法

js 复制代码
let shadow = this.attachShadow({
  mode: 'open'
  // mode: 'closed'
})

mode参数需要注意一下

  • open 这是默认值。当模式为 "open" 时,任何人都可以访问到这个 Shadow DOM,并对其进行读取和修改。
  • closed 当模式为 "closed" 时,只有创建了这个 Shadow DOM 的代码可以访问它。其他代码无法读取或修改这个 Shadow DOM 的内容。

使用 mode: "closed" 可以提供额外的封装性,确保组件的内部结构不被外部代码意外或恶意地修改。但是,这也意味着当你试图调试或检查元素的 Shadow DOM 时,可能会遇到一些限制,因为开发工具可能无法访问到这些封闭的 Shadow DOM。

HTML templates

这个其实是template模板+slot共同作用

template

template元素是一种容器,用于包含一组 HTML 内容,这些内容将作为模板供以后使用,元素在页面加载时不会显示,它主要用于在 JavaScript 中动态创建和插入 HTML 内容。

slot

学过vue的同学肯定都了解slot插槽的用法,通过插槽可以让组件更灵活,Web Components技术套件中的slot其实和vue中的作用差不多,我们直接来看看相关用法吧,这里就不细讲slot的语法了,也有默认插槽和具名插槽的区别

html 复制代码
<template id="my-component">  
    <div>
        <!-- 具名插槽 -->
        <slot name="header"></slot>  
        <p>This is the main content.</p>  
        <slot name="footer"></slot>
        <!-- 默认插槽 -->
        <slot></slot> 
    </div>  
</template>

<my-component>  
    <h1 slot="header">My Component</h1>
    <!-- 会放在 -->
    <p>This is the content for the main slot.</p>  
    <p slot="footer">This is the footer content.</p>  
    <p>This is another content for the main slot.</p>
</my-component>
js 复制代码
class MyComponent extends HTMLElement {
  constructor() {
    super()
    const template = document.getElementById('my-component')
    const content = template.content
    let shadow = this.attachShadow({
      mode: 'open'
      // mode: 'closed'
    })

    shadow.appendChild(content.cloneNode(true))
  }
}
window.customElements.define('my-component', MyComponent)

实战

完成sku-card页卡

html 复制代码
<template id="sku">
    <style>
      .sku {
        width: 160px;
        overflow: hidden;
      }
      .img-wraper {
        background-color: #fff;
        width: 160px;
        height: 160px;
        position: relative;
      }
      .img-wraper .img {
        width: 100%;
        height: 100%;
      }
      .sku-name {
        margin-bottom: 12px;
        color: #000000;
        text-align: left;
        max-height: 38px;
        font-size: 14px;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
      }
      .sku-price {
        display: flex;
        align-items: baseline;
        line-height: 24px;
        color: #FF450C;
        font-weight: bold;
        font-size: 16px;
        margin-top: 12px;
      }
    </style>
    <div class="sku">
      <div class="img-wraper">
        <slot class="img" name="skuImg"></slot>
      </div>
      <p class="sku-name">
        <slot name="skuName"></slot> 
      </p>
      <p class="sku-price">¥<slot name="skuPrice"></slot></p>
    </div>
  </template>
  <sku-card>
    <img class="img" slot="skuImg" src="https://wanshunfu-1301582899.cos.ap-guangzhou.myqcloud.com/null/9b496c274eacdd0606e79714d08d8c1d.png"/>
    <span slot="skuName">国产新疆羊肉有机乳羔羊排酸羊肉礼盒(只发广东) 有机羔羊  1kg /(羊排500g/羊脊椎500g/羊腱子500g/羊脖子500g/羊后腿500g 任意选两种 </span>
    <span slot="skuPrice">1111</span>
  </sku-card>
js 复制代码
class SkuCard extends HTMLElement  {
  constructor() {
    super()
    const template = document.getElementById('sku')
    const content = template.content
    let shadow = this.attachShadow({
      mode: 'open'
      // mode: 'closed'
    })

    shadow.appendChild(content.cloneNode(true))

    // 组件内定义样式
    // let styleEle = document.createElement("style");
    // styleEle.textContent = `
    //   :host .img-wraper {
    //     width: 100%;
    //     height: 100%;
    //   }
    // `;
    // shadow.appendChild(styleEle);

  }
}

window.customElements.define('sku-card', SkuCard)
相关推荐
吴敬悦29 分钟前
领导:按规范提交代码conventionalcommit
前端·程序员·前端工程化
ganlanA30 分钟前
uniapp+vue 前端防多次点击表单,防误触多次请求方法。
前端·vue.js·uni-app
卓大胖_31 分钟前
Next.js 新手容易犯的错误 _ 性能优化与安全实践(6)
前端·javascript·安全
m0_7482463532 分钟前
Spring Web MVC:功能端点(Functional Endpoints)
前端·spring·mvc
SomeB1oody40 分钟前
【Rust自学】6.4. 简单的控制流-if let
开发语言·前端·rust
云只上41 分钟前
前端项目 node_modules依赖报错解决记录
前端·npm·node.js
程序员_三木42 分钟前
在 Vue3 项目中安装和配置 Three.js
前端·javascript·vue.js·webgl·three.js
lxw18449125141 小时前
vue 基础学习
前端·vue.js·学习
徐_三岁1 小时前
Vue3 Suspense:处理异步渲染过程
前端·javascript·vue.js
萧寂1731 小时前
Pinia最简单使用(vite+vue3)
前端·javascript·vue.js