0


十年了,您还不认识 WebComponent 吗?!

在这里插入图片描述


🧑‍💼 个人简介:一个不甘平庸的平凡人🍬
🖥️ Node专栏:Node.js从入门到精通
🖥️ TS知识总结:十万字TS知识点总结
👉 你的一键三连是我更新的最大动力❤️!
📢 欢迎私信博主加入前端交流群🌹


📑 目录


前言

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

简单的说,Web Components 就是使用标准化的原生技术实现可重用的组件化开发模式!

Web Components 并不是新的概念!该规范最早于 2011 年推出!经多 10 多年的发展,这套规范逐渐壮大并被主流浏览器所实现。在国内,我们可能对它并不熟悉甚至会感到陌生,日常开发中我们也很少会遇到它,那它就不重要了吗?

回答这个问题之前,先简单列举一些目前较为知名的 Web Components 组件库和框架,以及基于此创建的网站应用程序:

  • Stencil:一个专门用于构建 Web Components 的库,可集成于 React、Vue 等 JavaScript 框架(Github 11.8k)
  • Lit:Google 推出的用于构建 Web Components 的库,同时提供响应式状态、作用域样式和声明式模板系统,与 Stencil 相比更像是一个完整的 UI 框架(Github 15.5k)
  • FAST:微软推出的 Web Component 组件库,同时提供了自定义 Web Components 的方法(Github 8.5k)
  • YouTube:打开控制台,您会发现大量的自定义元素!
  • Photoshop网页版:对!您没看错!这就是 Adobe 使用 Lit 构建的网页版 Photoshop (目前还处于 Beta 版本)
  • MSN:微软使用基于 FAST 的 Web Components 重构了 MSN(之前是 React 构建的)

现在回答上面的问题,Web Components 不重要吗?

我想说:未必! 至少对于原生组件开发的方向,它依然是目前唯一并且权威的解决方案!

Vue 官网文档明确指出其组件语法部分参考了 Web Components(通过本文下面的介绍您也会发现 Vue 与原生 Web Components 的某些相似之处),这足以说明这套规范并不是“空无主义”,它的出现的确对 Web 开发的模式和流程产生了重要的影响。

现如今我们为了复用组件、增强文档自定义能力,我们在 Web 开发中大多都会使用一些 JavaScript 框架,如:Vue、 React 等。但随着 Web 技术的发展、Web Components 的完善与改进,在未来的某一天,我们真的有可能会在构建 UI 时,将专注点从 JavaScript 上重新转移到原生 HTML 中!

目前,Web Components 规范包含三项主要技术:

  • Custom element(自定义元素):一组 JavaScript API,允许您自定义 HTML 标签及其行为。
  • Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML template and slots(HTML 模板和插槽): <template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

1. Custom element

Web Components 标准非常重要的一个特性是,它使开发者能够将 HTML 页面的功能封装为 custom elements(自定义元素),而往常,开发者不得不写一大堆冗长、深层嵌套的标签来实现同样的页面功能。

创建一个自定义元素很简单,整体分为两步:

  • 通过类(class)来定义自定义元素的结构和内容。
  • 通过全局对象 customElements 里的 define 方法来注册自定义元素。

先看一个简单的例子:

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>CustomElements —— BakerChen</title><style>.userInfo{color:rgb(103, 103, 232);}</style></head><body><!-- 使用自定义元素,可以像普通元素一样添加class --><user-infoname="baker"class="userInfo"></user-info><script>// 先创建一个继承与HTMLElement的类classUserInfoextendsHTMLElement{constructor(){// 必须在构造器中先调用一下 supersuper();// 使用attachShadow创建根元素const shadow =this.attachShadow({mode:'open'});// 创建各种元素const info = document.createElement('span');
      info.setAttribute('class','info');// 获取元素的name属性const text =this.getAttribute('name');
      info.textContent = text;// 创建style标签用来存放样式const style = document.createElement('style');
      style.textContent =`
        .info {
          font-size: 80px;
          padding: 10px;
          background: #333;
          border-radius: 10px;
        }
      `;// 将元素添加到根元素中
      shadow.appendChild(style);
      shadow.appendChild(info);}}// 注册新的元素user-info,UserInfo为该元素的处理类
  customElements.define('user-info', UserInfo);</script></body></html>

效果:
CustomElements

上述代码中

this.attachShadow({mode: 'open'})

的作用可以简单理解为创建一个容器(如上图中的

#shadow-root(open)

),在该容器内可以进行任何的 DOM 操作,这其实是 Shadow DOM(影子 DOM)的 API,所以下面再对其细说。

因为自定义元素是使用类来定义,所以它还可以实现继承:

JavaScript:

// 定义一个继承自原生 input 的元素 classMyH1extendsHTMLInputElement{constructor(){super()// 定制一些效果this.value ='这是继承自原生input的自定义input'this.style.minWidth ='200px'this.style.padding ='5px 10px'this.style.border ='1px solid red'this.style.borderRadius ='5px'}}// 在使用 customElements.define 注册时,传入第三个参数(配置对象)来指定这个元素是继承自 input 的
customElements.define('my-h1', MyH1,{extends:"input"});

HTML:

<!-- 在使用继承原生标签的自定义元素时不是直接使用自定义元素,
而是使用继承的原生标签,然后通过 is 属性来指定自定义元素 --><inputtype="text"is="my-h1">

效果:

2023-07-27-18-45-08

自定义元素还具有自己的生命周期:

  • onnectedCallback:当自定义元素首次被插入文档 DOM 时,被调用。
  • disconnectedCallback:当自定义元素从文档 DOM 中删除时,被调用。
  • adoptedCallback:当自定义元素被移动到新的文档时,被调用。
  • attributeChangedCallback: 当自定义元素增加、删除、修改自身属性时,被调用(即使元素未挂载)。

这里通过一个完整的例子演示:

JavaScript:

// 创建自定义元素classSquareextendsHTMLElement{// 如果需要在元素属性变化后,触发attributeChangedCallback()回调函数,必须使用observedAttributes() get 函数来监听这个属性。staticgetobservedAttributes(){// 监听c和l这两个属性,当这俩属性改变时会触发attributeChangedCallback函数return['size','color'];}constructor(){super();const shadow =this.attachShadow({mode:'open'});const div = document.createElement('div');const style = document.createElement('style');
    shadow.appendChild(style);
    shadow.appendChild(div);}// 定义生命周期connectedCallback(){
    console.log('元素首次被插入文档 DOM ');const shadow =this.shadowRoot;// 拿到自定义元素的根容器 (💢 注意这一行)// 元素首次挂载时先设置一下自己的尺寸,通过设置style标签内容来实现
    shadow.querySelector('style').textContent =`
      div {
        width: ${this.getAttribute('size')}px;
        height: ${this.getAttribute('size')}px;
        background-color: ${this.getAttribute('color')};
      }
    `;}disconnectedCallback(){
    console.log('元素从文档 DOM 中删除');}adoptedCallback(){
    console.log('元素被移动到新的文档');}attributeChangedCallback(name, oldValue, newValue){
    console.log('元素增加、删除、修改自身属性');const shadow =this.shadowRoot;
    shadow.querySelector('style').textContent =`
      div {
        width: ${this.getAttribute('size')}px;
        height: ${this.getAttribute('size')}px;
        background-color: ${this.getAttribute('color')};
      }
    `;}}// 注册
customElements.define('custom-square', Square);const add = document.querySelector('.add');const pink = document.querySelector('.pink');const black = document.querySelector('.black');const remove = document.querySelector('.remove');let square = document.querySelector('custom-square');

add.onclick=function(){
  square = document.createElement('custom-square')// 初始的样式
  square.setAttribute('size','50');
  square.setAttribute('color','yellow')
  document.body.appendChild(square);};

pink.onclick=function(){
  square.setAttribute('size','200');
  square.setAttribute('color','pink');};

black.onclick=function(){
  square.setAttribute('size','100');
  square.setAttribute('color','black');};

remove.onclick=function(){
  square.remove()};

HTML:

<div><buttonclass="add">add</button><buttonclass="pink">200px pink</button><buttonclass="black">100px black</button><buttonclass="remove">remove</button></div>

效果:

需要注意的是:点击 “add” 时,会先运行两次

setAttribute

,然后将自定义元素插入到了文档中,所以控制台会打印:

元素增加、删除、修改自身属性

元素增加、删除、修改自身属性

元素首次被插入文档 DOM

2. Shadow DOM

Shadow DOM 其实很简单,它就是相当于一个独立的,里面内容不会影响外部作用域的文档容器,可以把它当作一个 div 来进行各种 DOM 操作,比如添加子元素,添加属性等等。

MDN 上指出有一些 Shadow DOM 特有的术语:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blBcXCo4-1692172950902)(https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM/shadowdom.svg)]

  • Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
  • Shadow tree:Shadow DOM 内部的 DOM 树。
  • Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
  • Shadow root: Shadow tree 的根节点。

Shadow DOM 最常用的地方就是在自定义元素里,上面介绍自定义元素时就是通过 Shadow DOM 的 Api 来创建容器的,Element.attachShadow 方法可以创建一个 Shadow DOM ,并把它挂载到 Element 元素下,函数返回对 Shadow DOM 的引用。

在使用

Element.attachShadow

方法时必须传入一个配置对象,这个配置对象内必须具有一个 mode 属性,该属性有两个值:

  • open: 表示可以从 js 外部访问 shadow root 根节点,例如上面自定义元素生命周期例子中表明需要注意的那一行js:const shadow = this.shadowRoot
  • closed: 拒绝从 js 外部访问关闭的 shadow root 根节点
Element.shadowRoot

目前还是一个实验性的方法,它用来获取通过

Element.attachShadow

挂载的 Shadow DOM 。

3.

<template>

原生的

<template>

标签,里面的内容在加载页面时不会被呈现出来,但这个标签本身会呈现在 DOM 树中:

原生template标签

从上图可以看到,

<template>

里面的实际内容被包含进了一个文档碎片(DocumentFragment)中。

当我们想要将实际内容展示到页面上时,我们可以通过 JavaScript 来进行操作:

<templateid="my-name"><h1>Baker</h1></template><buttonid="btn">test</button><script>const template = document.getElementById("my-name");// template元素有个content属性,该属性是只读的 DocumentFragment ,包含了模板所表示的 DOM 树。const templateContent = template.content;
  document.body.appendChild(templateContent);const btn = document.getElementById("btn");
  btn.onclick=function(){
    console.log(template.content);}</script>

2023-08-13-16-01-16

注意!

上述代码中,是使用 appendChild 将

<template>

DocumentFragment

插入到文档末尾。

由于

appendChild

作用于

DocumentFragment

时,会将

DocumentFragment

的全部内容 转移(不是复制!!!) 到指定父节点的子节点列表中。

所以当你点击

test

按钮时,打印出的

<template>

DocumentFragment

内容为空,如果你不想改动

<template>

的内容,可以在使用

appendChild

之前对其克隆一份:

document.body.appendChild(templateContent.cloneNode(true));

**Vue与原生

<template>

的区别:**

在 Vue 中有一个

<template>

内置元素 ,这势必会导致与原生的

<template>

标签出现冲突,并且 Vue2 与 Vue3 在模版解析时遇到

<template>

标签的行为还不太一样,下面我们来看看:

Vue 中

<template>

内置元素 的行为与原生的刚好相反:Vue 的

<template>

内置元素 里面的内容会被呈现,只是

<template>

这个标签本身不会被呈现,也就是说 Vue 中的

<template>

内置元素 只是一个虚拟容器的占位符。

**Vue2 默认会将模版中的

<template>

解析为 Vue 内置元素,而不是原生的

<template>

标签**:

Vue2中template内置组件的行为

**那么我们如何在 Vue2 中使用原生的

<template>

标签呢?**

方法稍微有点麻烦,我们需要注册一个组件并自定义

render

渲染函数,来手动创建原生的

template

标签:

<template><divid="app"><!-- 使用注册的组件来应用原生的template标签 --><my-component></my-component></div></template><script>exportdefault{components:{MyComponent:{// 自定义渲染函数,使用 createElement 创建原生的 template 标签render:function(createElement){returncreateElement('template',[createElement('h1','Baker')]);},},},};</script>

这样之后的效果就与原生使用

<template>

一致了:

vue2使用原生template

然而在 Vue3 中就没有 Vue2 那么麻烦了!因为 Vue3 与 Vue2 正好相反:

**Vue3 默认会将模版中的

<template>

解析为原生的

<template>

标签,而不是内置的

<template>

元素!**

只有当

<template>

v-if

v-else-if

v-else

v-for

v-slot

中任一指令一起使用时,Vue3 才会将

<template>

当作内置的

<template>

元素来处理。

注意!
上面提到的 Vue

<template>

内置元素并不包括 Vue 单文件组件中包裹整个模版的顶层

<template>

标签(该顶层标签不是模板本身的一部分!)。

4.

<slot>

原生的

<slot>

标签与 Vue 中的基础用法基本一致,并且 MDN 上对其的中文解释也是参考了 Vue 的文档。

有意识的是,Vue 官方文档上说明:

“ Vue 组件的插槽机制是受原生 Web Component

<slot>

元素的启发而诞生,同时还做了一些功能拓展。”

只能说是青出于蓝而胜于蓝了🧐

<slot>

可以理解为一个占位内容,当使用时可以对其进行定制化的替换,这样就能满足一个通用组件在各种应用场景下的可定制性。可以为其设置一个

name

属性,外界可以使用此

name

来选择性的替换对应的

<slot>

内容。

单独使用它没有太大的意义,因此常将它与

<template>

、Custom element、Shadow DOM 配合使用,下面是一个完整的使用案例:

<!-- 使用 template 创建一个模版 --><templateid="user-info"><!-- 模版内样式 --><style>*{margin: 0;padding: 0;}.container{display: inline-block;border: 1px solid #ccc;border-radius: 10px;padding: 10px;width: 200px;height: 80px;}.username{color: pink;}.userinfo{color: #626161;text-decoration: green wavy underline;}</style><!-- 模版内结构 --><divclass="container"><h1class="username"><!-- 使用 slot 具名插槽,外部使用时可以通过 slot="username" 来替换此内容 --><slotname="username">Baker</slot></h1><pclass="userinfo"><!-- 使用 slot 默认插槽,外部使用时,自定义元素的内部元素(没有指定slot的元素)会替换此内容 --><slot>一个前端菜鸟</slot></p></div></template><!-- 使用自定义元素 --><user-info></user-info><user-info><!-- 替换模版中的 <slot name="username">Baker</slot> 内容 --><spanslot="username">张三</span><!-- 替换模版中 <slot>一个前端菜鸟</slot> 内容 --><span>一个大帅哥</span><!-- 上面等价于 <span slot>一个大帅哥</span> --></user-info><user-info><spanslot="username">李红</span>
  一个小美女
</user-info><script>// 创建自定义元素 user-info 的处理类classUserInfoextendsHTMLElement{constructor(){// 必须在构造器中先调用一下 supersuper();// 获取模版内容const template = document.getElementById("user-info");const templateContent = template.content;// 使用 影子DOM API attachShadow 创建自定义元素的容器const shadowRoot =this.attachShadow({mode:'open'});// 将模版内容添加到容器当中
      shadowRoot.appendChild(templateContent.cloneNode(true));}}// 注册自定义元素user-info,UserInfo为该元素的处理类
  customElements.define('user-info', UserInfo);</script>

效果:

2023-08-13-16-54-35

通过上面的这个例子可以明白,将

<template>

与 Custom element、Shadow DOM 结合使用可以构建一个通用组件,而引入

<slot>

可增加该组件的可定制性。

<template>

不同的是,Vue 在模版解析时遇到

<slot>

会根据使用场景及用户配置自动进行处理(毕竟原生的和 Vue 的差距并不是很大)

结语

通过前面的介绍,您会发现 Web Component 是一套丰富的技术方案,它通过 Custom element、Shadow DOM、

<template>

<slot>

的结合使用来在原生层面实现组件的封装与复用

组件化开发是一种趋势,原生的支持为各种 UI 框架提供了标准化的思路和方向,UI 框架的完善与扩展也反过来刺激了原生方案的发展。只是不知道原生的支持是否会持续快速的更新,我们拭目以待!

参考:

  • 2023 Web Components 现状(译文)

Web Components 规范庞大且复杂,本文所介绍的内容只是其中比较直观且易于理解的一部分内容,如果大佬们意犹未尽,可以去查阅官方规范,也欢迎在评论区留言讨论。

标签: html5 前端 javascript

本文转载自: https://blog.csdn.net/m0_51969330/article/details/132271077
版权归原作者 Baker-Chen 所有, 如有侵权,请联系我们删除。

“十年了,您还不认识 WebComponent 吗?!”的评论:

还没有评论