Web Components 基础入门-Web Components概念详解(上)

2,726 阅读9分钟

Web Components 相关概念

Web Components 得到越来越多的关注。而且微软的 Edge 开发团队在 2019 年就宣布,在 Edge 上开始支持 Custom ElementsShadow Dom,所有的主流浏览器已经开始支持 Web Components

另外,国外一些公司,像 Github、 Netflix、 YoutubeING 这样的公司甚至已经在生产环境中使用 Web Components

image.png

🙋 What are web components?

Web components 是一套标准,允许开发者创建新的自定义、可重用、封装的 HTML 元素,以便在 Web 页面和 Web 
应用程序中使用。
自定义组件和小部件建立在 Web components 标准之上,可以跨现代浏览器工作,并且可以与任何 JavaScript 库
或框架一起使用。

Web components 基于现有的 Web 标准。支持 Web components 的特性目前正被添加到 HTMLDOM 规范中,

让 web 开发人员可以通过封装样式和自定义行为轻松扩展 HTML。
来源:https://www.webcomponents.org/introduction

Web Components 是一套不同的技术组成,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)
并且在您的web应用中使用它们
来源: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

✨ 划重点

首先: Web components 是一套标准,有不同的技术组成,允许我们编写模块化、可重用和封装的 HTML 元素。

它最大的优点: 由于它们是基于 web 标准的,我们不需要安装任何框架或库就可以开始使用它们。也就是可以通过普通的 javascript 编写 web 组件了!

📂 使用

作为开发者,我们都想要尽可能多的重用代码。这对于自定义UI 控件来说通常不是那么容易 — 想想复杂的 HTML(以及相关的样式和脚本),有时您不得不写很多代码来呈现自定义 UI 控件,在不依赖任何框架的情况下,想要复用自定义的UI控件只能不断的粘贴复制相关代码。

Web Components 旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装特定功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码或者样式冲突。

三项主要技术简单介绍:

1.Custom elements(自定义元素)

一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。简单的解释就是,用户自定义HTML元素。它们通过使用CustomElementRegistry来注册一个新的元素,通过window.customElements中一个叫define的方法来获取注册的实例

 window.customElements.define('my-element', MyElement);

方法中的第一个参数定义了新创造元素的标签名字,我们可以非常简单的直接使用

<my-element></my-element>

为了避免和native标签冲突,所以规定自定义标签强制使用中划线来连接。

2.Shadow DOM(影子 DOM)

一组 JavaScript API,用于将封装的"影子"DOM 树附加到元素中(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有化,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

Web components 的一个重要属性是封装——可以将标记结构样式行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。

使用 Shadow DOM,自定义元素的 HTMLCSS 完全封装在组件内。这意味着元素将以单个的 HTML 标签出现在文档的 DOM 树种。其内部的结构将会放在#shadow-rootimage

Shadow DOM 使得HTMLCSS主文档DOM保持分离。

备注: Firefox(从版本 63 开始),Chrome,Opera 和 Safari 默认支持 Shadow DOM。基于 Chromium 的新 Edge 也支持 Shadow DOM;而旧 Edge 未能撑到支持此特性。

为什么要把一些代码和网页上其他的代码分离?

原因之一是,大型站点若 CSS 没有良好的组织,导航的样式可能就『泄露』到本不应该去的地方,如主要内容区域,反之亦然。随着站点、应用的拓展,这样的事难以避免。

重新认识

实际上一些原生的 HTML 元素也使用了 Shadow DOM。例如你再一个网页中有一个<video>元素,它将会作为一个单独的标签展示,但它也将显示播放和暂停视频的控件,当你在浏览器开发工具中查看 video 标签,是看不到这些控件。

这些控件实际上就是 video 元素的 Shadow DOM 的一部分,因此默认情况下是隐藏的。要在 Chrome 中显示 Shadow DOM,进入开发者工具中的 Preferences 中,选中 Show user agent Shadow DOM。当你在开发者工具中再次查看 video 元素时,你就可以看到该元素的 Shadow DOM 了。

详细的操作步骤如下:

image

image

image

注意:

不管从哪个方面来看,Shadow DOM 都不是一个新事物——在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 <video> 元素为例。如上图所示,你所能看到的只是一个 <video> 标签,实际上,在它的 Shadow DOM 中,包含来一系列的按钮和其他控制器。Shadow DOM 标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。

属性

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

这里,有一些 Shadow DOM 特有的术语需要我们了解:

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

基础用法

可以使用 Element.attachShadow() 方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed

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

open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot 属性:

let myShadowDom = myCustomElem.shadowRoot

如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode 设置为 closed,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot 将会返回 null。浏览器中的某些内置元素就是如此,例如<video>,包含了不可访问的 Shadow DOM

如果你想将一个 Shadow DOM 附加到 custom element 上,可以在 custom element 的构造函数中添加如下实现(目前,这是 shadow DOM 最实用的用法):

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

Shadow DOM 附加到一个元素之后,就可以使用 DOM APIs 对它进行操作,就和处理常规 DOM 一样。

var para = document.createElement('p')
shadow.appendChild(para)

3.HTML templates(HTML 模板)

<template><slot>元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

除了使用 this.shadowRoot.innerHTML 来向一个元素的 shadow root 添加 HTML,你也可以使用 <template>来做。template 保存 HTML 供以后使用。它不会被渲染,并只有确保内容是有效的才会进行解析。模板中的 JavaScript 不会被执行,也会获取任何外部资源,默认情况下它是隐藏的。

当一个 web component 需要根据不同的情况来渲染不同的标记时,可以用不同的模板来完成

wheat-uibutton组件 中 因为button组件会根据不同的属性渲染不同的标签

// 同时创建了两个标签
const template = document.createElement('template')
const templateTagA = document.createElement('template')
template.innerHTML = `
  <div class="wheat-button-container">
    <button class='wheat-button'>
      <slot name='icon'/>
      Label
    </button>
  </div>
`
templateTagA.innerHTML = `
  <div class="wheat-button-container">
    <a class='wheat-button'><slot name='icon'/>链接</a>
  </div>
`
// 在渲染的时候,会根据href熟悉去渲染不同的template

this._shadowRoot = this.attachShadow({ mode: 'open' })
this._shadowRoot.appendChild(
  this.href ? templateTagA.content.cloneNode(true) : template.content.cloneNode(true),
)

cloneNode

Node.cloneNode() 方法返回调用该方法的节点的一个副本.

语法:var dupNode = node.cloneNode(deep);
  • node 将要被克隆的节点

  • dupNode 克隆生成的副本节点

  • deep 可选 是否采用深度克隆,如果为 true,则该节点的所有后代节点也都会被克隆,如果为 false,则只克隆该节点本身.

克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如 onclick="alert(1)"),但不会拷贝那些使用 addEventListener()方法或者 node.onclick = fn 这种用 JavaScript 动态绑定的事件.

在使用 Node.appendChild()或其他类似的方法将拷贝的节点添加到文档中之前,那个拷贝节点并不属于当前文档树的一部分,也就是说,它没有父节点.

如果 deep 参数设为 false,则不克隆它的任何子节点.该节点所包含的所有文本也不会被克隆,因为文本本身也是一个或多个的 Text 节点.

如果 deep 参数设为 true,则会复制整棵 DOM 子树(包括那些可能存在的 Text 子节点).对于空结点(例如<img><input>元素),则 deep 参数无论设为 true 还是设为 false,都没有关系,但是仍然需要为它指定一个值.

关于使用cloneNode的目的是;获取<template>节点以后,克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。

添加样式

自定义元素还没有样式,可以给它指定全局样式,比如下面这样。

wheat-button {
  /* ... */
}

但是,组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在<template>里面。在wheat-uibutton组件;

// 设置样式
const style = `
.wheat-button {
  /* ... */
}
`
// 在template中使用
const template = document.createElement('template')
const templateTagA = document.createElement('template')
template.innerHTML = `
    ${style} // 重点是这一行
  <div class="wheat-button-container">
    <button class='wheat-button'>
      <slot name='icon'/>
      Label
    </button>
  </div>
`
templateTagA.innerHTML = `
  <div class="wheat-button-container">
    <a class='wheat-button'><slot name='icon'/>链接</a>
  </div>
`

就像之前的 JQ 一样,当 document.querySelector 开始得到浏览器的广泛支持后,使得 JQ 的使用频率开始变低。这样的事也有可能发生在 Angular 、React、Vue 这些框架上