科技前沿 科普Web 基础教程建站系列

DOM 操作:JavaScript 如何控制页面

📅 2026-07-06

HTML 搭骨架,CSS 上妆面,但让网页真正「活过来」的是 JavaScript。用户点一个按钮就切换内容、提交表单时即时校验、点赞后数字 +1——这些都是 DOM 操作的功劳。

白物集的仪表盘页面(dashboard.astro)就是一个很好的例子:五个 Tab 切换、用户资料编辑、通知已读标记、封禁表单提交——没跳转页面,全靠 JavaScript 在浏览器里操作 DOM。

DOM 是什么

DOM(Document Object Model)是浏览器把 HTML 解析后生成的一棵树。每个 HTML 标签变成一个节点(Node),节点之间有父子关系。

document
 └── html
      ├── head
      │    └── title
      └── body
           ├── header
           ├── main
           └── footer

JavaScript 通过操作这棵树来改变页面——不是刷新页面,是直接在浏览器里改。

第一步:找到你要改的元素

操作 DOM 之前,先找到目标元素。白物集仪表盘里用这几种方式:

// 按 id 找文档定位元素——最快、最精确
const content = document.getElementById('dashboard-content');
const badge = document.getElementById('notif-badge');

// 按 CSS 选择器找——返回第一个匹配
const firstTab = document.querySelector('.dashboard-tab');

// 按 CSS 选择器找所有匹配——返回 NodeList
const tabs = document.querySelectorAll('.dashboard-tab');

getElementByIdquerySelector 快,但 querySelector 更灵活(支持任意 CSS 选择器)。日常写代码优先用 getElementById 找唯一元素,用 querySelectorAll 找同类元素集合。

第二步:读取和修改内容

找到元素后,可以读它的内容,也可以写。白物集的登录页面实现了一个 GitHub OAuth 回调处理:

// 读取内容
const hash = window.location.hash;
const params = new URLSearchParams(hash.replace('#', '?'));
const accessToken = params.get('access_token');

// 修改内容——textContent(安全,不解析 HTML)
const nick = document.getElementById('profile-nickname');
nick.textContent = d.user.nickname || '用户';

// 修改内容——innerHTML(渲染 HTML 结构)
content.innerHTML = '<div>加载中...</div>';

textContentinnerHTML 的区别:

  • textContent:纯文本,只改文字内容。即使用户输入 <script>alert('xss')</script>,它也只会当作文本显示
  • innerHTML:解析 HTML 标签,可以渲染复杂结构。但如果内容来自用户输入,有 XSS 风险

白物集的实践中,用户生成内容(评论、昵称)用 textContent 或专门的 escapeHtml() 函数处理,只有自己控制的模板字符串才用 innerHTML

第三步:改变样式

最常见的交互效果就是「点中某个元素、高亮它、其他恢复默认」。白物集仪表盘的 Tab 切换就是这么做的:

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    // 先全部恢复默认
    tabs.forEach(t => {
      t.style.color = 'var(--text-muted)';
      t.style.borderBottomColor = 'transparent';
      t.style.fontWeight = '400';
    });
    // 再高亮点中的
    tab.style.color = 'var(--text)';
    tab.style.borderBottomColor = 'var(--primary)';
    tab.style.fontWeight = '600';
  });
});

这种「先全部重置、再单独设置」的模式(也叫「排他思想」)在 Tab 切换、手风琴、菜单导航中非常常见。

也可以用 classList 替代直接操作 style——把样式写在 CSS 里,JS 只负责切换 class:

// CSS 里定义 .active 样式
element.classList.add('active');    // 增加
element.classList.remove('active'); // 删除
element.classList.toggle('active'); // 切换

用 class 比用 style 好维护:样式统一在 CSS 里改,JS 只做逻辑。

第四步:事件绑定

「用户点击了怎么办?」——用 addEventListener。白物集登录页面多处用到:

// 表单提交
document.getElementById('login-form')
  .addEventListener('submit', async (e) => {
    e.preventDefault();  // 阻止页面刷新
    const email = document.getElementById('email').value;
    // ... 发 API 请求
  });

// 按钮点击
const githubBtn = document.getElementById('github-login');
githubBtn.addEventListener('click', async (e) => {
  e.preventDefault();
  githubBtn.disabled = true;
  githubBtn.textContent = '⏳ 跳转中...';
  // ... 调 Supabase OAuth
});

几个要点:

  • e.preventDefault():阻止默认行为(表单不刷新页面、链接不跳转)
  • addEventListener 可以绑定多个处理函数,比 onclick 属性灵活
  • 事件处理函数里 this 指向绑定元素,但用箭头函数时继承外层作用域

事件委托

白物集仪表盘的封禁操作面板用了事件委托——不是给每个按钮单独绑定,而是在父元素上监听一次:

document.addEventListener('click', async (e) => {
  const btn = e.target.closest('.mod-action');
  if (!btn) return;

  const action = btn.dataset.action;
  if (action === 'delete-comment') {
    // 删除评论
    await fetch('/api/comments?id=' + btn.dataset.cid, { method: 'DELETE' });
    btn.closest('[style*="border-left:3px"]')?.remove();
  } else if (action === 'approve') {
    // 放行
    await fetch('/api/moderation', { method:'POST', ... });
    btn.closest('[style*="border-left:3px"]')?.remove();
  }
});

好处:动态生成的按钮不需要重新绑定事件,减少内存占用。

踩坑经验

1. innerHTML 重复渲染丢失状态

innerHTML 重新渲染一个区域时,这个区域里的所有事件绑定、输入框状态、焦点都会被清空——因为 DOM 节点被替换了,不是更新。

白物集仪表盘早期在切换 Tab 时直接用 innerHTML 替换整个内容区,导致 Tab 内的子元素事件需要重新绑定。解决方案是对不需要频繁切换的部分用 textContentappendChild 增量更新,而不是全量替换。

2. document.querySelector 返回 null

如果选择器没匹配到任何元素,返回的是 null,不是 undefined。调用 null.textContent 会报 TypeError

// 安全写法
const el = document.getElementById('notif-badge');
if (el) el.style.display = 'none';

// 或者用可选链
document.getElementById('notif-badge')?.style.display = 'none';

白物集里大量用 document.getElementById('xxx')?.addEventListener(...) 这种模式,因为某些元素在特定 Tab 下才渲染,直接绑定会报错。

3. Astro SSR 中 <script> 多标签合并

Astro 页面里如果用多个 <script> 标签,它们会被合并到同一个作用域。白物集仪表盘早期把 Tab 切换逻辑和加载逻辑写在不同的 <script> 里,结果变量互相覆盖。修正:把关联逻辑放在同一个 <script> 里,或者用 ES module 导出。


下一篇讲异步编程——fetch 怎么调接口、async/await 怎么处理错误、白物集的内容 API 是怎么串联起来的。

← shadcn/improve——强模型审计,弱模型执行 → Apple 频道 异步编程:Fetch API 与数据请求 →
🍎 Apple 深度分析
本文基于 Apple 公开资料及行业分析撰写。观点仅供参考与学习交流。