DOM 操作
¥DOM Manipulation
HTMLElement
类型探索
¥An exploration into the HTMLElement
type
自标准化以来的 20 多年里,JavaScript 取得了长足的进步。虽然在 2020 年,JavaScript 可以用于服务器、数据科学甚至物联网设备,但重要的是要记住它最流行的用例:网络浏览器。
¥In the 20+ years since its standardization, JavaScript has come a very long way. While in 2020, JavaScript can be used on servers, in data science, and even on IoT devices, it is important to remember its most popular use case: web browsers.
网站由 HTML 和/或 XML 文档组成。这些文件是静态的,它们不会改变。文档对象模型 (DOM) 是浏览器实现的一种编程接口,可使静态网站正常运行。DOM API 可用于更改文档结构、样式和内容。API 非常强大,围绕它开发了无数前端框架(jQuery、React、Angular 等),使动态网站的开发变得更加容易。
¥Websites are made up of HTML and/or XML documents. These documents are static, they do not change. The Document Object Model (DOM) is a programming interface implemented by browsers to make static websites functional. The DOM API can be used to change the document structure, style, and content. The API is so powerful that countless frontend frameworks (jQuery, React, Angular, etc.) have been developed around it to make dynamic websites even easier to develop.
TypeScript 是 JavaScript 的类型化超集,它为 DOM API 提供了类型定义。这些定义在任何默认的 TypeScript 项目中都很容易获得。在 lib.dom.d.ts 的 20,000 多行定义中,有一个脱颖而出:HTMLElement
。这种类型是使用 TypeScript 进行 DOM 操作的主干。
¥TypeScript is a typed superset of JavaScript, and it ships type definitions for the DOM API. These definitions are readily available in any default TypeScript project. Of the 20,000+ lines of definitions in lib.dom.d.ts, one stands out among the rest: HTMLElement
. This type is the backbone for DOM manipulation with TypeScript.
你可以浏览 DOM 类型定义 的源代码
¥You can explore the source code for the DOM type definitions
基本示例
¥Basic Example
给定一个简化的 index.html 文件:
¥Given a simplified index.html file:
html
<!DOCTYPE html><html lang="en"><head><title>TypeScript Dom Manipulation</title></head><body><div id="app"></div><!-- Assume index.js is the compiled output of index.ts --><script src="index.js"></script></body></html>
让我们探索一个将 <p>Hello, World!</p>
元素添加到 #app
元素的 TypeScript 脚本。
¥Let’s explore a TypeScript script that adds a <p>Hello, World!</p>
element to the #app
element.
ts
// 1. Select the div element using the id propertyconst app = document.getElementById("app");// 2. Create a new <p></p> element programmaticallyconst p = document.createElement("p");// 3. Add the text contentp.textContent = "Hello, World!";// 4. Append the p element to the div elementapp?.appendChild(p);
编译并运行 index.html 页面后,生成的 HTML 将是:
¥After compiling and running the index.html page, the resulting HTML will be:
html
<div id="app"><p>Hello, World!</p></div>
Document
接口
¥The Document
Interface
TypeScript 代码的第一行使用了一个全局变量 document
。检查变量表明它是由 lib.dom.d.ts 文件中的 Document
接口定义的。代码片段包含对两个方法的调用,getElementById
和 createElement
。
¥The first line of the TypeScript code uses a global variable document
. Inspecting the variable shows it is defined by the Document
interface from the lib.dom.d.ts file. The code snippet contains calls to two methods, getElementById
and createElement
.
Document.getElementById
该方法的定义如下:
¥The definition for this method is as follows:
ts
getElementById(elementId: string): HTMLElement | null;
向它传递一个元素 ID 字符串,它将返回 HTMLElement
或 null
。该方法引入了最重要的类型之一,HTMLElement
。它作为所有其他元素接口的基础接口。例如,代码示例中的 p
变量是 HTMLParagraphElement
类型。另外,请注意此方法可以返回 null
。这是因为该方法无法确定运行前是否能够实际找到指定的元素。在代码片段的最后一行中,新的可选链接运算符用于调用 appendChild
。
¥Pass it an element id string and it will return either HTMLElement
or null
. This method introduces one of the most important types, HTMLElement
. It serves as the base interface for every other element interface. For example, the p
variable in the code example is of type HTMLParagraphElement
. Also, take note that this method can return null
. This is because the method can’t be certain pre-runtime if it will be able to actually find the specified element or not. In the last line of the code snippet, the new optional chaining operator is used to call appendChild
.
Document.createElement
此方法的定义是(我省略了已弃用的定义):
¥The definition for this method is (I have omitted the deprecated definition):
ts
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;
这是一个重载的函数定义。第二个重载最简单,并且与 getElementById
方法非常相似。将任何 string
传递给它,它将返回一个标准的 HTMLElement。此定义使开发者能够创建唯一的 HTML 元素标签。
¥This is an overloaded function definition. The second overload is simplest and works a lot like the getElementById
method does. Pass it any string
and it will return a standard HTMLElement. This definition is what enables developers to create unique HTML element tags.
例如 document.createElement('xyz')
返回一个 <xyz></xyz>
元素,显然不是 HTML 规范指定的元素。
¥For example document.createElement('xyz')
returns a <xyz></xyz>
element, clearly not an element that is specified by the HTML specification.
对于那些感兴趣的人,你可以使用
document.getElementsByTagName
与自定义标签元素进行交互¥For those interested, you can interact with custom tag elements using the
document.getElementsByTagName
对于 createElement
的第一个定义,它使用了一些高级泛型模式。最好将其分解为块,从泛型表达式开始:<K extends keyof HTMLElementTagNameMap>
。该表达式定义了一个泛型参数 K
,它被限制为接口 HTMLElementTagNameMap
的键。映射接口包含每个指定的 HTML 标签名称及其对应的类型接口。例如,这里是前 5 个映射值:
¥For the first definition of createElement
, it is using some advanced generic patterns. It is best understood broken down into chunks, starting with the generic expression: <K extends keyof HTMLElementTagNameMap>
. This expression defines a generic parameter K
that is constrained to the keys of the interface HTMLElementTagNameMap
. The map interface contains every specified HTML tag name and its corresponding type interface. For example here are the first 5 mapped values:
ts
interface HTMLElementTagNameMap {"a": HTMLAnchorElement;"abbr": HTMLElement;"address": HTMLElement;"applet": HTMLAppletElement;"area": HTMLAreaElement;...}
有些元素不具有唯一属性,因此它们只返回 HTMLElement
,但其他类型确实具有唯一属性和方法,因此它们返回其特定接口(将从 HTMLElement
扩展或实现 HTMLElement
)。
¥Some elements do not exhibit unique properties and so they just return HTMLElement
, but other types do have unique properties and methods so they return their specific interface (which will extend from or implement HTMLElement
).
现在,对于 createElement
定义的其余部分:(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]
。第一个参数 tagName
被定义为泛型参数 K
。TypeScript 解释器足够聪明,可以从这个参数中推断出泛型参数。这意味着开发者在使用该方法时不必指定泛型参数;传递给 tagName
参数的任何值都将被推断为 K
,因此可以在定义的其余部分中使用。这正是发生的事情;返回值 HTMLElementTagNameMap[K]
采用 tagName
参数并使用它返回相应的类型。此定义是代码片段中的 p
变量如何获取 HTMLParagraphElement
类型的方式。如果代码是 document.createElement('a')
,那么它将是 HTMLAnchorElement
类型的元素。
¥Now, for the remainder of the createElement
definition: (tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]
. The first argument tagName
is defined as the generic parameter K
. The TypeScript interpreter is smart enough to infer the generic parameter from this argument. This means that the developer does not have to specify the generic parameter when using the method; whatever value is passed to the tagName
argument will be inferred as K
and thus can be used throughout the remainder of the definition. This is exactly what happens; the return value HTMLElementTagNameMap[K]
takes the tagName
argument and uses it to return the corresponding type. This definition is how the p
variable from the code snippet gets a type of HTMLParagraphElement
. And if the code was document.createElement('a')
, then it would be an element of type HTMLAnchorElement
.
Node
接口
¥The Node
interface
document.getElementById
函数返回一个 HTMLElement
。HTMLElement
接口扩展 Element
接口,Element
接口扩展 Node
接口。此原型扩展允许所有 HTMLElements
使用标准方法的子集。在代码片段中,我们使用 Node
接口上定义的属性将新的 p
元素附加到网站。
¥The document.getElementById
function returns an HTMLElement
. HTMLElement
interface extends the Element
interface which extends the Node
interface. This prototypal extension allows for all HTMLElements
to utilize a subset of standard methods. In the code snippet, we use a property defined on the Node
interface to append the new p
element to the website.
Node.appendChild
代码片段的最后一行是 app?.appendChild(p)
。前面的 document.getElementById
部分详细说明了此处使用了可选的链接运算符,因为 app
在运行时可能为 null。appendChild
方法定义如下:
¥The last line of the code snippet is app?.appendChild(p)
. The previous, document.getElementById
, section detailed that the optional chaining operator is used here because app
can potentially be null at runtime. The appendChild
method is defined by:
ts
appendChild<T extends Node>(newChild: T): T;
此方法的工作方式与 createElement
方法类似,因为泛型参数 T
是从 newChild
参数推断出来的。T
被限制到另一个基本接口 Node
。
¥This method works similarly to the createElement
method as the generic parameter T
is inferred from the newChild
argument. T
is constrained to another base interface Node
.
children
和 childNodes
的区别
¥Difference between children
and childNodes
此前,本文档详细介绍了 HTMLElement
接口扩展自 Element
,Element
扩展自 Node
。在 DOM API 中有一个子元素的概念。例如在下面的 HTML 中,p
标签是 div
元素的子元素
¥Previously, this document details the HTMLElement
interface extends from Element
which extends from Node
. In the DOM API there is a concept of children elements. For example in the following HTML, the p
tags are children of the div
element
tsx
<div><p>Hello, World</p><p>TypeScript!</p></div>;const div = document.getElementsByTagName("div")[0];div.children;// HTMLCollection(2) [p, p]div.childNodes;// NodeList(2) [p, p]
捕获 div
元素后,children
属性将返回包含 HTMLParagraphElements
的 HTMLCollection
列表。childNodes
属性将返回类似的 NodeList
节点列表。每个 p
标签仍然是 HTMLParagraphElements
类型,但 NodeList
可以包含 HTMLCollection
列表不能包含的额外 HTML 节点。
¥After capturing the div
element, the children
prop will return an HTMLCollection
list containing the HTMLParagraphElements
. The childNodes
property will return a similar NodeList
list of nodes. Each p
tag will still be of type HTMLParagraphElements
, but the NodeList
can contain additional HTML nodes that the HTMLCollection
list cannot.
通过删除 p
标记之一修改 HTML,但保留文本。
¥Modify the HTML by removing one of the p
tags, but keep the text.
tsx
<div><p>Hello, World</p>TypeScript!</div>;const div = document.getElementsByTagName("div")[0];div.children;// HTMLCollection(1) [p]div.childNodes;// NodeList(2) [p, text]
查看两个列表如何变化。children
现在只包含 <p>Hello, World</p>
元素,childNodes
包含一个 text
节点而不是两个 p
节点。NodeList
的 text
部分是包含文本 TypeScript!
的字面 Node
。children
列表不包含此 Node
,因为它不被视为 HTMLElement
。
¥See how both lists change. children
now only contains the <p>Hello, World</p>
element, and the childNodes
contains a text
node rather than two p
nodes. The text
part of the NodeList
is the literal Node
containing the text TypeScript!
. The children
list does not contain this Node
because it is not considered an HTMLElement
.
querySelector
和 querySelectorAll
方法
¥The querySelector
and querySelectorAll
methods
这两种方法都是获取符合更独特约束集的 dom 元素列表的好工具。它们在 lib.dom.d.ts 中定义为:
¥Both of these methods are great tools for getting lists of dom elements that fit a more unique set of constraints. They are defined in lib.dom.d.ts as:
ts
/*** Returns the first element that is a descendant of node that matches selectors.*/querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;querySelector<E extends Element = Element>(selectors: string): E | null;/*** Returns all element descendants of node that match selectors.*/querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
querySelectorAll
的定义与 getElementsByTagName
类似,只是它返回一个新类型:NodeListOf
。此返回类型本质上是标准 JavaScript 列表元素的自定义实现。可以说,用 E[]
替换 NodeListOf<E>
会导致非常相似的用户体验。NodeListOf
只实现了以下属性和方法:length
、item(index)
、forEach((value, key, parent) => void)
和数字索引。此外,此方法返回元素列表,而不是节点,这是 NodeList
从 .childNodes
方法返回的内容。虽然这可能看起来不一致,但请注意接口 Element
是从 Node
扩展而来的。
¥The querySelectorAll
definition is similar to getElementsByTagName
, except it returns a new type: NodeListOf
. This return type is essentially a custom implementation of the standard JavaScript list element. Arguably, replacing NodeListOf<E>
with E[]
would result in a very similar user experience. NodeListOf
only implements the following properties and methods: length
, item(index)
, forEach((value, key, parent) => void)
, and numeric indexing. Additionally, this method returns a list of elements, not nodes, which is what NodeList
was returning from the .childNodes
method. While this may appear as a discrepancy, take note that interface Element
extends from Node
.
要查看这些方法的实际效果,请将现有代码修改为:
¥To see these methods in action modify the existing code to:
tsx
<ul><li>First :)</li><li>Second!</li><li>Third times a charm.</li></ul>;const first = document.querySelector("li"); // returns the first li elementconst all = document.querySelectorAll("li"); // returns the list of all li elements
有兴趣了解更多吗?
¥Interested in learning more?
lib.dom.d.ts 类型定义最好的部分是它们反映了 Mozilla 开发者网络 (MDN) 文档站点中注释的类型。例如,HTMLElement
接口在 MDN 上由这个 HTMLElement 页面 记录。这些页面列出了所有可用的属性、方法,有时甚至是示例。这些页面的另一个重要方面是它们提供了指向相应标准文档的链接。这是 W3C 对 HTMLElement 的推荐 的链接。
¥The best part about the lib.dom.d.ts type definitions is that they are reflective of the types annotated in the Mozilla Developer Network (MDN) documentation site. For example, the HTMLElement
interface is documented by this HTMLElement page on MDN. These pages list all available properties, methods, and sometimes even examples. Another great aspect of the pages is that they provide links to the corresponding standard documents. Here is the link to the W3C Recommendation for HTMLElement.
资料来源:
¥Sources: