前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Web Components 上手指南

Web Components 上手指南

作者头像
童欧巴
发布于 2021-03-18 06:30:05
发布于 2021-03-18 06:30:05
1K00
代码可运行
举报
文章被收录于专栏:前端食堂前端食堂
运行总次数:0
代码可运行

现在的前端开发基本离不开 React、Vue 这两个框架的支撑,而这两个框架下面又衍生出了许多的自定义组件库:

  • Element(Vue)
  • Ant Design(React)

这些组件库的出现,让我们可以直接使用已经封装好的组件,而且在开源社区的帮助下,出现了很多的模板项目( vue-element-admin、Ant Design Pro ),能让我们快速的开始一个项目。

虽然 React、Vue 为我们的组件开发提供了便利,但是这两者在组件的开发思路上,一个是自创的 JSX 语法,一个是特有的单文件模板的语法,两者的目标都是想提供一种组件的封装方法。毕竟都有其原创的东西在里面,和我们刚开始接触的 Web 基础的 HTML、CSS、JS 的方式还是有些出入的。今天介绍的就是,通过 HTML、CSS、JS 的方式来实现自定义的组件,也是目前浏览器原生提供的方案:Web Components。

什么是 Web Components?

Web Components 本身不是一个单独的规范,而是由一组DOM API 和 HTML 规范所组成,用于创建可复用的自定义名字的 HTML 标签,并且可以直接在你的 Web 应用中使用。

代码的复用一直都是我们追求的目标,在 JS 中可复用的代码我们可以封装成一个函数,但是对于复杂的HTML(包括相关的样式及交互逻辑),我们一直都没有比较好的办法来进行复用。要么借助后端的模板引擎,要么借助已有框架对 DOM API 的二次封装,而 Web Components 的出现就是为了补足浏览器在这方面的能力。

如何使用 Web Components?

Web Components 中包含的几个规范,都已在 W3C 和 HTML 标准中进行了规范化,主要由三部分组成:

  • Custom elements(自定义元素):一组 JavaScript API,用来创建自定义的 HTML标签,并允许标签创建或销毁时进行一些操作;
  • Shadow DOM(影子DOM):一组 JavaScript API,用于将创建的 DOM Tree 插入到现有的元素中,且 DOM Tree 不能被外部修改,不用担心元素被其他地方影响;
  • HTML templates(HTML模板):通过 <template><slot> 直接在 HTML 文件中编写模板,然后通过 DOM API 获取。

Custom elements(自定义元素)

浏览器提供了一个方法:customElements.define() , 来进行自定义标签的定义。该方法接受三个参数:

  • 自定义元素的名称,一个 DOMString 标准的字符串,为了防止自定义元素的冲突,必须是一个带短横线连接的名称(e.g. custom-tag)。
  • 定义自定义元素的一些行为,类似于 React、Vue 中的生命周期。
  • 扩展参数(可选),该参数类型为一个对象,且需要包含 extends 属性,用于指定创建的元素继承自哪一个内置元素(e.g. { extends: 'p' })。

下面通过一些例子,演示其用法,完整代码放到了 JS Bin 上。

创建一个新的 HTML 标签

先看看如何创建一个全新的自定义元素。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class HelloUser extends HTMLElement {
  constructor() {
    // 必须调用 super 方法
    super();

    // 创建一个 div 标签
    const $box = document.createElement("p");
    let userName = "User Name";
    if (this.hasAttribute("name")) {
      // 如果存在 name 属性,读取 name 属性的值
      userName = this.getAttribute("name");
    }
    // 设置 div 标签的文本内容
    $box.innerText = `Hello ${userName}`;

    // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
    const shadow = this.attachShadow({ mode: "open" });
    shadow.appendChild($box);
  }
}

// 定义一个名为 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<hello-user name="Shenfq"></hello-user>

这时候页面上就会生成一个 <p> 标签,其文本内容为:Hello Shenfq。这种形式的自定义元素被称为:Autonomous custom elements,是一个独立的元素,可以在 HTML 中直接使用。

扩展已有的 HTML 标签

我们除了可以定义一个全新的 HTML 标签,还可以对已有的 HTML 标签进行扩展,例如,我们需要封装一个与 <ul> 标签能力类似的组件,就可以使用如下方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class SkillList extends HTMLUListElement {
  constructor() {
    // 必须调用 super 方法
    super();

    if (
      this.hasAttribute("skills") &&
      this.getAttribute("skills").includes(',')
    ) {
      // 读取 skills 属性的值
      const skills = this.getAttribute("skills").split(',');
      skills.forEach(skill => {
        const item = document.createElement("li");
        item.innerText = skill;
        this.appendChild(item);
      })
    }
  }
}

// 对 <ul> 标签进行扩展
customElements.define("skill-list", SkillList, { extends: "ul" });
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<ul is="skill-list" skills="js,css,html"></ul>

对已有的标签进行扩展,需要用到 customElements.define 方法的第三个参数,且第二参数的类,也需要继承需要扩展标签的对应的类。使用的时候,只需要在标签加上 is 属性,属性值为第一个参数定义的名称。

生命周期

自定义元素的生命周期比较简单,一共只提供了四个回调方法:

  • connectedCallback:当自定义元素被插入到页面的 DOM 文档时调用。
  • disconnectedCallback:当自定义元素从 DOM 文档中被删除时调用。
  • adoptedCallback:当自定义元素被移动时调用。
  • attributeChangedCallback: 当自定义元素增加、删除、修改自身属性时调用。

下面演示一下使用方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class HelloUser extends HTMLElement {
  constructor() {
    // 必须调用 super 方法
    super();

    // 创建一个 div 标签
    const $box = document.createElement("p");
    let userName = "User Name";
    if (this.hasAttribute("name")) {
      // 如果存在 name 属性,读取 name 属性的值
      userName = this.getAttribute("name");
    }
    // 设置 div 标签的文本内容
    $box.innerText = `Hello ${userName}`;

    // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
    const shadow = this.attachShadow({ mode: "open" });
    shadow.appendChild($box);
  }
  connectedCallback() {
    console.log('创建元素')
    // 5s 后移动元素到 iframe
    setTimeout(() => {
      const iframe = document.getElementsByTagName("iframe")[0]
      iframe.contentWindow.document.adoptNode(this)
    }, 5e3)
  }
  disconnectedCallback() {
    console.log('删除元素')
  }
  adoptedCallback() {
    console.log('移动元素')
  }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- 页面插入一个 iframe,将自定义元素移入其中 -->
<iframe width="0" height="0"></iframe>
<hello-user name="Shenfq"></hello-user>

在元素被创建后,等待 5s,然后将自定义元素移动到 iframe 文档中,这时候能看到控制台会同时出现 删除元素移动元素 的 log。

Console

Shadow DOM(影子DOM)

在前面介绍自定义元素的时候,已经用到了 Shadow DOM。Shadow DOM 的作用是让内部的元素与外部隔离,让自定义元素的结构、样式、行为不受到外部的影响。

我们可以看到前面定义的 <hello-user> 标签,在控制台的 Elements 内,会显示一个 shadow-root ,表明内部是一个 Shadow DOM。

Shadow DOM

其实 Web Components 没有提出之前,浏览器内部就有使用 Shadow DOM 进行一些内部元素的封装,例如 <video> 标签。我们需要现在控制台的配置中,打开 Show user agent ashdow DOM 开关。

设置

然后在控制台的 Elements 内,就能看到 <video> 标签内其实也有一个 shadow-root

video 标签

创建 Shadow DOM

我们可以在任意一个节点内部创建一个 Shadow DOM,在获取元素实例后,调用 Element.attachShadow() 方法,就能将一个新的 shadow-root 附加到该元素上。

该方法接受一个对象,且只有一个 mode 属性,值为 openclosed,表示 Shadow DOM 内的节点是否能被外部获取。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<div id="root"></div>
<script>
  // 获取页面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '创建一个 shadow 节点';
  const shadow = $root.attachShadow({mode: 'open'});
  shadow.appendChild($p);
</script>

Shadow DOM

mode 的差异

前面提到了 mode 值为 openclosed,主要差异就是是否可以使用 Element.shadowRoot 获取到 shadow-root 进行一些操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<div id="root"></div>
<script>
  // 获取页面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '创建一个 shadow 节点';
  const shadow = $root.attachShadow({mode: 'open'});
  shadow.appendChild($p);
  console.log('is open', $div.shadowRoot);
</script>

open mode

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<div id="root"></div>
<script>
  // 获取页面的
  const $root = document.getElementById('root');
  const $p = document.createElement('p');
  $p.innerText = '创建一个 shadow 节点';
  const shadow = $root.attachShadow({mode: 'closed'});
  shadow.appendChild($p);
  console.log('is closed', $div.shadowRoot);
</script>

closed mode

HTML templates(HTML模板)

前面的案例中,有个很明显的缺陷,那就是操作 DOM 还是得使用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率明显更低,为了解决这个问题,在 HTML 规范中引入了 <tempate><slot> 标签。

使用模板

模板简单来说就是一个普通的 HTML 标签,可以理解成一个 div,只是这个元素内的所以内容不会展示到界面上。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<template id="helloUserTpl">
  <p class="name">Name</p>
  <a target="blank" class="blog">##</a>
</template>

在 JS 中,我们可以直接通过 DOM API 获取到该模板的实例,获取到实例后,一般不能直接对模板内的元素进行修改,要调用 tpl.content.cloneNode 进行一次拷贝,因为页面上的模板并不是一次性的,可能其他的组件也要引用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 通过 ID 获取标签
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);

我们在获取到拷贝的模板后,就能对模板进行一些操作,然后再插入到 Shadow DOM 中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<hello-user name="Shenfq" blog="http://blog.shenfq.com" />

<script>
  class HelloUser extends HTMLElement {
    constructor() {
      // 必须调用 super 方法
      super();

      // 通过 ID 获取标签
      const tplElem = document.getElementById('helloUserTpl');
      const content = tplElem.content.cloneNode(true);

      if (this.hasAttribute('name')) {
        const $name = content.querySelector('.name');
        $name.innerText = this.getAttribute('name');
      }
      if (this.hasAttribute('blog')) {
        const $blog = content.querySelector('.blog');
        $blog.innerText = this.getAttribute('blog');
        $blog.setAttribute('href', this.getAttribute('blog'));
      }
      // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
      const shadow = this.attachShadow({ mode: "closed" });
      shadow.appendChild(content);
    }
  }

  // 定义一个名为 <hello-user /> 的元素
  customElements.define("hello-user", HelloUser);
</script>
添加样式

<template> 标签中可以直接插入 <style> 标签在,模板内部定义样式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<template id="helloUserTpl">
  <style>
    :host {
      display: flex;
      flex-direction: column;
      width: 200px;
      padding: 20px;
      background-color: #D4D4D4;
      border-radius: 3px;
    }

    .name {
      font-size: 20px;
      font-weight: 600;
      line-height: 1;
      margin: 0;
      margin-bottom: 5px;
    }

    .email {
      font-size: 12px;
      line-height: 1;
      margin: 0;
      margin-bottom: 15px;
    }
  </style>
  <p class="name">User Name</p>
  <a target="blank" class="blog">##</a>
</template>

其中 :host 伪类用来定义 shadow-root的样式,也就是包裹这个模板的标签的样式。

占位元素

占位元素就是在模板中的某个位置先占据一个位置,然后在元素插入到界面上的时候,在指定这个位置应该显示什么。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<template id="helloUserTpl">
  <p class="name">User Name</p>
  <a target="blank" class="blog">##</a>
  <!--占位符-->
  <slot name="desc"></slot> 
</template>

<hello-user name="Shenfq" blog="http://blog.shenfq.com">
  <p slot="desc">欢迎关注公众号:更了不起的前端</p>
</hello-user>

这里用的用法与 Vue 的 slot 用法一致,不做过多的介绍。

总结

到这里 Web Components 的基本用法就介绍得差不多了,相比于其他的支持组件化方案的框架,使用 Web Components 有如下的优点:

  • 浏览器原生支持,不需要引入额外的第三方库;
  • 真正的内部私有化的 CSS,不会产生样式的冲突;
  • 无需经过编译操作,即可实现的组件化方案,且与外部 DOM 隔离;

Web Components 的主要缺点就是标准可能还不太稳定,例如文章中没有提到的模板的模块化方案,就已经被废除,现在还没有正式的方案引入模板文件。而且原生的 API 虽然能用,但是就是不好用,要不然也不会出现 jQuery 这样的库来操作 DOM。好在现在也有很多基于 Web Components 实现的框架,后面还会开篇文章专门讲一讲使用 Web Components 的框架 lit-htmllit-element

好啦,今天的文章就到这里了,希望大家能有所收获。

公众号:前端食堂

知乎:童欧巴

掘金:童欧巴

这是一个终身学习的男人,他在坚持自己热爱的事情,欢迎你加入前端食堂,和这个男人一起开心的变胖~

“如果你觉得读了本文有收获的话可以点个在看让我看到。阅读过程中有任何问题、想法或者感触也欢迎你在下方留言,也可以在后台回复加群进入食堂的交流群。 沟通创造价值,分享带来快乐。也欢迎你分享给身边有需要的同学,利他就是最好的利己。 ”

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端食堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Go 并发编程之 Mutex
友情提示:此篇文章大约需要阅读 18分钟0秒,不足之处请多指教,感谢你的阅读。 订阅本站
Meng小羽
2020/11/23
6300
Go 并发编程之 Mutex
通俗易懂!图解Go协程原理及实战
导语 | 本文主要介绍一下线程、协程的原理,以及写成的基本使用,希望能对此方面感兴趣的开发者提供一些经验和启发。 引言 Golang的语法和运行时直接内置了对并发的支持。Golang里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为goroutine时,Golang会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。 Golang运行时的调度器是一个复杂的软件,能管理被创建的所有goroutine并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时
腾讯云开发者
2022/07/27
1.3K0
通俗易懂!图解Go协程原理及实战
Go 语言互斥锁
在并发编程中,互斥锁(Mutex,全称 Mutual Exclusion)是一个重要的同步原语,用于确保多个线程或进程在访问共享资源时不会发生竞态条件。竞态条件是指在多个线程同时访问或修改共享数据时,由于操作顺序的不确定性,导致数据不一致或者程序行为不可预测的问题。
FunTester
2025/02/19
940
Go 语言互斥锁
Go 精妙的互斥锁设计
在并发编程中,互斥锁(Mutex)是控制并发访问共享资源的重要工具。Go 语言的互斥锁设计以其简洁、高效和易用性著称。本文将详细介绍 Go 语言中的互斥锁设计,探讨其内部实现原理,并展示如何在实际项目中正确使用互斥锁。
Michel_Rolle
2024/06/30
2.7K0
Golang并发编程控制
重学编程之Golang的plan中的上一篇文章我向大家介绍了,并发编程基础,goroutine的创建,channel,正由于go语言的简洁性,我们可以简易快速的创建任意个协程。同时也留下了许多隐患,如果没有更加深入的学习,其实很难直接将其运用到实际项目中,实际生活中。为什么呢?并发的场景许许多多,但一味的只知道其创建,是很难有效的解决问题。例如以下场景-资源竞争
PayneWu
2020/12/18
5740
Golang并发编程控制
Go并发编程之美-互斥锁
go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。相比Java来说go提供了独特的基于通道的同步措施。本节我们先来看看go中互斥锁
加多
2019/02/15
3630
Java程序员学习Go指南(三)
转载:https://www.luozhiyun.com/archives/213
luozhiyun
2020/02/18
3030
Java程序员学习Go指南(三)
[Go] golang互斥锁mutex
1.互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码 2.Lock()和Unlock()定义临界区
唯一Chat
2019/09/10
9690
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
上一篇 《原生并发 goroutine channel 和 select 常见使用场景》 介绍了基于 CSP 模型的并发方式。
张拭心 shixinzhang
2022/05/10
4100
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
避坑:Go并发编程时,如何避免发生竞态条件和数据竞争
现在,我们已经知道了。在编写并发程序时,如果不谨慎,没有考虑清楚共享资源的访问方式和同步机制,那么就会发生竞态条件和数据竞争这些问题,那么如何避免踩坑?避免发生竞态条件和数据竞争的办法有哪些?请看下面:
不背锅运维
2023/04/25
1K0
避坑:Go并发编程时,如何避免发生竞态条件和数据竞争
Go语言入门(八)线程安全&锁
线程安全&锁 定时器&一次性定时器 定时器 func main() { ticker := time.NewTicker(time.Second) //ticker.C是一个只读的chan,所以直接可以使用for range读取 for v := range ticker.C { fmt.Printf("hello %v\n",v) //按秒输出 } } 一次性定时器 func main() { select { case <- time.After(ti
alexhuiwang
2020/09/24
3910
Go:深入理解互斥锁,实现与应用
在并发编程中,互斥锁是一种基本的同步机制,用于保护共享资源不被多个线程或进程同时访问,从而避免数据竞争和保证数据的一致性。本文将深入探讨互斥锁的概念、工作原理,并通过Go语言的具体实现来展示互斥锁在实际编程中的应用。
运维开发王义杰
2024/05/10
2570
Go:深入理解互斥锁,实现与应用
盘点Golang并发那些事儿之二
上一节提到,golang中直接使用关键字go创建goroutine,无法满足我们的需求。主要问题如下
PayneWu
2021/06/10
5100
盘点Golang并发那些事儿之二
GO的锁和原子操作分享
要是对协程的使用感兴趣的话,可以看看这篇文章简单了解一下瞅一眼就会使用GO的并发编程分享
阿兵云原生
2023/02/16
3280
go的并发小知识
从第3点钟的操作状态表中可以看到,我们有四种操作会导致goroutine阻塞,三种操作会导致程序panic!因此,为了尽可能转移这些风险,我们需要分配channel的所有权。即,channel的所有者做实例化、写入和关闭操作;channel的使用者做读取操作,且约束其他人无法对其做相应的操作。一个优雅的实现:
天地一小儒
2022/12/28
2290
go的并发小知识
Go 专栏|并发编程:goroutine,channel 和 sync
原文链接: Go 专栏|并发编程:goroutine,channel 和 sync
AlwaysBeta
2021/09/16
6700
Go 专栏|并发编程:goroutine,channel 和 sync
Go语言核心36讲(Go语言实战与应用九)--学习笔记
我们在前几次讲的互斥锁、条件变量和原子操作都是最基本重要的同步工具。在 Go 语言中,除了通道之外,它们也算是最为常用的并发安全工具了。
郑子铭
2021/11/21
2230
Go语言核心36讲(Go语言实战与应用九)--学习笔记
协程锁
我们对一个变量total 进行1000次 +1 操作,不过我们是在多个协程中进行的,猜猜结果如何,我们运行五次看结果
酷走天涯
2019/06/11
5780
协程锁
《GO IN ACTION》读后记录:GO的并发与并行
一、使用goroutine来运行程序 1. Go的并发与并行 Go的并发能力,是指让某个函数独立于其他函数运行的能力。当为一个函数创建goroutine时,该函数将作为一个独立的工作单元,被 调度器 调度到可用的逻辑处理器上执行。Go的运行时调度器是个复杂的软件,它做的工作大致是: 管理被创建的所有goroutine,为其分配执行时间 将操作系统线程与语言运行时的逻辑处理器绑定 参考The Go scheduler ,这里较浅显地说一下Go的运行时调度器。操作系统会在物理处理器上调度操作系统线程来运行,而G
李海彬
2018/03/28
9880
《GO IN ACTION》读后记录:GO的并发与并行
浅谈Go并发原语
在操作系统中,往往设计一些完成特定功能的、不可中断的过程,这些不可中断的过程称为原语。
闫同学
2024/02/12
4010
相关推荐
Go 并发编程之 Mutex
更多 >
LV.3
腾讯专家工程师
目录
  • 什么是 Web Components?
  • 如何使用 Web Components?
    • Custom elements(自定义元素)
      • 创建一个新的 HTML 标签
      • 扩展已有的 HTML 标签
      • 生命周期
    • Shadow DOM(影子DOM)
      • 创建 Shadow DOM
      • mode 的差异
    • HTML templates(HTML模板)
      • 使用模板
      • 添加样式
      • 占位元素
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档