联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

十年了,您还不认识Web组件吗?!

作者:小教学发布时间:2023-09-17分类:程序开发学习浏览:102


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

十年了,您还不认识Web组件吗?!


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


📑目录

    • 前言
    • 1.自定义元素
    • 2.阴影DOM
    • 3.`<;模板>;`
    • 4.`<;槽>;`
    • 结语


前言

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

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

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

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

  • 模具:一个专门用于构建Web Components的库,可集成于Reaction、Vue等脚本框架(Github 11.8k)
  • LIT:谷歌推出的用于构建网络组件的库,同时提供响应式状态、作用域样式和声明式模板系统,与模板相比更像是一个完整的用户界面框架(Github15.5k)
  • FAST:微软推出的Web Component组件库,同时提供了自定义Web Components的方法(Github8.5k)
  • YouTube:打开控制台,您会发现大量的自定义元素!
  • PhotoShop网页版:对!您没看错!这就是Adobe使用Lit构建的网页版PhotoShop(目前还处于测试版版本)
  • Msn:微软使用基于FAST的网络组件重构了msn(之前是React构建的)

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

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

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

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

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

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

1.自定义元素

Web组件标准非常重要的一个特性是,它使开发者能够将超文本标记语言页面的功能封装为定制元素(自定义元素),而往常,开发者不得不写一大堆冗长、深层嵌套的标签来实现同样的页面功能.

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

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

先看一个简单的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="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-info name="baker" class="userInfo"> </user-info>

  <script>
  // 先创建一个继承与HTMLElement的类
  class UserInfo extends HTMLElement {
    constructor() {
      // 必须在构造器中先调用一下 super
      super();

      // 使用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>

效果:
十年了,您还不认识Web组件吗?!

上述代码中this.attachShadow({mode: 'open'})的作用可以简单理解为创建一个容器(如上图中的#shadow-root(open)),在该容器内可以进行任何的DOM操作,这其实是Shadow Dom(影子DOM)的api,所以下面再对其细说。

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

JavaScrip:

// 定义一个继承自原生 input 的元素 
class MyH1 extends HTMLInputElement {
  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 属性来指定自定义元素 -->
<input type="text" is="my-h1">

效果:

十年了,您还不认识Web组件吗?!

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

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

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

JavaScrip:

// 创建自定义元素
class Square extends HTMLElement {
  // 如果需要在元素属性变化后,触发attributeChangedCallback()回调函数,必须使用observedAttributes() get 函数来监听这个属性。
  static get observedAttributes() {
    // 监听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>
  <button class="add">add</button>
  <button class="pink">200px pink</button>
  <button class="black">100px black</button>
  <button class="remove">remove</button>
</div>

效果:


需要注意的是:点击“Add”时,会先运行两次setAttribute,然后将自定义元素插入到了文档中,所以控制台会打印:

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

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

元素首次被插入文档DOM

2.阴影DOM

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

Mdn上指出有一些影子DOM特有的术语:

十年了,您还不认识Web组件吗?!

  • 影子主机:一个常规DOM节点,影子DOM会被附加到这个节点上。
  • 影子树:影子DOM内部的DOM树。
  • 阴影边界:阴影DOM结束的地方,也是常规DOM开始的地方。
  • 影子根:影子树的根节点。

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

在使用Element.attachShadow方法时必须传入一个配置对象,这个配置对象内必须具有一个模式属性,该属性有两个值:

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

Element.shadowRoot目前还是一个实验性的方法,它用来获取通过Element.attachShadow挂载的暗影多姆。

3.<template>

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

十年了,您还不认识Web组件吗?!

从上图可以看到,<template>里面的实际内容被包含进了一个文档碎片(DocumentFragment)中.

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

<template id="my-name">
  <h1>Baker</h1>
</template>
<button id="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>

十年了,您还不认识Web组件吗?!

注意!

上述代码中,是使用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>标签

十年了,您还不认识Web组件吗?!

那么我们如何在Vue2中使用原生的<template>标签呢?

方法稍微有点麻烦,我们需要注册一个组件并自定义render渲染函数,来手动创建原生的template标签:

<template>
  <div id="app">
    <!-- 使用注册的组件来应用原生的template标签 -->
    <my-component></my-component>
  </div>
</template>

<script>
export default {
  components: {
    MyComponent: {
      // 自定义渲染函数,使用 createElement 创建原生的 template 标签
      render: function (createElement) {
        return createElement('template', [createElement('h1', 'Baker')]);
      },
    },
  },
};
</script>

这样之后的效果就与原生使用<template>一致了:

十年了,您还不认识Web组件吗?!

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

VUE3默认会将模版中的<template>解析为原生的<template>标签,而不是内置的<template>元素!

只有当<template>v-ifv-else-ifv-elsev-forv-slot中任一指令一起使用时,VUE3才会将<template>当作内置的<template>元素来处理.

注意!
上面提到的Vue<template>内置元素并不包括Vue单文件组件中包裹整个模版的顶层<template>标签(该顶层标签不是模板本身的一部分!).

4.<slot>

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

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

“Vue组件的插槽机制是受原生Web组件<slot>元素的启发而诞生,同时还做了一些功能拓展。“

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

<slot>可以理解为一个占位内容,当使用时可以对其进行定制化的替换,这样就能满足一个通用组件在各种应用场景下的可定制性.可以为其设置一个name属性,外界可以使用此name来选择性的替换对应的<slot>内容.

单独使用它没有太大的意义,因此常将它与<template>、自定义元素、阴影DOM配合使用,下面是一个完整的使用案例:

<!-- 使用 template 创建一个模版 -->
<template id="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>
  <!-- 模版内结构 -->
  <div class="container">
    <h1 class="username">
      <!-- 使用 slot 具名插槽,外部使用时可以通过 slot="username" 来替换此内容 -->
      <slot name="username">Baker</slot>
    </h1>
    <p class="userinfo">
      <!-- 使用 slot 默认插槽,外部使用时,自定义元素的内部元素(没有指定slot的元素)会替换此内容 -->
      <slot>一个前端菜鸟</slot>
    </p>
  </div>
</template>

<!-- 使用自定义元素 -->
<user-info></user-info>
<user-info>
  <!-- 替换模版中的 <slot name="username">Baker</slot> 内容 -->
  <span slot="username">张三</span>
  <!-- 替换模版中 <slot>一个前端菜鸟</slot> 内容 -->
  <span>一个大帅哥</span>
  <!-- 上面等价于 <span slot>一个大帅哥</span> -->
</user-info>
<user-info>
  <span slot="username">李红</span>
  一个小美女
</user-info>

<script>
  // 创建自定义元素 user-info 的处理类
  class UserInfo extends HTMLElement {
    constructor() {
      // 必须在构造器中先调用一下 super
      super();
      // 获取模版内容
      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>

效果:

十年了,您还不认识Web组件吗?!

通过上面的这个例子可以明白,将<template>与自定义元素、阴影DOM结合使用可以构建一个通用组件,而引入<slot>可增加该组件的可定制性.

<template>不同的是,Vue在模版解析时遇到<slot>会根据使用场景及用户配置自动进行处理(毕竟原生的和Vue的差距并不是很大)

结语

通过前面的介绍,您会发现Web组件是一套丰富的技术方案,它通过自定义元素、阴影DOM、<template><slot>的结合使用来在原生层面实现组件的封装与复用那就是。

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

参考:

  • 2023年网络组件现状(译文)

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





标签:十年了您还不认识Web组件吗?!_BAKER-CHEN的博客


程序开发学习排行
最近发表
网站分类
标签列表