JavaScript Web APIs 深度解析

中等 🟡Js/Ts
7 个标签
预计阅读时间:36 分钟
JavaScriptWeb APIsDOMFetchStorageWeb WorkersPWA

JavaScript Web APIs 深度解析

Web APIs 是浏览器提供的一组接口,使 JavaScript 能够与浏览器和操作系统交互。

需要澄清一个常见误解:很多我们天天用的东西其实不属于 JavaScript 语言本身。`Array`、`Promise`、`Math` 属于 ECMAScript 语言规范;而 `document`、`fetch`、`localStorage`、`setTimeout` 都是浏览器额外注入的"超能力",统称 Web APIs。可以把 JavaScript 引擎想象成"大脑",Web APIs 就是浏览器给这个大脑装上的"手、眼、耳、记忆",让它能操作页面、发请求、存数据。

为什么要理解 Web APIs

现代前端框架(React、Vue)帮我们封装了大量 DOM 操作,但底层依然是 Web APIs。理解它们能帮你:

写出更高性能的代码(比如减少重排、用事件委托)
排查框架"魔法"失效时的问题
在不引入框架的场景(浏览器扩展、脚本、库开发)中游刃有余

DOM API

DOM(Document Object Model,文档对象模型)把 HTML 文档解析成一棵可用 JavaScript 操作的节点树。

选择元素:

javascriptCode
// 单个元素(返回第一个匹配,找不到返回 null)
const title = document.querySelector('.title');
const btn = document.querySelector('#submit');

// 多个元素(返回静态 NodeList)
const items = document.querySelectorAll('li.item');
items.forEach(item => console.log(item.textContent));

// 传统方法(性能略高,返回动态集合)
const byId = document.getElementById('app');
const byClass = document.getElementsByClassName('card');

创建与修改元素:

javascriptCode
// 创建
const div = document.createElement('div');
div.className = 'card';
div.textContent = 'Hello';

// 插入
document.body.appendChild(div);
document.body.insertBefore(div, document.body.firstChild);

// 现代插入 API
container.append(div, '文本节点');   // 可插入多个、支持字符串
container.prepend(div);
div.remove();                        // 直接移除自身

// 属性操作
div.setAttribute('data-id', '123');
div.getAttribute('data-id');
div.removeAttribute('data-id');
console.log(div.dataset.id);         // 通过 dataset 读取 data-*

事件处理与事件委托:

事件委托是性能优化的关键技巧:利用事件冒泡,把子元素的监听器"上移"到父元素,一个监听器管理成百上千个子元素。

javascriptCode
// 基础监听
button.addEventListener('click', (event) => {
  console.log('被点击的元素:', event.target);
  event.preventDefault();   // 阻止默认行为(如表单提交)
  event.stopPropagation();  // 阻止冒泡
});

// 事件委托:一个监听器管理整个列表
document.querySelector('#todo-list').addEventListener('click', (e) => {
  // 只有点到删除按钮才响应
  if (e.target.matches('.delete-btn')) {
    const item = e.target.closest('li');
    item.remove();
  }
});

DOM 遍历与样式:

javascriptCode
// 遍历
node.parentElement;
node.children;              // 只含元素节点
node.nextElementSibling;
node.previousElementSibling;

// 样式操作
el.style.color = 'red';
el.classList.add('active');
el.classList.toggle('open');
el.classList.contains('active');

// 读取计算后的最终样式(含 CSS 文件里的)
const computed = getComputedStyle(el);
console.log(computed.fontSize);

高性能 DOM 操作

DOM 操作慢,主要慢在"重排(reflow)"和"重绘(repaint)"。频繁增删会触发多次重排。用 DocumentFragment 可以把多次操作合并为一次。

javascriptCode
// 反例:循环里 1000 次 appendChild,触发大量重排
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  list.appendChild(li); // 每次都可能重排
}

// 正例:先在内存里的 fragment 组装,一次性插入
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
list.appendChild(fragment); // 只触发一次重排

实测中,插入 1 万个节点时,DocumentFragment 方案通常比逐个 appendChild 快 3~10 倍。

Fetch API

Fetch 是基于 Promise 的现代网络请求接口,取代了古老的 XMLHttpRequest。

javascriptCode
// GET 请求
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);

  // 关键坑:fetch 只在网络故障时 reject,
  // HTTP 4xx/5xx 不会抛错,必须手动检查 res.ok
  if (!res.ok) {
    throw new Error(`HTTP 错误:${res.status}`);
  }
  return res.json();
}

// POST 请求
async function createUser(data) {
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    credentials: 'include', // 携带 cookie
  });
  return res.json();
}

请求超时与取消(AbortController):

javascriptCode
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(url, { signal: controller.signal });
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error('请求超时');
    }
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

Fetch 请求方法与语义:

| 方法 | 语义 | 幂等 |

| --- | --- | --- |

| GET | 获取资源 | 是 |

| POST | 新建资源 | 否 |

| PUT | 整体替换资源 | 是 |

| PATCH | 部分更新资源 | 否 |

| DELETE | 删除资源 | 是 |

存储 API

浏览器提供多种客户端存储方案,选型要看数据大小、生命周期、是否需要索引查询。

javascriptCode
// localStorage:持久存储,关闭浏览器也不丢
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme');      // 'dark'
localStorage.removeItem('theme');

// 存对象要序列化
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alice' }));
const user = JSON.parse(localStorage.getItem('user'));

// sessionStorage:仅当前标签页会话有效,关标签页即清除
sessionStorage.setItem('draft', '临时草稿');

IndexedDB: 面向大量结构化数据的异步数据库,支持索引和事务。

javascriptCode
const request = indexedDB.open('MyDB', 1);

request.onupgradeneeded = (e) => {
  const db = e.target.result;
  const store = db.createObjectStore('users', { keyPath: 'id' });
  store.createIndex('byName', 'name', { unique: false });
};

request.onsuccess = (e) => {
  const db = e.target.result;
  const tx = db.transaction('users', 'readwrite');
  tx.objectStore('users').put({ id: 1, name: 'Alice' });
};

存储方案对比:

| 方案 | 容量 | 生命周期 | 数据类型 | 同步/异步 | 随请求发送 |

| --- | --- | --- | --- | --- | --- |

| Cookie | 约 4KB | 可设过期 | 字符串 | 同步 | 是(每次请求) |

| localStorage | 约 5-10MB | 永久 | 字符串 | 同步 | 否 |

| sessionStorage | 约 5-10MB | 标签页会话 | 字符串 | 同步 | 否 |

| IndexedDB | 数百 MB 以上 | 永久 | 结构化对象 | 异步 | 否 |

选型经验:小配置用 localStorage;临时状态用 sessionStorage;需要随请求带给服务端的凭证用 Cookie;大量离线数据用 IndexedDB。

其他常用 Web APIs

地理位置 API:

javascriptCode
navigator.geolocation.getCurrentPosition(
  (pos) => {
    console.log('纬度:', pos.coords.latitude);
    console.log('经度:', pos.coords.longitude);
  },
  (err) => console.error('定位失败:', err.message),
  { enableHighAccuracy: true, timeout: 5000 }
);

通知 API:

javascriptCode
async function notify(title, body) {
  if (Notification.permission !== 'granted') {
    const perm = await Notification.requestPermission();
    if (perm !== 'granted') return;
  }
  new Notification(title, { body, icon: '/icon.png' });
}

IntersectionObserver(图片懒加载利器):

比监听 scroll 事件性能高得多,是实现懒加载、无限滚动的现代标准做法。

javascriptCode
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 进入视口才加载真图
      observer.unobserve(img);
    }
  });
}, { rootMargin: '100px' });

document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));

Web Workers(后台线程):

JavaScript 主线程是单线程的,长时间计算会卡死界面。Web Worker 把重活丢到后台线程。

javascriptCode
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => {
  console.log('计算结果:', e.data);
};

// worker.js
self.onmessage = (e) => {
  const sum = e.data.numbers.reduce((a, b) => a + b, 0);
  self.postMessage(sum); // 算完发回主线程
};

Service Workers(离线缓存与 PWA 核心):

Service Worker 是一个运行在浏览器后台的网络代理,可拦截请求、提供离线缓存、推送通知。

javascriptCode
// 注册
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

// sw.js:缓存优先策略
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

WebSocket(实时双向通信):

javascriptCode
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 'lobby' }));
ws.onmessage = (e) => console.log('收到消息:', e.data);
ws.onclose = () => console.log('连接关闭');

真实案例:图片密集页面的加载优化

某新闻站首页有 60+ 张图片,首屏加载耗时 4.2 秒。优化方案:

1.用 IntersectionObserver 做懒加载,仅加载视口内图片
2.用 Service Worker 缓存静态资源
3.用 DocumentFragment 批量渲染文章列表

优化后首屏可交互时间从 4.2 秒降到 1.3 秒,首屏图片请求数从 60 降到 6,流量节省约 70%。

通信 API 对比

| 方案 | 方向 | 实时性 | 适用场景 |

| --- | --- | --- | --- |

| Fetch / XHR | 客户端发起 | 一次性 | 常规数据请求 |

| SSE (EventSource) | 服务端 → 客户端 | 高 | 消息推送、股价 |

| WebSocket | 双向 | 极高 | 聊天、游戏、协同 |

| 轮询 | 客户端发起 | 低 | 兼容性兜底 |

Server-Sent Events(SSE)

SSE 是"服务端单向推送"的轻量方案,基于普通 HTTP,比 WebSocket 简单得多,自带断线重连,适合消息通知、进度、股价、AI 流式输出等场景。

javascriptCode
// 客户端
const es = new EventSource('/api/stream');
es.onmessage = (e) => console.log('默认事件:', e.data);
es.addEventListener('price', (e) => {
  const data = JSON.parse(e.data);
  updatePrice(data);
});
es.onerror = () => console.log('连接中断,浏览器会自动重连');
// 用完记得关闭,否则会一直占用连接
// es.close();

// 服务端(Express)——注意特定的响应头和数据格式
app.get('/api/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });
  const timer = setInterval(() => {
    // 每条消息以 data: 开头,以两个换行结束
    res.write(`event: price\ndata: ${JSON.stringify({ v: Math.random() })}\n\n`);
  }, 1000);
  req.on('close', () => clearInterval(timer)); // 客户端断开要清理
});

SSE vs WebSocket 抉择: 只需服务端推、数据是文本、想省心用 SSE;需要双向实时、二进制、低延迟(游戏、协同编辑)用 WebSocket。

高级 Fetch:流式读取与进度

Fetch 的 `response.body` 是一个 ReadableStream,可以边下载边处理,实现大文件进度条或 AI 逐字输出效果。

javascriptCode
async function downloadWithProgress(url, onProgress) {
  const res = await fetch(url);
  const total = Number(res.headers.get('Content-Length')) || 0;
  const reader = res.body.getReader();
  let received = 0;
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    received += value.length;
    if (total) onProgress(received / total); // 0 到 1 的进度
  }
  return new Blob(chunks);
}

await downloadWithProgress('/big-file.zip', p => {
  console.log(`已下载 ${(p * 100).toFixed(1)}%`);
});

读取流式文本(如 AI 接口的 SSE/流式响应):

javascriptCode
const res = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ q }) });
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  appendToUI(value); // 逐块把文本追加到界面,实现打字机效果
}

请求缓存与去重

同一份数据被多个组件同时请求时,应做请求去重,避免重复网络往返。

javascriptCode
const inflight = new Map(); // 正在进行的请求缓存

function dedupedFetch(url) {
  if (inflight.has(url)) return inflight.get(url); // 复用进行中的 Promise
  const promise = fetch(url)
    .then(r => r.json())
    .finally(() => inflight.delete(url)); // 完成后清掉
  inflight.set(url, promise);
  return promise;
}

// 同一时刻发起 3 次,实际只有 1 个网络请求
const [a, b, c] = await Promise.all([
  dedupedFetch('/api/config'),
  dedupedFetch('/api/config'),
  dedupedFetch('/api/config'),
]);

MutationObserver:监听 DOM 变化

当你无法控制某段 DOM 何时被第三方脚本修改时,MutationObserver 可以异步监听 DOM 树变化,比早已废弃的 Mutation Events 性能好得多。

javascriptCode
const observer = new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (m.type === 'childList') {
      console.log('新增节点:', m.addedNodes.length);
    } else if (m.type === 'attributes') {
      console.log(`属性 ${m.attributeName} 变了`);
    }
  }
});

observer.observe(document.querySelector('#app'), {
  childList: true,   // 子节点增删
  attributes: true,  // 属性变化
  subtree: true,     // 包括所有后代
});
// 停止监听
// observer.disconnect();

ResizeObserver:监听元素尺寸

监听元素自身尺寸变化,比监听 window 的 resize 更精准(能感知非窗口引起的尺寸变化,如内容撑开、flex 重排),是响应式组件的利器。

javascriptCode
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`尺寸变为 ${width} × ${height}`);
    // 例如根据宽度切换布局
    entry.target.classList.toggle('compact', width < 400);
  }
});
ro.observe(document.querySelector('.chart-container'));

三大 Observer 对比

| API | 监听什么 | 典型用途 |

| --- | --- | --- |

| IntersectionObserver | 元素与视口/祖先的交叉状态 | 懒加载、无限滚动、曝光埋点 |

| MutationObserver | DOM 树结构与属性变化 | 监控第三方注入、富文本编辑器 |

| ResizeObserver | 元素尺寸变化 | 响应式组件、图表自适应 |

History API 与前端路由

单页应用(SPA)的前端路由本质就是用 History API 在不刷新页面的情况下改变 URL。

javascriptCode
// 压入新历史记录,URL 变化但页面不刷新
history.pushState({ page: 'about' }, '', '/about');
// 替换当前记录(不新增历史条目)
history.replaceState({ page: 'home' }, '', '/home');

// 监听前进/后退按钮
window.addEventListener('popstate', (e) => {
  console.log('导航到:', location.pathname, '状态:', e.state);
  renderRoute(location.pathname);
});

// 一个极简路由器
function navigate(path) {
  history.pushState({}, '', path);
  renderRoute(path);
}
document.addEventListener('click', (e) => {
  const link = e.target.closest('a[data-spa]');
  if (link) {
    e.preventDefault();
    navigate(link.getAttribute('href'));
  }
});

剪贴板、分享与文件

javascriptCode
// 现代剪贴板 API(需 HTTPS + 用户手势)
await navigator.clipboard.writeText('要复制的内容');
const text = await navigator.clipboard.readText();

// 原生分享(移动端调起系统分享面板)
if (navigator.share) {
  await navigator.share({ title: '标题', text: '描述', url: location.href });
}

// File API:读取用户选择的文件
input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  console.log(file.name, file.size, file.type);
  const text = await file.text();       // 读文本
  const buf = await file.arrayBuffer(); // 读二进制
  const url = URL.createObjectURL(file); // 生成预览 URL
  img.src = url;
  // 用完释放,避免内存泄漏
  // URL.revokeObjectURL(url);
});

Web Worker 进阶:结构化克隆与 Transferable

主线程与 Worker 之间 postMessage 传数据默认走结构化克隆(深拷贝),大数据会有拷贝开销。对 ArrayBuffer 等可转移对象,用 Transferable 做零拷贝转移,性能天差地别。

javascriptCode
// 普通传递:大 buffer 会被完整拷贝一份
worker.postMessage({ buffer });

// 零拷贝转移:所有权移交 Worker,主线程随后不能再用该 buffer
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64MB
worker.postMessage({ buffer }, [buffer]); // 第二个参数声明转移列表
// 转移后 buffer.byteLength === 0,转移 64MB 从数十毫秒降到接近 0

结构化克隆的限制: 函数、DOM 节点、Symbol 无法克隆,会抛错;但 Date、RegExp、Map、Set、ArrayBuffer、Blob 等可以。可用 `structuredClone()` 在主线程做深拷贝。

性能与用户体验相关 API

javascriptCode
// requestIdleCallback:在浏览器空闲时执行低优先级任务,不抢占关键渲染
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && tasks.length) {
    processTask(tasks.shift());
  }
});

// Page Visibility:标签页切到后台时暂停轮询/动画,省电省流量
document.addEventListener('visibilitychange', () => {
  if (document.hidden) stopPolling();
  else startPolling();
});

// navigator.sendBeacon:页面卸载时可靠地发送埋点,不会被打断
window.addEventListener('pagehide', () => {
  navigator.sendBeacon('/analytics', JSON.stringify({ event: 'leave' }));
});

Canvas 与 OffscreenCanvas

Canvas 提供像素级绘图能力,是图表、游戏、图像处理的基础。OffscreenCanvas 更可把绘制搬到 Worker,彻底不阻塞主线程。

javascriptCode
const canvas = document.querySelector('#chart');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#4f46e5';
ctx.fillRect(10, 10, 100, 50);
ctx.beginPath();
ctx.arc(200, 100, 40, 0, Math.PI * 2);
ctx.stroke();

// OffscreenCanvas:把渲染移交 Worker,主线程零负担
const offscreen = document.querySelector('#big').transferControlToOffscreen();
const worker = new Worker('render-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]); // 转移所有权

// render-worker.js
self.onmessage = (e) => {
  const ctx = e.data.canvas.getContext('2d');
  function draw() {
    ctx.clearRect(0, 0, 800, 600);
    // ...重绘逻辑,即使很耗时也不卡主线程
    requestAnimationFrame(draw);
  }
  draw();
};

BroadcastChannel:跨标签页通信

同源的多个标签页之间广播消息,常用于"一个页面登出,其他页面同步登出"这类场景。

javascriptCode
const channel = new BroadcastChannel('auth');
// A 标签页登出后广播
channel.postMessage({ type: 'logout' });

// 其他标签页监听并响应
channel.onmessage = (e) => {
  if (e.data.type === 'logout') {
    location.href = '/login';
  }
};

跨标签页通信的其他方案还有 `storage` 事件(localStorage 变化会通知其他同源标签页)和 SharedWorker(多标签共享一个 Worker 实例)。

拖放 API(Drag and Drop)

javascriptCode
// 可拖动元素
dragItem.draggable = true;
dragItem.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', dragItem.id);
  e.dataTransfer.effectAllowed = 'move';
});

// 放置区域
dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // 必须阻止默认行为,否则无法触发 drop
});
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const id = e.dataTransfer.getData('text/plain');
  dropZone.appendChild(document.getElementById(id));
});

// 拖拽上传文件
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const files = [...e.dataTransfer.files]; // 用户从桌面拖入的文件
  files.forEach(uploadFile);
});

网络与设备信息 API

javascriptCode
// Network Information:根据网速调整策略(如慢网络不自动播放视频)
const conn = navigator.connection;
if (conn) {
  console.log('网络类型:', conn.effectiveType); // '4g' | '3g' | '2g'
  console.log('省流量模式:', conn.saveData);
  if (conn.effectiveType === '2g' || conn.saveData) {
    loadLowQualityAssets(); // 弱网降级
  }
}

// 剪贴板粘贴图片、屏幕方向、震动等设备能力
navigator.vibrate?.([200, 100, 200]); // 移动端震动反馈

常见坑

fetch 不把 4xx/5xx 当错误:必须手动检查 `res.ok`。
localStorage 只能存字符串:存对象忘记 JSON 序列化会得到 `[object Object]`。
localStorage 是同步的:存大数据会阻塞主线程,大数据请用 IndexedDB。
DOM 操作触发重排:循环里逐个插入节点性能极差,用 DocumentFragment。
事件监听器不移除导致内存泄漏:SPA 中组件卸载要 `removeEventListener`。
Service Worker 缓存更新难:改版后老用户可能一直用旧缓存,需要版本化缓存名。

最佳实践

性能优化:

减少 DOM 操作,批量更新用 DocumentFragment
用事件委托代替海量监听器
懒加载用 IntersectionObserver 而非 scroll 监听
重计算丢给 Web Worker

兼容性:

用前先检测:`if ('geolocation' in navigator)`
提供降级方案,必要时引入 polyfill

安全性:

永远校验和转义用户输入,防止 XSS
敏感数据不要存 localStorage(易被 XSS 读取)
全站 HTTPS,Service Worker 也强制要求 HTTPS

代码组织:

把 API 调用封装成独立模块
统一错误处理与超时控制
兼顾可访问性(a11y)

总结

| 类别 | 代表 API | 核心用途 |

| --- | --- | --- |

| DOM | querySelector、classList、DocumentFragment | 操作页面结构与样式 |

| 网络 | Fetch、AbortController、WebSocket | 数据请求与实时通信 |

| 存储 | localStorage、sessionStorage、IndexedDB、Cookie | 客户端数据持久化 |

| 观察者 | IntersectionObserver、MutationObserver | 高性能监听变化 |

| 并发/离线 | Web Worker、Service Worker | 后台计算与离线能力 |

| 设备 | Geolocation、Notification、MediaDevices | 访问系统能力 |

一句话:框架帮你隐藏了 Web APIs,但真正遇到性能瓶颈和疑难杂症时,能救你的正是对底层 Web APIs 的理解。

实际应用场景

前端应用: 表单处理、数据可视化、实时通信、离线功能。

交互体验: 拖拽、键盘导航、触摸事件、动画。

工具开发: 浏览器扩展、开发者工具、性能分析工具。

PWA(渐进式 Web 应用): 离线访问、安装到主屏幕、推送通知、后台同步。