1. 浏览器中的JavaScript:BOM与DOM的定位与关系
作为一名前端开发者,我经常遇到新手分不清BOM和DOM区别的情况。简单来说,BOM(Browser Object Model)是浏览器提供给JavaScript操作浏览器窗口的接口集合,而DOM(Document Object Model)则是浏览器将HTML文档解析成的树状结构模型。两者虽然经常一起使用,但职责完全不同。
BOM的核心对象包括:
- window:浏览器窗口的顶级对象
- navigator:浏览器信息(如用户代理、插件等)
- location:当前页面URL信息
- history:浏览器历史记录
- screen:用户屏幕信息
- document:这个属性实际上属于DOM的入口
DOM则是我们操作页面内容的主要方式:
- 文档树结构(节点、元素、文本等)
- 查询和修改方法(getElementById、querySelector等)
- 事件系统(addEventListener等)
- CSS样式操作
重要提示:虽然document对象技术上属于DOM,但它通过window.document访问,这正体现了BOM和DOM的紧密关系。
2. BOM核心API详解与实战应用
2.1 window对象的强大能力
window对象是BOM的核心,它代表了浏览器窗口本身。以下是我在项目中常用的window属性和方法:
// 获取窗口尺寸(视口大小,不包括工具栏等) const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 滚动控制 window.scrollTo(0, 100); // 滚动到指定位置 window.scrollBy(0, 50); // 相对当前滚动位置移动 // 定时器(注意内存泄漏问题) const timer = setTimeout(() => { console.log('延迟执行'); }, 1000); // 取消定时器 clearTimeout(timer);实际应用场景:响应式布局中,我们经常需要监听窗口大小变化:
window.addEventListener('resize', () => { if (window.innerWidth < 768) { // 移动端布局调整 } else { // 桌面端布局调整 } });2.2 location对象的URL操作技巧
location对象提供了丰富的URL操作能力,这是我总结的实用方法:
// 获取当前URL信息 console.log(location.href); // 完整URL console.log(location.protocol); // 协议(http:或https:) console.log(location.hostname); // 域名 console.log(location.pathname); // 路径部分 // 页面跳转的三种方式 location.href = 'https://example.com'; // 记录历史 location.assign('https://example.com'); // 同上 location.replace('https://example.com'); // 不记录历史 // 刷新页面 location.reload(); // 可能从缓存加载 location.reload(true); // 强制从服务器重新加载避坑指南:使用replace()而不是href/assign()可以避免产生多余的历史记录,这在处理登录跳转等场景特别有用。
2.3 history API的现代前端应用
history对象允许我们在不刷新页面的情况下操作浏览器历史记录。现代单页应用(SPA)的核心就是基于此:
// 添加历史记录(不会触发页面刷新) history.pushState({page: 1}, "title 1", "?page=1"); // 替换当前历史记录 history.replaceState({page: 2}, "title 2", "?page=2"); // 监听popstate事件(用户点击前进/后退按钮) window.addEventListener('popstate', (event) => { console.log("Location: " + document.location + ", state: " + JSON.stringify(event.state)); });实战经验:在使用Vue Router或React Router时,它们底层就是基于history API实现的。我曾遇到一个坑:直接修改URL参数但忘记更新页面状态,导致视图与URL不一致。正确的做法是:
// 更新URL同时更新应用状态 function updatePage(page) { history.pushState({page}, `Page ${page}`, `?page=${page}`); renderPage(page); // 同步更新视图 }3. DOM核心概念与高效操作
3.1 理解DOM树结构
DOM将HTML文档表示为节点树,这是我画的一个简单DOM树示意图:
document ├── html │ ├── head │ │ ├── title │ │ └── meta │ └── body │ ├── div#app │ │ ├── h1 │ │ └── p │ └── script节点主要类型:
- 元素节点(如)
- 文本节点(元素内的文字)
- 属性节点(如id="app")
- 注释节点
性能提示:DOM操作是昂贵的,因为每次修改都可能触发浏览器的重排(reflow)和重绘(repaint)。我的经验是批量操作DOM时,可以先操作文档片段(DocumentFragment):
const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = `Item ${i}`; fragment.appendChild(li); } document.getElementById('list').appendChild(fragment);3.2 现代DOM查询方法对比
选择元素有多种方式,这是我整理的性能对比和使用建议:
| 方法 | 返回类型 | 实时性 | 性能 | 适用场景 |
|---|---|---|---|---|
| getElementById | 单个元素 | 是 | ★★★★ | 精确ID查找 |
| getElementsByClassName | HTMLCollection | 是 | ★★★ | 类名查找 |
| getElementsByTagName | HTMLCollection | 是 | ★★★ | 标签查找 |
| querySelector | 单个元素 | 否 | ★★ | CSS选择器查找单个 |
| querySelectorAll | NodeList | 否 | ★★ | CSS选择器查找多个 |
实际建议:
- 优先使用getElementById处理已知ID的元素
- 需要CSS选择器灵活性时用querySelector/querySelectorAll
- 注意HTMLCollection是实时的,而querySelectorAll返回的NodeList是静态的
// 实时集合 vs 静态集合示例 const liveCollection = document.getElementsByClassName('item'); const staticList = document.querySelectorAll('.item'); console.log(liveCollection.length); // 假设初始2个 console.log(staticList.length); // 2 // 添加一个新元素 document.body.appendChild(document.createElement('div')).className = 'item'; console.log(liveCollection.length); // 现在3个 console.log(staticList.length); // 仍然是23.3 高效DOM操作的最佳实践
基于多年项目经验,我总结出以下DOM操作黄金法则:
最小化DOM访问:将DOM引用缓存到变量中
// 不好:多次查询DOM for (let i = 0; i < 10; i++) { document.getElementById('list').innerHTML += `<li>${i}</li>`; } // 好:缓存引用 const list = document.getElementById('list'); let html = ''; for (let i = 0; i < 10; i++) { html += `<li>${i}</li>`; } list.innerHTML = html;使用事件委托:利用事件冒泡减少事件监听器数量
// 不好:每个按钮都添加监听器 document.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('click', handleClick); }); // 好:在父元素上委托 document.getElementById('container').addEventListener('click', (event) => { if (event.target.classList.contains('btn')) { handleClick(event); } });合理使用重绘与重排:
// 触发多次重排 element.style.width = '100px'; element.style.height = '200px'; element.style.margin = '10px'; // 更好:使用class或一次性修改 element.classList.add('new-style'); // 或 element.style.cssText = 'width:100px;height:200px;margin:10px;';
4. BOM与DOM的交互与高级应用
4.1 利用BOM信息优化DOM操作
结合BOM信息可以做出更智能的DOM操作决策。例如,根据设备特性加载不同的内容:
// 检测移动设备 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // 根据设备加载不同大小的图片 const img = document.createElement('img'); img.src = isMobile ? 'small.jpg' : 'large.jpg'; document.body.appendChild(img);性能进阶:使用window.requestAnimationFrame优化动画:
function animate() { const element = document.getElementById('animated'); let pos = 0; function frame() { if (pos == 350) { clearInterval(id); } else { pos++; element.style.left = pos + 'px'; element.style.top = pos + 'px'; window.requestAnimationFrame(frame); } } window.requestAnimationFrame(frame); }4.2 现代前端框架中的BOM/DOM应用
虽然React/Vue等框架抽象了直接DOM操作,但理解底层原理仍然重要:
虚拟DOM的优化原理:
- 框架维护虚拟DOM的内存表示
- 状态变化时比较新旧虚拟DOM
- 计算最小DOM操作集(这就是为什么需要key属性)
- 批量应用到真实DOM
在React中安全访问BOM:
function ResponsiveComponent() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return <div>当前宽度: {width}px</div>; }Vue中的DOM更新时机:
Vue.component('example', { template: '<div>{{ message }}</div>', data() { return { message: '未更新' }; }, mounted() { this.message = '已更新'; // DOM尚未更新 console.log(this.$el.textContent); // "未更新" this.$nextTick(() => { // 现在DOM已更新 console.log(this.$el.textContent); // "已更新" }); } });
4.3 常见问题与调试技巧
问题1:javascript:void(0)是什么意思?
- 这是一个不执行任何操作的JavaScript表达式
- 常用于阻止a标签的默认行为:
<a href="javascript:void(0)" onclick="myFunction()">点击</a> - 现代做法是使用event.preventDefault()
问题2:JavaScript堆内存不足错误 当看到"JavaScript heap out of memory"错误时,可能是:
- 内存泄漏(未清除的定时器、事件监听器)
- 大数据处理未分片
- 递归调用没有终止条件
解决方案:
// 增加Node.js内存限制(前端项目通常不需要) node --max-old-space-size=4096 yourScript.js // 更好的方法是优化代码: // 1. 避免全局变量 // 2. 及时清除引用 // 3. 分块处理大数据调试技巧:
- 使用Chrome DevTools的Performance面板记录操作
- 使用Memory面板检测内存泄漏
- 使用Elements面板实时编辑DOM和样式
- 使用Console快速测试BOM/DOM API
// 在控制台快速测试选择器性能 console.time('query'); document.querySelectorAll('.some-class'); console.timeEnd('query');5. 浏览器兼容性与安全实践
5.1 跨浏览器兼容性处理
不同浏览器对BOM/DOM的实现有差异,这是我总结的兼容性处理方案:
特性检测优于浏览器检测:
// 不好:检测特定浏览器 if (navigator.userAgent.indexOf('MSIE') !== -1) { // IE特定代码 } // 好:检测特性支持 if ('querySelector' in document) { // 使用现代方法 } else { // 回退方案 }常用polyfill:
- classList.forEach(IE不支持NodeList.forEach)
- CustomEvent构造函数(IE9+需要特殊写法)
- requestAnimationFrame
CSS前缀处理:
function getSupportedTransform() { const prefixes = ['', 'webkit', 'moz', 'ms', 'o']; const style = document.createElement('div').style; for (let prefix of prefixes) { const property = prefix ? `${prefix}Transform` : 'transform'; if (property in style) { return property; } } return null; }
5.2 安全最佳实践
避免XSS攻击:
- 永远不要直接使用innerHTML插入用户输入
- 使用textContent而不是innerHTML处理文本
- 对用户输入进行转义
// 危险! element.innerHTML = userProvidedContent; // 安全 element.textContent = userProvidedContent; // 或使用DOMPurify等库净化HTML限制敏感信息:
- 避免在客户端代码中硬编码API密钥
- 使用Content Security Policy (CSP)头
- 注意location.href可能泄露referrer信息
安全地打开新窗口:
// 不好:可能被浏览器拦截 window.open('https://example.com'); // 好:作为用户操作的结果 button.addEventListener('click', () => { const newWindow = window.open(); newWindow.location.href = 'https://example.com'; });
6. 性能优化进阶技巧
6.1 DOM操作性能基准测试
为了量化不同DOM操作方法的性能差异,我进行了以下测试(Chrome 120):
| 操作方式 | 操作1000次耗时(ms) |
|---|---|
| innerHTML拼接 | 12 |
| appendChild单个 | 185 |
| DocumentFragment | 15 |
| cloneNode(true) | 18 |
| insertAdjacentHTML | 10 |
结论:
- 对于大量DOM创建,insertAdjacentHTML和DocumentFragment性能最佳
- 避免在循环中直接appendChild
- cloneNode适合复制现有DOM结构
6.2 高效事件处理模式
被动事件监听器: 对于scroll/touch等频繁事件,使用passive选项提高滚动性能:
window.addEventListener('scroll', onScroll, { passive: true });一次性事件: 使用{ once: true }选项让事件自动移除:
button.addEventListener('click', handleClick, { once: true });信号中止: 现代浏览器支持AbortController管理事件:
const controller = new AbortController(); button.addEventListener('click', handleClick, { signal: controller.signal }); // 需要时取消所有监听 controller.abort();
6.3 现代API替代方案
IntersectionObserver替代滚动监听:
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { console.log('元素进入视口'); } }); }); observer.observe(document.getElementById('target'));ResizeObserver替代resize事件:
const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { console.log('新尺寸:', entry.contentRect); } }); resizeObserver.observe(document.getElementById('resizable'));MutationObserver监听DOM变化:
const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { console.log('DOM变化:', mutation.type); }); }); observer.observe(document.getElementById('target'), { attributes: true, childList: true, subtree: true });
7. 实战案例:构建一个弹幕系统
让我们综合运用BOM/DOM知识实现一个简单的弹幕系统:
class Danmu { constructor(container) { this.container = container; this.width = container.clientWidth; this.height = container.clientHeight; this.tracks = Math.floor(this.height / 30); // 计算轨道数 this.trackStatus = new Array(this.tracks).fill(0); window.addEventListener('resize', this.handleResize.bind(this)); } handleResize() { this.width = this.container.clientWidth; this.height = this.container.clientHeight; const newTracks = Math.floor(this.height / 30); if (newTracks !== this.tracks) { this.tracks = newTracks; this.trackStatus = new Array(this.tracks).fill(0); } } send(text, options = {}) { const danmu = document.createElement('div'); danmu.className = 'danmu'; danmu.textContent = text; // 样式设置 Object.assign(danmu.style, { position: 'absolute', whiteSpace: 'nowrap', color: options.color || '#fff', fontSize: options.size || '24px', textShadow: '1px 1px 2px #000' }); // 选择轨道 let track; for (let i = 0; i < this.tracks; i++) { if (this.trackStatus[i] <= performance.now()) { track = i; break; } } if (track === undefined) { track = Math.floor(Math.random() * this.tracks); } const top = track * 30; danmu.style.top = `${top}px`; this.container.appendChild(danmu); const duration = options.duration || 10000; const start = performance.now(); const danmuWidth = danmu.offsetWidth; this.trackStatus[track] = start + duration * 0.7; // 轨道占用时间 function animate(time) { const elapsed = time - start; const progress = elapsed / duration; if (progress >= 1) { danmu.remove(); return; } danmu.style.left = `${this.width - danmuWidth * progress}px`; requestAnimationFrame(animate.bind(this)); } requestAnimationFrame(animate.bind(this)); } } // 使用示例 const container = document.getElementById('danmu-container'); const danmu = new Danmu(container); // 发送弹幕 danmu.send('Hello World!', { color: '#ff0000' });这个案例综合运用了:
- BOM的window.innerWidth/innerHeight
- DOM的元素创建与样式操作
- requestAnimationFrame实现动画
- 性能优化的轨道管理系统
- 响应式设计的resize处理
8. 前沿趋势与未来展望
虽然BOM/DOM是Web开发的基石,但新兴技术正在改变我们的使用方式:
Web Components:封装自定义元素,减少直接DOM操作
class MyElement extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style>:host { display: block; }</style> <slot></slot> `; } } customElements.define('my-element', MyElement);WebAssembly:高性能计算减少DOM操作负担
- 将计算密集型任务转移到WebAssembly
- DOM操作仍然需要通过JavaScript胶水代码
Service Worker:离线缓存和网络代理
// 注册Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); }Web Workers:后台线程处理减轻主线程负担
// 主线程 const worker = new Worker('worker.js'); worker.postMessage(data); // worker.js self.onmessage = (e) => { const result = heavyComputation(e.data); self.postMessage(result); };
这些新技术不是要取代BOM/DOM,而是让我们能够更合理地使用它们。在我最近的项目中,通过将Canvas动画的计算部分移到Web Worker,主线程的DOM操作流畅度提升了40%。
9. 学习资源与工具推荐
根据我的经验,这些资源对深入理解BOM/DOM特别有帮助:
官方文档:
- MDN Web Docs(最权威的参考)
- WHATWG DOM Living Standard
调试工具:
- Chrome DevTools
- Elements面板:实时DOM检查和编辑
- Performance面板:分析DOM操作性能
- Memory面板:检测DOM相关内存泄漏
- Firefox的DOM Inspector
- Chrome DevTools
性能分析工具:
- Lighthouse
- WebPageTest
兼容性检查:
- Can I Use(检查API支持情况)
- BrowserStack(跨浏览器测试)
实用库:
- jQuery(虽然过时但DOM操作API设计值得学习)
- Lodash(实用的工具函数)
- DOMPurify(HTML净化)
10. 面试常见问题解析
根据我作为面试官的经验,这些BOM/DOM问题是高频考点:
事件循环与DOM更新: "点击按钮后立即console.log元素内容,为什么看不到DOM更新?"
button.addEventListener('click', () => { element.textContent = '更新'; console.log(element.textContent); // 可能还是旧值 // 因为浏览器尚未重绘 });事件代理的实现原理: "如何给动态添加的元素绑定事件?"
// 通过父元素代理 parent.addEventListener('click', (e) => { if (e.target.matches('.dynamic-item')) { // 处理动态元素点击 } });虚拟DOM的diff算法: "为什么列表项需要key属性?"
- key帮助框架识别节点身份
- 减少不必要的DOM操作
- 维持组件状态正确
重排(reflow)与重绘(repaint): "哪些操作会触发重排?"
- 添加/删除可见DOM元素
- 元素位置、尺寸改变
- 内容变化(如文本改变)
- 页面渲染初始化
- 窗口大小改变
内存泄漏场景: "哪些情况会导致DOM相关内存泄漏?"
- 未清除的定时器引用DOM元素
- 事件监听器未移除
- 闭包中保存DOM引用
- 全局变量存储DOM节点
11. 个人经验与实用技巧
在多年的前端开发中,我总结了这些宝贵的BOM/DOM实践经验:
DOM操作可视化调试: 在Chrome DevTools的Console中,可以使用以下命令临时高亮元素:
// 高亮元素5秒 inspect(document.getElementById('target')); // 或使用 $0.style.outline = '2px solid red'; // $0是Elements面板当前选中的元素快速创建DOM结构: 使用template标签创建可复用的DOM模板:
<template id="user-card"> <div class="card"> <h2 class="name"></h2> <p class="email"></p> </div> </template> <script> const template = document.getElementById('user-card'); const clone = template.content.cloneNode(true); clone.querySelector('.name').textContent = '张三'; clone.querySelector('.email').textContent = 'zhangsan@example.com'; document.body.appendChild(clone); </script>高效属性操作: 使用dataset处理自定义数据属性:
<div id="user">element.classList.add('active'); element.classList.remove('hidden'); element.classList.toggle('visible'); // 比直接操作className字符串更可靠 // 不好: element.className += ' active';DOM变化监听技巧: 当需要知道DOM何时完成变化时,可以使用这个技巧:
function onDomChange(element, callback) { const observer = new MutationObserver(() => { callback(); observer.disconnect(); }); observer.observe(element, { childList: true, subtree: true, attributes: true }); } // 使用 onDomChange(document.getElementById('list'), () => { console.log('DOM已更新'); });
12. 性能监控与异常处理
12.1 监控DOM操作性能
在实际项目中,我使用PerformanceObserver监控关键DOM操作的性能:
const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(`[${entry.name}] 耗时: ${entry.duration.toFixed(2)}ms`); } }); observer.observe({ entryTypes: ['measure', 'paint'] }); // 测量DOM操作 performance.mark('start-render'); renderLargeList(); // 你的DOM操作函数 performance.mark('end-render'); performance.measure('DOM Render', 'start-render', 'end-render');12.2 错误处理最佳实践
DOM操作中的错误处理至关重要:
// 安全地查询元素 function getElementSafe(selector) { const element = document.querySelector(selector); if (!element) { console.error(`元素 ${selector} 未找到`); return null; } return element; } // 事件监听器的错误处理 function addEventListenerWithErrorHandling(element, event, handler) { element.addEventListener(event, (...args) => { try { return handler(...args); } catch (error) { console.error(`处理事件 ${event} 时出错:`, error); // 可以选择阻止事件冒泡 // args[0].stopPropagation(); } }); } // 使用 const button = getElementSafe('#submit'); if (button) { addEventListenerWithErrorHandling(button, 'click', () => { // 可能抛出错误的代码 }); }12.3 内存泄漏检测
使用Chrome DevTools检测DOM相关内存泄漏:
- 打开DevTools → Memory
- 选择"Heap snapshot"并拍摄快照
- 执行可能泄漏的操作
- 再次拍摄快照
- 比较两个快照,查看Detached DOM树
常见泄漏模式:
- 被JavaScript引用但已从DOM移除的元素
- 未清除的事件监听器
- 缓存中的DOM节点引用
// 典型的泄漏模式 const elements = []; function createLeak() { const div = document.createElement('div'); document.body.appendChild(div); elements.push(div); // 即使从DOM移除,数组仍保持引用 document.body.removeChild(div); }13. 移动端特殊处理
移动设备上的BOM/DOM有一些特殊考量:
13.1 触摸事件处理
// 同时处理触摸和鼠标事件 element.addEventListener('touchstart', handleTouch); element.addEventListener('mousedown', handleMouse); function handleTouch(e) { // 阻止触摸事件触发鼠标事件 e.preventDefault(); const touch = e.changedTouches[0]; console.log(`触摸位置: ${touch.pageX}, ${touch.pageY}`); } function handleMouse(e) { console.log(`点击位置: ${e.pageX}, ${e.pageY}`); }13.2 视口控制
// 检测设备像素比 const dpr = window.devicePixelRatio || 1; // 设置视口meta标签(通常在HTML中) const meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = `width=device-width, initial-scale=${1/dpr}, maximum-scale=${1/dpr}, user-scalable=no`; document.head.appendChild(meta);13.3 移动端性能优化
减少复合层:
/* 慎用这些属性,它们会创建新的复合层 */ .element { will-change: transform; transform: translateZ(0); }使用touch-action控制手势:
// 禁止某些元素的默认触摸行为 document.getElementById('scrollable').style.touchAction = 'pan-y';被动事件监听器改进滚动性能:
document.addEventListener('touchmove', onTouchMove, { passive: true });
14. 服务端渲染(SSR)中的DOM处理
服务端渲染时,BOM对象不可用,需要特殊处理:
14.1 环境检测
// 检测是否在浏览器环境 const isBrowser = typeof window !== 'undefined'; // 安全地访问window function getWindow() { if (isBrowser) { return window; } return { innerWidth: 1024, innerHeight: 768, // 其他必要属性的默认值 }; }14.2 动态导入浏览器专用代码
if (isBrowser) { import('./browser-only-module').then(module => { module.init(); }); }14.3 Next.js/Nuxt.js中的处理
在流行SSR框架中的最佳实践:
// Next.js示例 import { useEffect, useState } from 'react'; function useWindowSize() { const [size, setSize] = useState({ width: 1024, height: 768, }); useEffect(() => { // 只在客户端执行 if (typeof window !== 'undefined') { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', handleResize); } }, []); return size; }15. 测试与自动化
15.1 单元测试DOM操作
使用Jest和jsdom测试DOM相关代码:
// __tests__/domUtils.test.js const { querySelectorAll } = require('../domUtils'); describe('DOM工具函数', () => { beforeAll(() => { document.body.innerHTML = ` <div id="container"> <button class="btn">按钮1</button> <button class="btn">按钮2</button> </div> `; }); test('querySelectorAll返回正确数量', () => { const buttons = querySelectorAll('.btn'); expect(buttons.length).toBe(2); }); });15.2 E2E测试
使用Cypress测试真实DOM交互:
describe('登录表单', () => { it('成功提交', () => { cy.visit('/login'); cy.get('#username').type('testuser'); cy.get('#password').type('password123'); cy.get('#submit').click(); cy.url().should('include', '/dashboard'); }); });15.3 视觉回归测试
使用Storybook + Chromatic确保DOM变化不会意外影响UI:
// Button.stories.js export default { title: 'Components/Button', component: Button, }; export const Primary = () => <Button primary>按钮</Button>; export const Secondary = () => <Button secondary>按钮</Button>;16. 架构设计中的DOM考量
16.1 状态与DOM分离
良好的架构应该最小化直接DOM操作:
// 不好:直接操作DOM的业务逻辑 function updateUserProfile(user) { document.getElementById('name').textContent = user.name; document.getElementById('email').textContent = user.email; // 更多DOM操作... } // 好:分离状态管理和渲染 class UserProfile { constructor() { this.state = { name: '', email: '' }; } setState(newState) { this.state = { ...this.state, ...newState }; this.render(); } render() { document.getElementById('name').textContent = this.state.name; document.getElementById('email').textContent = this.state.email; } }16.2 组件化设计
将DOM结构封装为可重用组件:
class Modal { constructor(content) { this.content = content; this.element = this.createModal(); } createModal() { const modal = document.createElement('div'); modal.className = 'modal'; const content = document.createElement('div'); content.className = 'modal-content'; content.innerHTML = this.content; const close = document.createElement('button'); close.className = 'close'; close.textContent = '×'; close.addEventListener('click', () => this.hide()); modal.append(close, content); return modal; } show() { document.body.appendChild(this.element); } hide() { this.element.remove(); } } // 使用 const modal = new Modal('<h2>你好</h2><p>这是一个模态框</p>'); modal.show();17. 无障碍访问(A11Y)实践
17.1 ARIA属性正确使用
// 动态更新ARIA属性 const button = document.getElementById('expand'); const content = document.getElementById('content'); button.setAttribute('aria-expanded', 'false'); button.addEventListener('click', () => { const expanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', String(!expanded)); content.hidden = expanded; });17.2 焦点管理
// 模态框的焦点管理 class AccessibleModal { constructor(element) { this.modal = element; this.focusableElements = Array.from( this.modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') ); this.firstElement = this.focusableElements[0]; this.lastElement = this.focusableElements[this.focusableElements.length - 1]; this.modal.addEventListener('keydown', this.handleTabKey.bind(this)); } handleTabKey(e) { if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === this.firstElement) { this.lastElement.focus(); e.preventDefault(); } else if (!e.shiftKey && document.activeElement === this.lastElement) { this.first