DOM – Web Components
前言
Web Components 已经听过很多年了, 但在开发中用纯 DOM 来实现还是比较少见的. 通常我们是配搭 Angular, React, Vue, Lit 来使用.
这篇就来讲讲纯 Web Components 长什么样子吧. Lit 和 Angular 的实现是尽可能依据规范的哦.
参考
YouTube – Web Components Crash Course
Web Components 基础入门-Web Components概念详解(上)
介绍
Web Components 其实是一个大概念, 它又可以被分为几个部分. 把几个部分拼凑一起才完整了 Web Components.
这些小概念分别是: Custom Elements, Shadow DOM 和 HTML Templates. 我们先把它们挨个挨个分开看. (它们单独也可以 working 的哦)
HTML Templates
它最简单, 所以先介绍它, 以前我们做动态内容是直接写 HTML Raw 的. 但这个方法对管理非常不友好.

现在都改用 Template 来管理了.
定义 template
<body>
<template>
<h1>dynamic title here...</h1>
<p>dynamic content here...</p>
</template>
</body>
template 是不会渲染出来的. 它只是一个模型.
使用 template
// step 1 : 获取 template
const template = document.querySelector<HTMLTemplateElement>("template")!; // step 2 : clone and binding data
const templateContent = template.content.cloneNode(true) as DocumentFragment;
templateContent.querySelector("h1")!.textContent = "Hello World 1";
templateContent.querySelector("p")!.textContent =
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusantium, laborum."; // step 3 : append to target
document.body.appendChild(templateContent);
记得要先 clone 了才能使用
Convert DocumentFragment to Raw String
template.content 是 DocumentFragment 来的, 有时候会需要把它转成 Raw HTML (比如我在 Google Map JavaScript API Info Window 的时候)
参考: Converting Range or DocumentFragment to string
有 2 招.
1. 创建一个 div 把 frag 丢进去, 然后 .innerHTML 取出来.
const div = document.createElement("div");
div.appendChild(templateContent);
console.log("rawHtml", div.innerHTML);
2. 用 XMLSerializer
const serializer = new XMLSerializer();
const rawHtml = serializer.serializeToString(templateContent);
console.log("rawHtml", rawHtml);
个人感觉, 第一招会比较安全一些. 毕竟 XML 和 HTML 还是有区别的. 未必 XMLSerializer 能处理好 HTML (只是一个猜想而已)
Shadow DOM
参考:
Shadow DOM 主要的功用是隔离 CSS. 任何一个 element 都可以开启一个 Shadow DOM 区域
在没有 Shadow DOM 的情况下, CSS Style 是全局互相影响的
<style>
h1 {
color: red;
}
</style>
<h1>Outside Text</h1>
<div class="container">
<style>
h1 {
color: blue;
}
</style>
<h1>Inside Text</h1>
</div>
最终 outside 和 inside text 都是蓝色. 因为 container 里面的 style 会覆盖全局的 style.
而使用 Shadow DOM 就可以隔离它们, 有点像 iframe 的效果.(外面影响不了里面, 里面也影响不了外面, 相互独立)
Setup Shadow DOM
Shadow DOM 必须使用 JS 来设定, 同样上面的例子
const container = document.querySelector<HTMLElement>('.container')!;
container.attachShadow({ mode: 'open' }); // 开启 Shadow DOM
// 创建内容
// <style>
// h1 {
// color: blue;
// }
// </style>
// <h1>Inside Text</h1>
const style = document.createElement('style');
style.textContent = `h1 { color: blue }`;
const h1 = document.createElement('h1');
h1.textContent = 'Inside Text';
// append 到 Shadow DOM 里面
container.shadowRoot!.appendChild(style);
container.shadowRoot!.appendChild(h1);
效果

inside text 的 style 没有覆盖全局的 style (同时 outside style 也无法影响 inside 的 element 哦)
用 DevTools 查看会发现多了 Shadow DOM

:host selector
虽说里面 CSS 影响不到外面, 但是它最多还是可以控制到 Shadow DOM element 的.
shadowRoot.innerHTML = `
<style>
:host { background: red; padding: 2px 5px; }
</style>
`;
这样 .container element 的 background 就变成 red 了 (最多只能到 host element, 在外面就不可能影响的到了)
:host-context() selector
:host-content() 是让 Shadow DOM 内部 preset 一些 style for 外部控制. 比如
:host-context(.active) {
h1 {
background-color: green;
}
}
上面的意思是当 host 或者 host 的 ancestor elements 任何一个包含 active class 的时候, 内部的 h1 会有绿色背景.
所以, 外部有能力通过 add class 等方式来控制内部的 style. but 前提是内部提前 preset 了这些逻辑. 如果外部想直接修改内部 style, 那还需要其它方法, 下面会介绍.
注:Firefox 和 Safari 都不支持 :host-context() 哦。
CSS variables pass through Shadow DOM
:host-context() 支持度不理想,替代方案是使用 CSS variables。
Shadow DOM 里面是可以拿到外面定义的 CSS variables 的,但反过来就不行哦。
相关提问:Stack Overflow – :host-context not working as expected in Lit-Element web component
mode: 'closed'
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
console.log(shadowRoot === this.shadowRoot); // true when open, false when closed
}
在调用 attachShodow 以后, 方法会返回 shadowRoot 对象.
如果是 open 那么这个 shadowRoot 之后可以通过 element.shadowRoot 访问. 算是一个公开的意思.
如果设置成 closed 那么 element.shadowRoot 将 != 返回的 shadowRoot.
之后要操作只能通过返回的 shadowRoot. element.shadowRoot 不可以使用.
closed 通常会在 Custom Element 中使用. 这样对外就是不公开的. 只有 element 内部保留了返回的 shadowRoot 并且可以使用它.
<video> 也是用了这个概念. 对外不开放.
<slot>
slot 只能在 Shadow DOM 下使用. 它一般上是用在 Custom Elements 上的 (但其实只要 Shadow DOM element 都可以用的)
它的主要功用就是传递 HTML 结构到 custom element 中. 例子说明
<div class="container">
<p>Lorem, ipsum dolor.</p>
</div>
container 将成为一个 Shadow DOM element. 它包裹的内容 (<p>) 将被 "转移" 到 Shadow DOM 内容的某个地方
const container = document.querySelector('.container')!;
const shadowRoot = container.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<h1>Hello World</h1>
<slot></slot>
<h1>Hello World End</h1>
`;
最终 container > p 会被转移到 Shadow DOM 的 <slot></slot> 这个位置里
效果

结构

multiple <slot>
上面的例子是传递一个 slot, 如果要传多个就需要加上命名
<div class="container">
<p slot="first">1. Lorem, ipsum dolor.</p>
<p slot="second">2. Lorem, ipsum dolor.</p>
</div>
JS
shadowRoot.innerHTML = `
<h1>Hello World</h1>
<slot name="first"></slot>
<h1>Hello World End</h1>
<slot name="second"></slot>
`;
效果

::slotted() selector
slot element 的 style 是跟外面跑的, 它不被 Shadow DOM 里面的 style 影响. 记得哦, 是外面负责 style.
如果想在 Shadow DOM 内部控制 slot element style 的话, 需要使用 ::slotted selector (改成里面负责 style)
<style>
h1 {
color: blue;
}
::slotted(p) {
color: blue !important;
}
</style>
之所以加上了 !important 是因为外面有定义了 style,需要 override。如果外部没有定义 style 那么里面就不需要 !important,但依然需要用 ::slotted 哦。
如果想指定其中一个 slot 可以这样写
slot[name='first']::slotted(p) {
color: blue !important;
}
::part() selector
::slotted 是让 Shadow DOM 内部控制 slot element style, ::part 则是反过来让外部控制 Shadow DOM 内的 element style
在 Shadow DOM 内给某个 element 加上 attribute part="name", 外部并不能随心所欲 set 任何 element 的 style, 只有 attribute part 的 element 才能被外部 style 哦

外部的 CSS Style
.container::part(h1) {
background-color: pink;
}
效果

Isolate querySelector
除了隔离 CSS, Shadow DOM 也隔离了 DOM query,
比如上面例子里, 通过
console.log(document.querySelectorAll('h1').length); // 1
我们只能获取到 1 个 h1. Shadow DOM 内的 h1 外部是无法 query 到的。
如果外部想 query Shadow DOM 内部,唯一的方法是透过 shadowRoot
document.querySelector('.container')!.shadowRoot!.querySelectorAll('h1');
但前提是 attachShadow 时 mode 一定要是 open。
const shadowRoot = container.attachShadow({ mode: 'open' });
另外 query ancestor 也是会被隔离掉
console.log(h1.closest('.container')); // null
Shadow DOM 里面是找不到外面 element。
唯一的方法是通过 parentNode + host 一层一层往上拿。
const shadowRoot = h1.parentNode as ShadowRoot;
const container = shadowRoot.host; // <div class="container"></div>
Query slotted elements
shadowRoot.querySelector 也无法获取到 slot 内的 elements (就是外部 transclude 进来的, 内部是 query 不到的)
但是可以通过 assignedElements 方法做到这一点
<div class="container">
<h1>Title</h1>
<p>Some description...</p>
</div>
container 是 shadow DOM, h1 和 p 是 transclude elements
const container = document.querySelector('.container')!;
const shadowRoot = container.attachShadow({ mode: 'closed' });
const div = document.createElement('div');
div.innerHTML = `
<p>I love you</p>
<slot></slot>
`;
shadowRoot.appendChild(div);
console.log(shadowRoot.querySelectorAll('p').length); // 1, query 不到 <slot> 内的 p
const slot = shadowRoot.querySelector('slot')!;
console.log(slot.assignedElements({ flatten: true })); // [h1, p], transclude elements
shadowRoot.querySelectorAll 拿不到 slot transclude 的 p。
可以通过 slot.assignedElements 获取到 transclude element h1 和 p。
flatten: true 的作用是 for 层中层 <slot>,比如 transclude 进来的是 parent 的 <slot> element。
assignedElements 只能拿到 element,如果有 Text / Comment 在最上层,那得使用 slot.assignedNodes。
Slotted elements parent 是谁?

请问 content 的 parent element 是谁?
答案是 my-parent,虽然最终显示的地方是 my-child 里,但是它是依据一开始 content 被定义在哪里,而不是最终它被放去哪里。

用 Chrome DevTools 会看的更明确。
Custom Elements
Custom Elements 算是 Web Components 的核心. Template 和 Shadow DOM 只是辅助它变得更好.
What is Element (or component)
先了解一下什么是 element.
element 在 HTML 中是这样的
<div id="my-id" class="my-class"></div>
一个 HTML(XML) 的表达, 用面向对象来表达的话, 它就是一个 class instance. id 和 class 是属性, class name 就是 div
class HTMLDivElement {
id!: string;
classList!: string;
}
游览器在解析 HTML 后会实例化 class 创建出对象, then 我们可以用 JS 去获取到这个对象, 然后修改它的属性, 或者调用方法.
const el = document.querySelector<HTMLDivElement>('#my-id')!;
console.log(el.id);
console.log(el.classList);
而 class 内部就会去调整 element 内部结构.
记住: 它用 HTML 去表达 -> 由游览器去创建对象 -> 通过操作对象去改变 element 结构 (它的玩法就是这样)
Input Element
div 太简单, 我们拿 input 为例子
<input placeholder="Name" type="text" />
效果

世间万物都可以用 div + CSS + JS event 来实现.
如果我们想做一个和原生 input 一摸一样的 UI/UX design 是完全可以做到的.
抽象看 input 也是 HTML 声明 -> 游览器创建对象 -> JS 操作对象.
const input = document.querySelector<HTMLInputElement>('input')!;
input.checkValidity(); // 方法
console.log(input.validationMessage); // 属性
Custom Element
游览器可以搞 input, video 这些多交互的 element / component, 我们自然也可以搞.
以前 div + CSS + JS 就可以做出很多东西了. 只是它们无法封装起来. 而 Custom Element 给了我们一种 "封装" 的能力.
游览器如何封装 input, 我们就如何封装 my-input, 就这么简单.
Define Custom Element
第一步声明 HTML, 有 tag 和 attribute
<say-hi name="Derrick"></say-hi>
第二步是定义 class
class SayHiElement extends HTMLElement {
constructor() {
super();
this.name = this.getAttribute('name')!;
this.render();
}
name: string;
setName(newName: string) {
this.name = newName;
this.render();
}
render(): void {
this.innerHTML = `<h1>Hi, ${this.name}</h1>`;
}
}
这个 class 里面有几个重点
1. attribute handle
HTML 是声明式的, 它只提供表达. 如何把 attribute 变成 property 和内部 element 结构, 那是 class 内部负责的逻辑.
2. property 读写. Element 必须可以通过修改 property 来到达修改内部 element 结构的效果.
3. render. Custom element 最终依然是要输出 native element 的, render 方法可以用 innerHTML, createElement + appendChild, 或者 HTML Template 来实现.
4. 生命周期
class SayHiElement extends HTMLElement {
connectedCallback() {
console.log('connected'); // 当被 append to document
}
disconnectedCallback() {
console.log('disconnected'); // 当被 remove from document
}
attributeChangedCallback(attributeName: string, oldValue: string, newValue: string) {
console.log([attributeName, oldValue, newValue]); // 当监听的 attributes add, remove, change value 的时候触发
}
static get observedAttributes() {
return ['name']; // 声明要监听的 attributes
}
}
从上面几个点可以感受到一个 custom element / component 它是如果 working 的.
Register Custom Element
定义好 class 之后, 接着需要告知游览器, 让 element HTML tag 对应上这个 class
window.customElements.define('say-hi', SayHiElement);
const sayHiElement = document.querySelector<SayHiElement>('say-hi')!;
sayHiElement.setName('keatkeat');
到这里, custom element 就可以解析成功了.
Web Components 3 in 1 Custom Elements + Shadow DOM + Template
这里给一个完整的例子.

一个计数器, 用 HTML + CSS + JS 实现是这样的

<div class="container">
<style>
.counter {
display: flex;
gap: 16px;
}
.counter :is(.minus, .plus) {
width: 64px;
height: 64px;
}
.counter .number {
width: 128px;
height: 64px;
border: 1px solid gray;
font-size: 36px;
display: grid;
place-items: center;
}
</style>
<div class="counter">
<button class="minus">-</button>
<span class="number">0</span>
<button class="plus">+</button>
</div>
<script>
const number = document.querySelector('.counter .number');
const minus = document.querySelector('.counter .minus');
const plus = document.querySelector('.counter .plus');
minus.addEventListener('click', () => {
number.textContent = +number.textContent - 1;
});
plus.addEventListener('click', () => {
number.textContent = +number.textContent + 1;
});
</script>
</div>
index.html
声明 counter-component
<div class="container">
<counter-component></counter-component>
</div>
counter-component.html
负责 template & style
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<template id="counter-component-template">
<style>
.counter {
display: flex;
gap: 16px;
}
.counter :is(.minus, .plus) {
width: 64px;
height: 64px;
}
.counter .number {
width: 128px;
height: 64px;
border: 1px solid gray;
font-size: 36px;
display: grid;
place-items: center;
}
</style>
<div class="counter">
<button class="minus">-</button>
<span class="number">0</span>
<button class="plus">+</button>
</div>
</template>
</body>
</html>
index.ts
class HTMLCounterElement extends HTMLElement {
private _shadowRoot: ShadowRoot;
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'closed' });
}
async connectedCallback() {
const response = await fetch('/counter-component.html', {
headers: {
Accept: 'text/html; charset=UTF-8',
},
});
const rawHtml = await response.text();
const domParser = new DOMParser();
const templateDocument = domParser.parseFromString(rawHtml, 'text/html');
const template = templateDocument.querySelector<HTMLTemplateElement>(
'#counter-component-template'
)!;
const templateContent = template.content.cloneNode(true) as DocumentFragment;
this.bindingEvent(templateContent);
this._shadowRoot.appendChild(templateContent);
}
private bindingEvent(templateContent: DocumentFragment) {
const number = templateContent.querySelector('.counter .number')!;
const minus = templateContent.querySelector('.counter .minus')!;
const plus = templateContent.querySelector('.counter .plus')!;
minus.addEventListener('click', () => {
number.textContent = (+number.textContent! - 1).toString();
});
plus.addEventListener('click', () => {
number.textContent = (+number.textContent! + 1).toString();
});
}
}
window.customElements.define('counter-component', HTMLCounterElement);
1. 定义 Custom Element,
2. fetch 去拿 Template
3. 把 template 丢进 Shadow DOM
其它常用到的技术是
1. 初始化通过 attribute 传入变量
2. 初始化通过 slot 传入 element
3. 修改 property 达到调整结构和 style 的效果
4. 监听 component 触发的 event (包括许多 Custom Event)
DOM – Web Components的更多相关文章
- html fragment & html template & virtual DOM & web components
html fragment & html template & virtual DOM https://developer.mozilla.org/en-US/docs/Web/API ...
- 前端未来趋势之原生API:Web Components
声明:未经允许,不得转载. Web Components 现世很久了,所以你可能听说过,甚至学习过,非常了解了.但是没关系,可以再重温一下,温故知新. 浏览器原生能力越来越强. js 曾经的 JQue ...
- Web Components All In One
Web Components All In One Web Components https://www.webcomponents.org/ HTML Template Custom Element ...
- 【shadow dom入UI】web components思想如何应用于实际项目
回顾 经过昨天的优化处理([前端优化之拆分CSS]前端三剑客的分分合合),我们在UI一块做了几个关键动作: ① CSS入UI ② CSS作为组件的一个节点而存在,并且会被“格式化”,即选择器带id前缀 ...
- Web Components初探
本文来自 mweb.baidu.com 做最好的无线WEB研发团队 是随着 Web 应用不断丰富,过度分离的设计也会带来可重用性上的问题.于是各家显神通,各种 UI 组件工具库层出不穷,煞有八仙过海之 ...
- Web Components之Custom Elements
什么是Web Component? Web Components 包含了多种不同的技术.你可以把Web Components当做是用一系列的Web技术创建的.可重用的用户界面组件的统称.Web Com ...
- 【转】Facebook React 和 Web Components(Polymer)对比优势和劣势
原文转自:http://segmentfault.com/blog/nightire/1190000000753400 译者前言 这是一篇来自 StackOverflow 的问答,提问的人认为 Rea ...
- Polymer——Web Components的未来
什么是polymer? polymer由谷歌的Palm webOS团队打造,并在2013 Google I/O大会上推出,旨在实现Web Components,用最少的代码,解除框架间的限制的UI 框 ...
- The state of Web Components
Web Components have been on developers’ radars for quite some time now. They were first introduced b ...
- Web Components
Web Components是不是Web的未来 今天 ,Web 组件已经从本质上改变了HTML.初次接触时,它看起来像一个全新的技术.Web组件最初的目的是使开发人员拥有扩展浏览器标签的能力,可以 ...
随机推荐
- 4 安卓h5分享功能未实现
安卓h5点击分享没有复制链接到剪切板
- MySql创建事件、计划、定时运行
CREATE EVENT IF NOT EXISTS check_timeout_eventON SCHEDULE EVERY 30 MINUTEDOBEGIN UPDATE safetyApp_in ...
- [oeasy]python0073_进制转化_eval_evaluate_衡量_oct_octal_八进制
进制转化 回忆上次内容 上次了解的是 整型数字类变量 integer 前缀为i 添加图片注释,不超过 140 字(可选) 整型变量 和 字符串变量 不同 整型变量 是 直接存储二进制形 ...
- oeasy教您玩转vim - 16 跳到某行
跳到某行 回忆上节课内容 上下行 向 下 是 j 向 上 是 k 上下行首 向 下 到行首非空字符 + 向 上 到行首非空字符 - 这些 motion 都可以加上 [count] 来翻倍 首尾行 首行 ...
- JavaScript一天一个算法题~持续更新中。。。。。
1,数组去重 i.暴力去重 思路:建一个空数组,通过判断原数组的元素是否在空数组内,如果在,不放入,不在,放入空数组. function clearCommnetArray(array){ let a ...
- 【工具】SpringBoot项目如何查看某个maven依赖是否存在以及依赖链路
当我在SpringBoot项目中想加个依赖,但是不确定现有依赖的依赖的依赖.....有没有添加过这个依赖,怎么办呢?如果添加过了但是不知道我需要的这个依赖属于哪个依赖的下面,怎么查呢? IDEA中提供 ...
- model.train方法的dataset_sink_mode参数设置为False时以step作为单位打印数据——(只在mode=context.GRAPH_MODE下成立,在mode=context.PYNATIVE_MODE模式下不成立)
如题: 官方中的内容支持: https://www.mindspore.cn/tutorial/training/zh-CN/r1.2/advanced_use/summary_record.html ...
- 【转载】 numpy数据类型dtype转换
原文地址: https://www.cnblogs.com/hhh5460/p/5129032.html =============================================== ...
- 【转载】failed to open /dev/dri/renderd128 permission denied
原文地址: https://juejin.cn/s/failed%20to%20open%20%2Fdev%2Fdri%2Frenderd128%20permission%20denied ===== ...
- Analysis of Set Union Algorithms 题解
题意简述 有一个集合,初始为空,你需要写一个数据结构,支持: 0 x 表示将 \(x\) 加入该集合,其中 \(x\) 为一由 \(\texttt{0} \sim \texttt{9}\) 组成的数字 ...