Web 组件概览与自定义元素
Web 组件由哪几项浏览器规范组成?
- 出发点: 把 UI 拆成自治的可视组件, 每个组件有自己的位置、职责与交互边界;
- 设计原则: 不要写出复杂软件, 复杂就拆成简单部分再以最明显的方式连接;
- 组件要素: 自己的 JS 类; 只由该类管理的 DOM 结构 (外部代码不访问); 作用于组件的 CSS; 对外 API (事件、方法等);
- Custom elements: 定义自定义 HTML 标签;
- Shadow DOM: 组件内部 DOM, 对外隐藏;
- CSS Scoping: 只作用于组件影子树的样式规则;
- Event retargeting: 让外部看到的事件像是发生在宿主元素上, 见 Shadow DOM 事件与组件模式;
- 现状: 自定义元素支持最好、可独立使用; 其余规范仍在演进, Chrome 跟进最快;
自定义元素的名字有什么硬性规则?
- 必须含连字符:
my-element、super-button合法,myelement非法; - 原因: 保证自定义标签与内置 HTML 标签永不重名;
- autonomous: 继承
HTMLElement的全新标签, 如<time-formatted>; - customized built-in: 继承
HTMLButtonElement等内置类, 复用原生语义与行为;
如何定义一个自定义元素并注册?
class MyElement extends HTMLElement {
/* 生命周期回调, 全部可选 */
}
customElements.define("my-element", MyElement);
document.createElement("my-element"); // JS 中同样可创建
- 效果: 页面上每个
<my-element>都成为MyElement的实例, 并触发相应回调; - 注册前就存在的同名标签不是错误, 只是"未定义", 见下文升级;
生命周期回调在什么时候触发?
| 回调 | 触发时机 |
|---|---|
constructor() | 实例创建; 此时属性还没赋值 |
connectedCallback() | 元素被插入文档; 反复增删会反复调用 |
disconnectedCallback() | 元素从文档移除; 同样可多次调用 |
static observedAttributes | 返回需要监听的属性名数组 |
attributeChangedCallback(name, oldValue, newValue) | 被监听的属性发生变化 |
adoptedCallback() | 元素被移入新文档 (document.adoptNode), 极少用 |
- 可选性: 只实现真正需要的回调即可;
未定义的元素如何升级?
- 选择器: 未定义元素可用
:not(:defined)选中, 定义后变成:defined; - 升级: 调用
customElements.define时, 已存在的同名元素各自创建实例并触发connectedCallback; customElements.get(name): 返回该名字对应的类;customElements.whenDefined(name): 返回一个在元素被定义后兑现 (无值) 的 Promise;
自定义内置元素如何扩展原生标签?
- 继承:
class HelloButton extends HTMLButtonElement { /* ... */ }; - 注册: 需要第三个参数指明扩展的标签, 因为多个标签可能共用一个 DOM 类;
- 使用: HTML 里写普通标签再加
is属性;
customElements.define("hello-button", HelloButton, { extends: "button" });
<button is="hello-button">Click me</button>
- 收益: 保留原生样式、
disabled等既有行为, 搜索引擎与无障碍设备能识别其语义;
为什么渲染要放在 connectedCallback 而不是 constructor?
- constructor 太早: 浏览器还没处理属性,
getAttribute返回null, 无法渲染; - 性能: 把工作推迟到真正需要时;
- 时机语义:
connectedCallback在元素真正成为页面一部分时触发, 不只是被append到某个游离元素下; - 收益: 可以先离线构建 DOM、准备好元素, 等它进入页面才渲染;
属性变化如何触发重新渲染?
- 声明: 在
observedAttributes中列出要监听的属性名; - 回调: 这些属性被修改时触发
attributeChangedCallback, 在其中重新渲染; - 性能: 未列出的属性不触发回调;
static get observedAttributes() { return ["datetime", "hour", "minute"]; }
attributeChangedCallback() { this.render(); }
解析顺序对 connectedCallback 中读取子节点有什么影响?
- 顺序: HTML 解析器先父后子, 父元素
connectedCallback触发时子节点尚未创建; - 现象: 此时读
this.innerHTML得到空字符串; - 传参: 优先用属性, 属性在元素创建时就可用;
- 延迟方案: 用零延迟
setTimeout等解析完成后再读, 但嵌套自定义元素会按外→内排队, 外层先完成初始化; - 现成回调: 没有"子元素就绪"回调; 需要时由内层派发
initialized事件、外层监听响应;