Web Components入门不完全指北

目前流行的各类前端框架,不管是react, angular还是vue,都有一个共同点,那就是支持组件化开发,但事实上随着浏览器的发展,现在浏览器也原生支持组件式开发,本文将通过介绍Web Components 的三个主要概念,带读者无障碍入门Web Components

1.Custom Elements(自定义组件)

顾名思义,Web Component 提供了一套模式让我们自定义组件,采用自定义类的方式,当我们需要自定义一个组件,我们需要声明一个类,并继承自原生的DOM 类,下面先给出一个示例

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面。");
  }

  disconnectedCallback() {
    console.log("自定义元素从页面中移除。");
  }

  adoptedCallback() {
    console.log("自定义元素移动至新页面。");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`属性 ${name} 已变更。`);
  }
}

customElements.define("my-custom-element", MyCustomElement);

如上面的代码所示,我们定义了一个最简单的类,这个类继承自HTMLElement,它有四个发方法,分别代表组件的四个生命周期,接着我们需要调用customElements.define这个方法来注册一个自定义组件,这个方法包含三个参数,第一个参数为自定义组件的名字,第二个参数我们需要传入刚刚定义的类,第三个参数是一个可选参数,后文会提及,这里可以先忽略,这样我们就定义个一个最简单的组件,这个组件没有任何功能没有任何形态,但是会有生命周期,我们可以像下面这样调用它

<my-custom-element></my-custom-element>

页面渲染之后我们可以看到控制台打印出下面的文本

接下来简单讲讲四个生命周期,第1,2,4这三个生命周期比较好理解,分别代表组件被添加到页面,从页面移除,以及组件参数的变化,对于组件参数,下文会提及,这里先有个概念就可以了,第三个生命周期adoptedCallback 相对来说比较难理解,移动到新页面的意思是从一个html文档移动到另一个html文档,一般情况下如果同一个组件,从普通页面移动到iframe里面,会触发这个事件,有兴趣的小伙伴可以自己尝试一下。自此我们简单的了解了如何创建一个最简单的自定义组件,接下来我们来了解一下Web Components 的核心,Shadow DOM。

2.Shadow DOM(影子dom)

Shadow DOM 这个名字直译过来就是影子DOM,大部分人看到这个名字可能会联想到react或者vue的虚拟dom,但这两者基本上不是一个东西,虚拟dom是用js对dom结构做一个映射,用js对象来表示dom结构,用来提升性能的一个东西,而影子dom反而可以理解为一个实实在在的dom,但提供了样式,dom结构隔离等功能的东西,为了方便理解,我们来看看下面的html代码

<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8"/>
        <title>web components</title>
    </head>
    <body>
        <input type="range"/>
        <input/>
    </body>
</html>

我们可以看到下面的页面

有没有考虑过一个问题,同样是input控件,为什么可以展现出不同的功能和样式?当然肯定有人会说是浏览器根据标签传入的参数渲染的结果,这么说也没毛病,但其实我们可以仔细窥探一下这里面的实现原理,首先我们打开一下浏览器的一下开关,路径为 设置/performance/Elements, 把下面这个开关打开

打开之后我们会发现,dom结构发生了很大的变化,我们看到了一些原本被隐藏起来的东西

shadow-root 底下的dom结构明明白白的告诉你这个组件是怎么被构建的,包括样式结构等等,可以这么理解,这部分就是原生组件的shadow dom,而Web Components把这部分功能开放出来,我们也可以利用shadow dom来构建自己的自定义组件,当然构建自定义组件不一定需要用到shadow dom, 只是用上它,能让组件更好的内聚,避免被其它组件影响,shadow dom 提供了一个隔离机制,可以使得组件内部的dom结构和style都仅作用于组件内部,并且不受外部的影响,外部也无法访问到组件内部的东西,从而避免全局样式之间的互相影响,不同组件之间你甚至可以使用相同类名而不用添加hash之类的东西用于区分

接下来我们开看看如何用shadow dom 创建一个简单的组件,我们在第一个组件里面添加一些内容

//components/MyCustomElement.js

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面。");
    const shadow = this.attachShadow({ mode: "open" });
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(".wrapper {color:red;}");

    const wrapper = document.createElement("span");
    wrapper.innerHTML = this.getAttribute("content");
    wrapper.setAttribute("class", "wrapper");
    shadow.adoptedStyleSheets = [sheet];
    shadow.appendChild(wrapper);

    const style = document.createElement("style");
    style.textContent = `
      .wrapper{
          color:red;
      }
      `;
  }

customElements.define("my-custom-element", MyCustomElement);
<!-- index.html -->

<my-custom-element content="wrapper1"></my-custom-element>

我们选择在connectedCallback 添加了一些代码,目的是在自定义组件添加至页面之后,我们通过shadow dom 给它内部添加一个span并指定样式,下面我们简单解释一下代码

shadow dom不能脱离宿主单独存在,所以我们在一开始通过调用attachShadow 方法给当前组件附加影子dom,这个方法支持传入一个mode参数,这个参数可以设置为“open”或者“closed”, open代表开放shadow dom, 虽然我们无法直接访问到shadow dom里面的元素,但还是可以通过特定途径访问得到,closed则代表对外部彻底关闭(一个前提,这些都是基于君子协议,并非可以真正封闭组件,通过浏览器插件等方式依旧可以访问到内部的内容,这并不是一个严格的安全措施)

添加样式的代码比较简单,就不进行解释,接下来我们通过getAttribute方法来获取组件的content属性,并将它作为span的内容,我们可以看到渲染出了文字,并添加了样式

现在我们在html上添加一些对比内容来展示一下shadow dom对于样式跟dom的隔离效果

<!-- index.html -->
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8"/>
        <title>web components</title>
        <link href="index.css" rel="stylesheet"/>
    </head>
    <body>
        <h1>web components</h1>
        <my-custom-element content="wrapper1" id="myCustomElement"></my-custom-element>
        <span class="wrapper">wrapper2</span>
        <button id="test">get dom from shadow dom</button>
        <script src="./components/MyCustomElement.js" defer></script>
        <script src="index.js"></script>
    </body>
</html>
/* index.css */
.wrapper{
    color:blue;
}
//index.js
window.onload = () => {
  document.getElementById("test").addEventListener(
    "click",
    () => {
      var wrapper = document.querySelectorAll(".wrapper");
      if (wrapper) {
        console.log("1");
        console.log(wrapper);
        console.log(wrapper[0].innerHTML)
      }

//通过宿主的shadowRoot来获取内部dom
      wrapper = document
        .getElementById("myCustomElement")
        .shadowRoot.querySelectorAll(".wrapper");
      if (wrapper) {
        console.log("2");
        console.log(wrapper);
        console.log(wrapper[0].innerHTML)
      }
    },
    false
  );
};

运行代码之后我们看到是下图的效果

红色的是组件内部定义的颜色,蓝色是组件外定义的全局样式,两者之间没有相互影响,样式之间做到了互相隔离,事实上不管我们以上么样的方式引入样式,组件的样式都不会影响到其他的组件。我们再尝试点击按钮,我们可以看到打印了下面的结果

我们发现,如果用普通的查询手段我们是无法查询到自定义组件内部的dom节点的,但我们可以通过宿主的shadowRoot来获取内部的dom,但这是有一个前提的,就是前文提到的,我们把mode参数设置成了open, 如果我们把mode修改成closed, 输出则会变成这样,我们无法访问到shadowRoot

shadowRoot 返回了null,因此我们的代码报错了 。

接下来,我们把第一段里面的坑填回去,我们现在已经知道我们可以通过Web Components来创建自定义组件了,但这对于日常的开发是远远不够的,我们有时候会有这样的需求,我们需要基于原有的组件进行二次开发,也就是说我们想继承原生组件的功能,但我们想新增一些功能上去,这样改怎么做呢,答案是回到customElements.define,我们添加第三个参数,把需要继承的原生组件名字赋值给extends,类似下面,就是自定义一个继承自textarea的组件

customElements.define("my-text-area", MyTextArea, { extends: "textarea" });

下面我们举个简单的例子,实现一个很常见的需求,我们将实现一个自定义的textarea, 下方显示能接受的最大字数和当前字数

//components/MyTextArea.js
class MyTextArea extends HTMLTextAreaElement {
  constructor() {
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面2。");
    var div = document.createElement("div");
    div.innerText = `0/${this.getAttribute("maxlength")}`;
    this.parentNode.insertBefore(div, this.nextSibling);
    this.addEventListener(
      "input",
      (e) => {
        div.innerText = `${e.currentTarget.value.length}/${this.getAttribute(
          "maxlength"
        )}`;
      },
      false
    );
  }
}

customElements.define("my-text-area", MyTextArea, { extends: "textarea" });

<!-- index.html -->
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8"/>
        <title>web components</title>
        <link href="index.css" rel="stylesheet"/>
    </head>
    <body>
        <h1>web components</h1>
        <textarea is="my-text-area" maxlength="20"></textarea>
        <textarea is="my-text-area" maxlength="50"></textarea>
        <script src="./components/MyTextArea.js" defer></script>
        <script src="index.js"></script>

    </body>
</html>

通过上述代码,我们可以看到运行的效果如下

 我们通过继承textarea组件,可以获取原生组件的所有功能,并能在这个功能的基础上扩展出自己的功能,上面我们做的事情很简单,在自定义组件被加载之后,我们通getAttribute函数获取当前传入的maxlength来获取允许输入的最大值,然后监听input事件,实时获取当前的字数,实现过程比较简单,这里就不再赘述了

3.Template and Slot(模板与插槽)

其实到了这里,我们已经基本上能够利用上述的功能来实现我们业务需求了,但还不够灵活,例如我们日常开发过程中经常会有这样的需求,在自定义组件下面给他添加子组件,就像普通的div一样,可以层层嵌套,类似这样:

<my-element>
    <div>
        <div>let's have some differences</div>
    </div>
</my-element>

基于但不仅仅基于这种需求,我们引入另外两个概念

3.1 Template(模板)

模板,顾名思义,我们可以在html文件里面定义一系列可重用的模板,这些模板除非最终被使用,否则不会渲染到页面中来,我们看看下面的代码

<!-- index.html -->
<!DOCTYPE html>

<template id="my-paragraph">
    <p>my paragrapy</p>
</template>

<html>
    <head>
        <meta charset="utf-8"/>
        <title>web components</title>
    </head>
    <body>
        <h1>web components</h1>
        <my-slot-element>
        </my-slot-element>
        <script src="./components/MySlotElement.js" defer></script>

    </body>
</html>
//components/MySlotElement.js

class MySlotElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    console.log("自定义元素添加至页面3。");
    let template = document.getElementById("my-paragraph");
    let templateContent = template.content;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(templateContent.cloneNode(true));
  }
}

customElements.define("my-slot-element", MySlotElement);

 上述代码实现的效果如下

上述的代码通过id获取到当前的template,通过template.content我们可以获取到当前模板的内容,然后我们我么们通过cloneNode方法把template的内容复制一份添加到影子DOM里面,注意我们传入了一个参数true,表示递归复制此节点下的所有子孙节点

当然,如果只是仅仅实现这样一个html代码重用的效果,template可能显得不是很有必要,毕竟我们有很多的方法来实现代码的重用,之所以把它拎出来讲,是因为template实现了插槽(slot)的功能

3.2 Slot(插槽)

Slot给template模板提供了自定义扩展的功能,我们可以在特定的位置先埋下插槽,在后续使用模板的过程中,我们可以把特定的代码插入到对应的插槽当中,我们来对上面的代码做一些扩展,我们修改一下template的定义,实现插槽的功能

<!DOCTYPE html>

<template id="my-paragraph">
    <p>my paragrapy</p>
    <slot name="my-text">my defalut text</slot>
</template>

<html>
    <head>
        <meta charset="utf-8"/>
        <title>web components</title>
        <link href="index.css" rel="stylesheet"/>
    </head>
    <body>
        <h1>web components</h1>
        <my-slot-element>
            <div slot="my-text">
                <div>let's have some differences</div>
            </div>
        </my-slot-element>
        <script src="./components/MySlotElement.js" defer></script>

    </body>
</html>

 实现的效果如下

我们在template里面预先埋入了一个叫my-text的插槽,然后我们在自定义组件的子节点编写了自定义的内容,并且指定了slot的名称,我们发现渲染之后,子节点被渲染到于P标签同一个层级的dom结构中,注意,如果我们在template中删除slot标签,自定义组件的子节点是不会被渲染出来的,而如果我们没有传入自定义内容到指定的slot中,slot中的默认内容将会被展示,这里由于篇幅原因,就不展示了,有兴趣的话可以自行测试

看到这里写过vue的小伙伴应该会觉得很熟悉,这个写法几乎如出一辙,事实上vue设计slot的时候是借鉴了Web Components的slot的(没错,Web Component出来的时间比vue早)

4.兼容性

接下来就是喜闻乐见的兼容性程度了,这关乎这个技术能否直接应用到生产当中,截至到本篇文章发布,目前浏览器对主要的特性支持如下

除了个别api没被支持之外,大部分的api都已经被主流的浏览器所支持,当然了,哪里有不兼容,哪里就会有polyfills, 例如polyfills可以对大部分不兼容的api提供支持,各位可以自行探索

以上,希望各位封装组件愉快,有什么疑问见解或者想交流的都可以评论区见

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/268367.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分析冒泡排序

#include <stdio.h> int main() { int arr[10] { 2,5,1,3,6,4,7,8,9,0 }; int i 0; int j 0; for( i 0 ;i < sizeof(arr)/sizeof(arr[0]) - 1 ; i) 红色的代表数组一共有n个元素&#xff0c;则需要n-1次 { for( j 0 // 这里可以让数组从哪一…

设计模式--职责链模式

实验15&#xff1a;职责链模式 本次实验属于模仿型实验&#xff0c;通过本次实验学生将掌握以下内容&#xff1a; 1、理解职责链模式的动机&#xff0c;掌握该模式的结构&#xff1b; 2、能够利用职责链模式解决实际问题。 [实验任务]&#xff1a;财务审批 某物资管理系统…

canvas入门笔记(上)

Canvas Canvas简介 Canvas API 提供了一个通过JavaScript 和 HTML的元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。 Canvas API 主要聚焦于 2D 图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的 2D 和…

抠图、换背景、正装图证件照制作方法

本篇灵感是最近又要使用别的底色的正装照的图片。上学的时候&#xff0c;要求证件照的底色是蓝底、党员档案里要求图片的底色是红底、 将来上班的证件照要求是白底&#xff0c;并且无论是考研还是找工作都是制作简历的时候&#xff0c;根据简历的样板不同需要更换不同的底色。 …

Webpack基础使用

目录 一.什么是Webpack 二.为什么要使用Webpack 三.Webpack的使用 1.下载yarn包管理器 2.Webpack的安装 3.Webpack的简单使用 4.效果 四.Webpack打包流程 一.什么是Webpack Webpack是一个静态模块打包工具 二.为什么要使用Webpack 在开发中&#xff0c;我们常常会遇到…

[Linux] Mysql数据库中的用户管理与授权

一、登录用户的管理 1.1 查看用户密码的信息 用户信息存放在 mysql 数据库下的 user 表&#xff08;MySQL 服务下存在一个系统自带的 mysql 数据库&#xff09;。 use mysql ; show tables; desc user; 查看密码信息的命令&#xff1a; 能看到密码信息&#xff1a;是经过加…

【大数据】NiFi 的基本使用

NiFi 的基本使用 1.NiFi 的安装与使用1.1 NiFi 的安装1.2 各目录及主要文件 2.NiFi 的页面使用2.1 主页面介绍2.2 面板介绍 3.NiFi 的工作方式3.1 基本方式3.2 选择处理器3.3 组件状态3.4 组件的配置3.4.1 SETTINGS&#xff08;通用配置&#xff09;3.4.2 SCHEDULING&#xff0…

饥荒Mod 开发(二二):显示物品信息

饥荒Mod 开发(二一)&#xff1a;超大便携背包&#xff0c;超大物品栏&#xff0c;永久保鲜 饥荒中的物品没有详细信息&#xff0c;基本上只有一个名字&#xff0c;所以很多物品的功能都不知道&#xff0c;比如浆果吃了也不知道恢复什么&#xff0c; 采集的胡萝卜也不知道什么功…

Airbert: In-domain Pretraining for Vision-and-Language Navigation

题目&#xff1a;Airbert&#xff1a;视觉和语言导航的域内预训练 摘要 为了解决VLN数据集稀缺的问题&#xff0c;本文创建了一个数据集BNB。我们首先从在线租赁市场的数十万个列表中收集图像标题 (IC) 对。接下来&#xff0c;我们使用 IC 对提出自动策略来生成数百万个 VLN …

OpenCV与YOLO学习与研究指南

引言 OpenCV是一个开源的计算机视觉和机器学习软件库&#xff0c;而YOLO&#xff08;You Only Look Once&#xff09;是一个流行的实时对象检测系统。对于大学生和初学者而言&#xff0c;掌握这两项技术将大大提升他们在图像处理和机器视觉领域的能力。 基础知识储备 在深入…

Apache Commons IO: 简化文件和IO操作

第1章&#xff1a;引言 咱们在做Java编程的时候&#xff0c;经常会遇到各种文件操作和输入输出&#xff08;IO&#xff09;的问题。不论是读取一个配置文件&#xff0c;还是把数据写入日志&#xff0c;这些看似简单的任务有时候会让人头疼。传统的Java IO操作&#xff0c;虽然…

Odoo16 实用功能之Form视图详解(表单视图)

目录 1、什么是Form视图 2、Form视图的结构 3、源码示例 1、什么是Form视图 Form视图是用于查看和编辑数据库记录的界面。每个数据库模型在Odoo中都有一个Form视图&#xff0c;用于显示该模型的数据。Form视图提供了一个可编辑的界面&#xff0c;允许用户查看和修改数据库记…

设计模式--迭代器模式

实验18&#xff1a;迭代器模式 本次实验属于模仿型实验&#xff0c;通过本次实验学生将掌握以下内容&#xff1a; 1、理解迭代器模式的动机&#xff0c;掌握该模式的结构&#xff1b; 2、能够利用迭代器模式解决实际问题。 [实验任务]&#xff1a;JAVA和C常见数据结构迭代…

PyTorch随机数生成:torch.rand,torch.randn,torch.randind,torch.rand_like

在用PyTorch做深度学习开发过程中&#xff0c;时常用到随机数生成功能&#xff0c;但经常记不住几个随机数生成函数的用法&#xff0c;现在正好有点时间&#xff0c;整理一下。 1. torch.rand() torch.rand(*size, *, generatorNone, outNone, dtypeNone, layouttorch.stride…

我们是如何测试人工智能的(一)基础效果篇(内含大模型的测试内容)

来源&#xff5c;TesterHome社区 作者&#xff5c;孙高飞 前言 这个系列算是科普文吧&#xff0c;尤其这第一篇可能会比较长&#xff0c;因为我这8年里一直在 AI 领域里做测试&#xff0c;涉及到的场景有些多&#xff0c;我希望能尽量把我经历过的东西都介绍一下&#xff0c;…

<JavaEE> 协议格式 -- 传输层协议 UDP

目录 一、UDP协议格式长啥样&#xff1f; 二、端口号和IP地址 1&#xff09;UDP协议中包含哪两个端口号&#xff1f; 2&#xff09;有没有包含IP地址&#xff1f; 三、UDP报文长度 1&#xff09;UDP报文长度最长多长&#xff1f; 2&#xff09;UDP报文的组成&#xff1f…

Socket地址

socket地址其实是一个结构体&#xff0c;封装端口号和IP等信息 。后面的 socket 相关的 api 中需要使用到这个socket地址。 客户端 -> 服务器需要知道服务器的&#xff08; IP, Port &#xff09; 一、通用 socket 地址 socket 网络编程接口中表示 socket 地址的是结构体…

linux cpu调度分析

一、cpu调度调试方法 echo 0 > /sys/kernel/debug/tracing/tracing_on echo > /sys/kernel/debug/tracing/trace echo 30720 > /sys/kernel/debug/tracing/buffer_size_kb echo nop > /sys/kernel/debug/tracing/current_tracer echo sched_switch sched_wakeup s…

MYSQL函数\约束\多表查询\事务

函数 字符串函数 数值函数 mod就是取余 日期函数 流程函数 约束 外键约束 删除更新\外键 多表查询 多表关系 一对多 多对多 一对一 多表查询 内连接 select e.name d.name from emp e join dept d on e.id d.id; 外连接 select emp.*, d.name from emp left join tm…

计算机网络(4):网络层

网络层提供的两种服务 虚电路服务&#xff08;Virtual Circuit Service&#xff09;和数据报服务&#xff08;Datagram Service&#xff09;是在网络层&#xff08;第三层&#xff09;提供的两种不同的通信服务。它们主要区别在于建立连接的方式和数据传输的方式。 虚电路服务…
最新文章