TypeScript 开发指南 / 17 - DOM 操作
DOM 操作
TypeScript 为浏览器 DOM API 提供了完整的类型定义,让 DOM 操作更加安全。
获取 DOM 元素
基本查询
// getElementById 返回 HTMLElement | null
const element = document.getElementById("app");
// element: HTMLElement | null
// 需要空值检查
if (element) {
element.innerHTML = "Hello";
}
// 使用非空断言(当你确定元素存在时)
const app = document.getElementById("app")!;
app.innerHTML = "Hello";
// querySelector 返回更精确的类型
const button = document.querySelector("button");
// button: HTMLButtonElement | null
const input = document.querySelector<HTMLInputElement>("#username");
// input: HTMLInputElement | null
const div = document.querySelector<HTMLDivElement>(".container");
// div: HTMLDivElement | null
元素类型层级
EventTarget
└── Node
├── Element
│ ├── HTMLElement
│ │ ├── HTMLInputElement
│ │ ├── HTMLButtonElement
│ │ ├── HTMLDivElement
│ │ ├── HTMLFormElement
│ │ ├── HTMLAnchorElement
│ │ ├── HTMLImageElement
│ │ ├── HTMLSelectElement
│ │ ├── HTMLTextAreaElement
│ │ └── ...更多
│ └── SVGElement
├── Document
├── DocumentFragment
└── CharacterData
├── Text
└── Comment
类型断言获取精确类型
// 使用 as 断言
const input = document.getElementById("username") as HTMLInputElement;
input.value = "Alice";
// 使用泛型参数
const input = document.querySelector<HTMLInputElement>("#username");
if (input) {
input.value = "Alice";
}
// 使用 satisfies(TypeScript 4.9+)
const config = {
container: document.getElementById("app") as HTMLDivElement,
input: document.getElementById("search") as HTMLInputElement
};
创建 DOM 元素
// createElement 返回精确类型
const div = document.createElement("div");
// div: HTMLDivElement
const button = document.createElement("button");
// button: HTMLButtonElement
const input = document.createElement("input");
// input: HTMLInputElement
// 设置属性
div.className = "container";
div.id = "main";
div.setAttribute("data-role", "content");
button.textContent = "Click me";
button.disabled = false;
input.type = "text";
input.placeholder = "Enter your name";
input.value = "";
// 添加子元素
document.body.appendChild(div);
div.appendChild(button);
div.appendChild(input);
DOM 属性操作
const element = document.getElementById("myElement")!;
// 常用属性
element.id = "newId";
element.className = "class1 class2";
element.classList.add("new-class");
element.classList.remove("old-class");
element.classList.toggle("active");
element.classList.contains("active"); // boolean
// 样式
element.style.color = "red";
element.style.fontSize = "16px";
element.style.backgroundColor = "#fff";
// 内容
element.textContent = "Hello World"; // 纯文本
element.innerHTML = "<strong>Hello</strong>"; // HTML
// 数据属性
element.dataset.userId = "123";
element.dataset.role = "admin";
元素尺寸和位置
const element = document.getElementById("box")!;
// 元素尺寸(只读)
const rect = element.getBoundingClientRect();
console.log(rect.width, rect.height);
console.log(rect.top, rect.left);
// 包含 padding 的尺寸
console.log(element.clientWidth, element.clientHeight);
// 包含 border 和 padding 的尺寸
console.log(element.offsetWidth, element.offsetHeight);
// 滚动尺寸
console.log(element.scrollHeight, element.scrollWidth);
// 滚动位置
element.scrollTop = 100;
element.scrollLeft = 0;
事件处理
基本事件绑定
const button = document.querySelector<HTMLButtonElement>("#myButton")!;
// addEventListener
button.addEventListener("click", (event) => {
// event: MouseEvent
console.log(event.clientX, event.clientY);
console.log(event.target); // EventTarget | null
console.log(event.currentTarget); // EventTarget | null
});
// 使用类型参数指定事件类型
button.addEventListener("click", (event: MouseEvent) => {
console.log(event.button); // 鼠标按键
console.log(event.buttons); // 按下的按键掩码
});
常见事件类型
// 鼠标事件
element.addEventListener("click", (e: MouseEvent) => {});
element.addEventListener("dblclick", (e: MouseEvent) => {});
element.addEventListener("mousedown", (e: MouseEvent) => {});
element.addEventListener("mouseup", (e: MouseEvent) => {});
element.addEventListener("mousemove", (e: MouseEvent) => {});
element.addEventListener("mouseenter", (e: MouseEvent) => {});
element.addEventListener("mouseleave", (e: MouseEvent) => {});
// 键盘事件
document.addEventListener("keydown", (e: KeyboardEvent) => {
console.log(e.key); // 按键名
console.log(e.code); // 按键码
console.log(e.ctrlKey); // 是否按下 Ctrl
console.log(e.shiftKey);
console.log(e.altKey);
});
// 输入事件
input.addEventListener("input", (e: Event) => {
const target = e.target as HTMLInputElement;
console.log(target.value);
});
input.addEventListener("change", (e: Event) => {
const target = e.target as HTMLInputElement;
console.log(target.value);
});
// 表单事件
form.addEventListener("submit", (e: Event) => {
e.preventDefault();
const formData = new FormData(form);
});
// 焦点事件
input.addEventListener("focus", (e: FocusEvent) => {});
input.addEventListener("blur", (e: FocusEvent) => {});
// 滚动事件
window.addEventListener("scroll", (e: Event) => {
console.log(window.scrollY);
});
// 拖拽事件
element.addEventListener("dragstart", (e: DragEvent) => {});
element.addEventListener("dragover", (e: DragEvent) => {});
element.addEventListener("drop", (e: DragEvent) => {});
事件委托
const list = document.querySelector<HTMLUListElement>("#todoList")!;
list.addEventListener("click", (event: MouseEvent) => {
const target = event.target as HTMLElement;
// 检查点击的元素
if (target.matches(".delete-btn")) {
const item = target.closest("li");
if (item) {
item.remove();
}
}
if (target.matches(".edit-btn")) {
// 编辑逻辑
}
});
自定义事件
// 创建自定义事件
interface UserEventDetail {
userId: number;
action: "login" | "logout";
}
const event = new CustomEvent<UserEventDetail>("userAction", {
detail: { userId: 1, action: "login" },
bubbles: true,
cancelable: true
});
// 监听自定义事件
document.addEventListener("userAction", ((e: CustomEvent<UserEventDetail>) => {
console.log(e.detail.userId);
console.log(e.detail.action);
}) as EventListener);
// 派发事件
document.dispatchEvent(event);
事件处理器的类型安全
// 封装类型安全的事件绑定
function addTypedEventListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
type: K,
handler: (event: HTMLElementEventMap[K]) => void
): void {
element.addEventListener(type, handler as EventListener);
}
// 使用
const button = document.querySelector<HTMLButtonElement>("#btn")!;
addTypedEventListener(button, "click", (e) => {
// e: MouseEvent(自动推断)
console.log(e.clientX);
});
addTypedEventListener(button, "mouseenter", (e) => {
// e: MouseEvent
});
表单操作
const form = document.querySelector<HTMLFormElement>("#myForm")!;
const nameInput = form.querySelector<HTMLInputElement>("#name")!;
const emailInput = form.querySelector<HTMLInputElement>("#email")!;
const selectBox = form.querySelector<HTMLSelectElement>("#role")!;
const checkbox = form.querySelector<HTMLInputElement>("#agree")!;
// 读取表单值
const name = nameInput.value;
const email = emailInput.value;
const role = selectBox.value;
const agreed = checkbox.checked;
// 设置表单值
nameInput.value = "Alice";
emailInput.value = "alice@example.com";
selectBox.value = "admin";
checkbox.checked = true;
// 表单提交
form.addEventListener("submit", (e: SubmitEvent) => {
e.preventDefault();
// FormData
const formData = new FormData(form);
// 转换为对象
const data = Object.fromEntries(formData.entries());
console.log(data);
});
// 表单验证
nameInput.addEventListener("input", () => {
if (nameInput.validity.valueMissing) {
nameInput.setCustomValidity("请输入姓名");
} else if (nameInput.validity.tooShort) {
nameInput.setCustomValidity("姓名太短");
} else {
nameInput.setCustomValidity("");
}
});
虚拟 DOM 类型
// 简化的虚拟 DOM 类型
interface VNode {
type: string;
props: Record<string, any>;
children: (VNode | string)[];
}
function h(
type: string,
props: Record<string, any> | null,
...children: (VNode | string)[]
): VNode {
return { type, props: props || {}, children };
}
// 类型安全的 JSX 工厂
const vnode = h("div", { class: "container" },
h("h1", null, "Hello"),
h("p", null, "World")
);
业务场景:拖拽排序
interface DraggableOptions {
container: HTMLElement;
itemSelector: string;
handleSelector?: string;
onReorder: (items: HTMLElement[]) => void;
}
function initDraggable(options: DraggableOptions): void {
const { container, itemSelector, handleSelector, onReorder } = options;
let draggedItem: HTMLElement | null = null;
container.addEventListener("dragstart", (e: DragEvent) => {
const target = e.target as HTMLElement;
if (!target.matches(itemSelector)) return;
draggedItem = target;
target.classList.add("dragging");
e.dataTransfer!.effectAllowed = "move";
});
container.addEventListener("dragover", (e: DragEvent) => {
e.preventDefault();
e.dataTransfer!.dropEffect = "move";
const target = (e.target as HTMLElement).closest(itemSelector) as HTMLElement;
if (target && target !== draggedItem) {
const rect = target.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
if (e.clientY < midY) {
container.insertBefore(draggedItem!, target);
} else {
container.insertBefore(draggedItem!, target.nextSibling);
}
}
});
container.addEventListener("dragend", () => {
if (draggedItem) {
draggedItem.classList.remove("dragging");
draggedItem = null;
const items = Array.from(container.querySelectorAll(itemSelector));
onReorder(items as HTMLElement[]);
}
});
}
注意事项
getElementById返回HTMLElement | null——使用querySelector<T>获取更精确的类型- 事件处理函数的类型——TypeScript 会根据事件名自动推断事件类型
event.target是EventTarget | null——需要类型断言才能访问特定元素的属性- 空值检查——DOM 查询可能返回 null,始终进行检查
- 使用
closest进行事件委托——确保事件处理正确的元素