一、概述
Chrome(基于开源项目 Chromium)是由 Google 开发的跨平台 Web 浏览器。其核心设计围绕三大原则展开:
- 安全性 — 多进程架构 + 沙箱隔离,每个站点独立运行
- 稳定性 — 单个页面/插件崩溃不影响其他标签页
- 性能 — GPU 加速合成、JIT 编译、多线程并行
Chrome 的渲染引擎 Blink 于 2013 年 4 月从 Apple 主导的 WebKit 项目分叉而来。此前 Chrome(自 2008 年发布起)一直使用 WebKit 作为渲染引擎,与 Safari 共享大量代码。分叉的核心原因是 Chrome 的多进程架构与 WebKit 的单进程设计在技术上难以调和,Google 希望独立发展更高性能的渲染管线。
Chrome 代码库规模庞大(约 3000 万行 C++ 代码),主要子系统包括:
| 子系统 | 功能 | 核心代码路径 |
|---|---|---|
| Blink | HTML/CSS 渲染引擎 | third_party/blink/ |
| V8 | JavaScript/WebAssembly 引擎 | v8/ |
| cc | Chrome Compositor(合成器) | cc/ |
| Mojo | 跨进程 IPC 框架 | mojo/ |
| net | 网络栈(HTTP/3, TCP, TLS) | net/ |
| Skia | 2D 图形库 | third_party/skia/ |
| ANGLE | OpenGL ES → D3D/Vulkan 翻译层 | third_party/angle/ |
| //content | 多进程嵌入层(Content API) | content/ |
| //chrome | Chrome 浏览器 UI + 功能 | chrome/ |
用例图:浏览器核心功能
从用户角度,Chrome 浏览器主要提供以下核心功能。
二、渲染管线
完整渲染流程(从 HTML 到像素)
渲染管线分为两个阶段:Paint(在 Main Thread 完成,产出 SkPicture 显示列表)和 Composite(在 Compositor Thread 完成,产出 GL 命令发给 GPU)。Tiling(分块)策略确保只对可见区域进行栅格化。
三、GPU 加速合成
Chrome 的合成器(cc)是管理 GraphicsLayer 树和协调帧生命周期的软件库。自 Chrome 引入 Impl-side Painting 后,合成器运行在独立的 Compositor Thread,可以独立于 Main Thread 响应滚动和动画。
关键机制:
- Commit:Main Thread 的 LayerTree 同步到 Compositor Thread(含新 SkPicture 录制内容),此时 Main Thread 短暂阻塞
- Pending Tree → Active Tree:新树在后台完成栅格化后才激活显示,避免用户看到未完成的内容
- Tile Prioritization:根据距视口距离和屏幕可见时间对 Tile 排序,优先分配 GPU 内存
- Rasterization:软件光栅化(Skia CPU → Bitmap → Upload Texture)或 GPU 光栅化(Ganesh → 直接写入纹理)
- Command Buffer:Renderer 的 GL 命令序列化后写入共享内存环形缓冲区,GPU Process 读取并执行
- Mailbox + Sync Points:跨 Command Buffer 共享纹理(如 WebGL FBO 传给合成器),通过 Sync Point 实现非阻塞同步
GPU Process 架构带来四大收益:安全(沙箱隔离)、鲁棒(GPU 崩溃不拖垮浏览器)、统一(所有平台统一用 GL ES 2.0)、并行(CPU 与 GPU 流水线工作)。
四、V8 JavaScript 引擎
V8 是 Google 开源的 C++ 高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。采用多层编译策略,在启动速度与峰值性能间取得平衡。
编译层级说明:
| 层级 | 编译器 | 产出 | 特点 |
|---|---|---|---|
| 1 | Parser | AST | 词法/语法分析 |
| 2 | Sparkplug | 非优化机器码 | 快速基线编译,无需类型反馈 |
| 3 | Ignition | Bytecode(字节码) | 解释器,启动快,收集类型反馈 |
| 4 | Maglev | 中等优化机器码 | 2023 年新增,介于 Ignition 和 TurboFan 之间 |
| 5 | TurboFan | 高度优化机器码 | 针对热点函数做推测优化(Speculative Optimization) |
如果推测失败(类型变化),TurboFan 编译的代码会 Deoptimize 回退到 Ignition 字节码执行,重新收集类型信息后再优化。
V8 的 Orinoco 垃圾回收器采用分代(Generational)、并发(Concurrent)、并行(Parallel)设计,极大减少了 Stop-the-World 停顿时间。
五、网络栈
Chrome 的所有网络通信都由 Browser 进程处理。这确保浏览器可以全局控制网络访问、维持 HTTP 连接池、统一管理 Cookie/缓存。Renderer 进程通过 IPC 发送资源请求给 Browser 的 ResourceDispatcherHost,后者通过 URLRequest 执行真正的 HTTP 请求。
关键设计点:
- Browser 的 IO Thread 专门处理 IPC 和网络请求,不阻塞 UI Thread
ResourceMessageFilter在 IO Thread 上直接拦截并处理资源请求消息,无需绕到 UI Thread- Renderer 使用全局单例
ResourceDispatcher生成唯一 RequestID,用于将响应路由回对应的RequestPeer CookieMonster(位于net/base)统一管理所有 Cookie,document.cookie访问通过同步 IPC 实现- Chrome 使用独立的网络栈(不共享操作系统栈如 WinINET),确保跨平台一致性
六、多进程架构
6.1 架构总览图
Chrome 采用多进程架构:每个标签页、扩展、插件都在独立进程中运行,进程间通过 Mojo IPC 框架通信。GPU 进程通过命令缓冲区(Command Buffer,位于共享内存)接收 Renderer 的 GL 命令。
6.2 Browser 进程内部架构
Browser 进程是系统的"大脑",运行 UI 线程和 IO 线程。每个标签页对应一个 WebContents 对象,它管理 RenderFrameHost(跨进程与 Renderer 通信)和若干 Tab Helper(如 Favicon、Infobar 等)。
6.3 进程类型总结
| 进程类型 | 职责 | 数量 |
|---|---|---|
| Browser | UI、网络、存储、权限管理、进程调度 | 1 个 |
| Renderer | 解析 HTML/CSS、执行 JS、排版绘制 | 每站点 1 个(Site Isolation) |
| GPU | 执行 GL/Vulkan/D3D 命令、交换缓冲区 | 1 个 |
| Utility | 音频解码、数据解析等耗时任务 | 按需 |
| Plugin | 运行 PPAPI 插件(已废弃) | 按需 |
| Extension | 运行扩展 Service Worker | 每个扩展 1 个 |
| Network Service | 独立的网络进程(Mojo 化后的网络栈) | 1 个 |
6.4 核心类层次(Content API)
Content API 是连接 Browser 进程 UI 与底层 Blink/V8 渲染引擎的关键抽象层。以下类图展示其核心 C++ 类的继承与组合关系。
七、模块依赖
源码模块依赖关系
Chromium 源码树各模块之间有严格的依赖层级。下层为上层提供基础能力,避免循环依赖。
八、Blink 四棵并行树
树结构与映射关系
Blink 内部维护四棵并行的树结构,逐步从 DOM 转换到可供合成器(cc)使用的 GraphicsLayer:
- DOM Tree:HTML 解析后的节点树
- RenderObject Tree:与可见 DOM 节点一一对应,知道如何绘制
- RenderLayer Tree:按 z-order 组织,处理层叠关系
- GraphicsLayer Tree:合成层,每个对应一个 GPU 纹理或共享内存 Bitmap
合成触发条件(满足以下任一条件,RenderLayer 会提升为合成层):
- 3D transform 或 perspective CSS 属性
- 硬件加速
<video> - WebGL 或加速 2D
<canvas> - CSS animation(opacity/transform)
- CSS filter
- 与合成层重叠(Layer Squashing 技术会合并部分重叠层以减少合成层数量)
九、Site Isolation
Site Isolation 是 Chrome 最核心的安全架构之一。它将不同站点(scheme + registered domain)放入不同 Renderer 进程,利用操作系统进程边界隔离攻击面。自 Chrome 67 起桌面端默认启用,Chrome 77 起 Android 端(2GB+ RAM 设备)启用。
Site 定义:scheme + registered domain(含 public suffix),忽略子域名、端口和路径。例如 a.example.com 和 b.example.com 属于同一个 site。
核心机制:
- OOPIF(Out-of-Process iframes):跨站 iframe 在独立 Renderer 中渲染
- Process Lock:Browser 进程为每个 Renderer 锁定其允许访问的 site
- CORB(Cross-Origin Read Blocking):Browser 进程阻止跨站 HTML/XML/JSON 数据进入 Renderer
- Browser Process Enforcements:Browser 端验证权限、Cookie、存储访问
Site Isolation 防御的威胁:
- Spectre 等推测执行侧信道攻击
- UXSS(通用跨站脚本)漏洞
- Renderer 进程被完全入侵后的跨站数据窃取
- 跨站 Cookie/密码/存储数据泄露
- 权限滥用(被入侵的 Renderer 无法使用其他站点的权限)
十、扩展系统
Chrome 扩展系统基于 Manifest V3 架构,核心组件包括 Service Worker(后台事件处理)、Content Script(注入页面操作 DOM)、以及丰富的 chrome.* API。
Extension 可实现的浏览器控制能力:
- UI 定制:Toolbar Action、Side Panel、Context Menu、Omnibox 建议
- 页面操控:Content Script 注入、Tab/Window 管理、网络请求拦截
- 浏览器控制:书签、历史、下载、通知、设置覆盖、DevTools 扩展
- 认证集成:OAuth 2.0(
chrome.identityAPI) - 原生通信:Native Messaging 与本地应用交换数据
十一、安全沙箱
Chrome 的安全模型基于最小权限原则:每个进程类型只被授予完成其职责所需的最小系统调用集。底层使用 Linux seccomp-bpf / Windows Restricted Token / macOS Sandbox 实现。
各进程沙箱策略:
| 进程 | 允许的操作 | 禁止的操作 |
|---|---|---|
| Renderer | IPC 通信、有限地址空间 | 直接文件/网络/设备访问 |
| GPU | GPU API 调用(GL/Vulkan/D3D) | 文件系统、网络 |
| Network | socket 系统调用 | 文件系统、显示设备 |
| Browser | 全部系统调用 | 无(最高权限) |
十二、部署架构
各进程以独立沙箱形式部署在操作系统上,通过 Mojo IPC 和共享内存通信。
十三、总结
Chrome 是一个高度复杂的分层、多进程、多线程系统。其架构可归纳为以下核心层次:
| 层次 | 组件 | 职责 |
|---|---|---|
| UI 层 | Views / Aura / Ash | 浏览器窗口管理、标签栏、书签栏 |
| Content API 层 | WebContents / RenderViewHost | 多进程嵌入抽象,隔离 chrome/ 和 content/ |
| IPC 层 | Mojo / Legacy IPC | 跨进程通信,Channel + ChannelProxy 线程代理 |
| 渲染层 | Blink / V8 / Skia | HTML/CSS 解析、JS 执行、2D 绑定 |
| 合成层 | cc | 图层管理、帧调度、Tile 化、GPU 合成 |
| 平台适配层 | ANGLE / Skia / GL ES | 跨平台 3D API 抽象 |
| 安全层 | Sandbox / Site Isolation | 进程隔离、系统调用过滤、跨站防护 |
Chrome 的架构演进始终围绕安全、稳定、性能三原则持续迭代。理解其多进程模型、Blink 渲染管线、V8 编译策略、cc 合成器、网络栈和安全沙箱,是深入理解现代浏览器工作原理的关键。
十四、Browser Web API 速查手册
1. DOM API
1.1 节点查询
| API | 说明 |
|---|---|
document.getElementById('id') | 按 ID,返回单个 Element 或 null |
document.getElementsByClassName('c') | 按 class,返回动态 HTMLCollection |
document.getElementsByTagName('div') | 按标签,返回动态 HTMLCollection |
document.getElementsByName('n') | 按 name 属性,返回 NodeList |
document.querySelector('.c') | CSS 选择器,返回第一个匹配 Element |
document.querySelectorAll('.c') | CSS 选择器,返回静态 NodeList |
el.closest('.wrapper') | 向上查找最近的匹配祖先 |
el.matches('.cls') | 是否匹配某选择器 (布尔) |
// querySelectorAll 返回的 NodeList 是静态快照,之后 DOM 变化不影响它 // getElementsBy* 返回的是动态集合,随 DOM 变化自动更新
1.2 节点遍历
| 属性 (Element) | 属性 (Node) | 说明 |
|---|---|---|
el.children | el.childNodes | 子元素 / 所有子节点(含文本注释) |
el.firstElementChild | el.firstChild | 第一个子 |
el.lastElementChild | el.lastChild | 最后一个子 |
el.previousElementSibling | el.previousSibling | 前一个兄弟 |
el.nextElementSibling | el.nextSibling | 后一个兄弟 |
el.parentElement | el.parentNode | 父元素/父节点 |
// 推荐用 Element 版本 — 跳过文本节点和注释 el.children; // HTMLCollection of Element el.childNodes; // NodeList (包含 #text, 注释节点)
1.3 节点增删改
| API | 说明 |
|---|---|
document.createElement('div') | 创建元素 |
document.createTextNode('text') | 创建文本节点 |
document.createDocumentFragment() | 创建 Fragment (批量插入用) |
parent.appendChild(child) | 末尾插入,返回插入的节点 |
parent.append(...nodes) | 末尾插入 (支持多个 + 字符串) 现代 |
parent.prepend(...nodes) | 开头插入 现代 |
parent.insertBefore(new, ref) | 在 ref 前插入 (ref 须是 parent 的子节点) |
ref.before(...nodes) | 在 ref 前插入兄弟节点 现代 |
ref.after(...nodes) | 在 ref 后插入兄弟节点 现代 |
child.replaceWith(...nodes) | 替换 现代 |
child.remove() | 自移除 现代 |
parent.removeChild(child) | 父移除子 |
parent.replaceChild(new, old) | 父替换子 |
el.cloneNode(true) | 克隆; true=深克隆, false=浅克隆 |
// Fragment 批量插入 — 减少回流
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
frag.appendChild(createItem(i));
}
list.appendChild(frag); // 一次性插入
1.4 属性 & 内容 & 样式
| 分类 | API | 说明 |
|---|---|---|
| HTML 属性 | el.getAttribute('href') | 读取原始属性值 |
| HTML 属性 | el.setAttribute('href', '/') | 设置属性 |
| HTML 属性 | el.removeAttribute('href') | 删除属性 |
| HTML 属性 | el.hasAttribute('href') | 是否存在 |
| Data 属性 | el.dataset.key | 读写 data-key="val" |
| 内容 | el.innerHTML | HTML 内容 (有 XSS 风险) |
| 内容 | el.outerHTML | 包含自身标签的 HTML |
| 内容 | el.textContent | 纯文本,保留空格换行 |
| 内容 | el.innerText | 纯文本,触发 reflow,忽略隐藏元素 |
| 内容 | el.insertAdjacentHTML(pos, html) | 比 innerHTML 快; pos: beforebegin/afterbegin/beforeend/afterend |
| class | el.classList.add/remove/toggle('c') | toggle 可传第二个参数 force |
| class | el.classList.contains('c') | 是否包含 |
| class | el.classList.replace('a','b') | 替换 |
| style | el.style.prop = 'val' | 设置内联样式 (驼峰命名: backgroundColor) |
| style | el.style.cssText = 'color:red;' | 批量设置内联样式 |
| style | getComputedStyle(el) | 读取最终计算样式 (含 CSS 层叠结果) |
| 尺寸 | el.offsetWidth / offsetHeight | 含 border + padding |
| 尺寸 | el.clientWidth / clientHeight | 含 padding,不含 border + 滚动条 |
| 尺寸 | el.scrollWidth / scrollHeight | 内容总尺寸 (含溢出部分) |
| 尺寸 | el.getBoundingClientRect() | 返回 {top, right, bottom, left, width, height, x, y} (相对视口) |
2. Event / 事件
2.1 事件绑定
| 方式 | 代码 | 特点 |
|---|---|---|
| addEventListener | el.addEventListener('click', fn) | 最推荐,支持多个监听器 |
| once | el.addEventListener('click', fn, { once: true }) | 触发一次后自动解绑 |
| passive | el.addEventListener('scroll', fn, { passive: true }) | 承诺不 preventDefault,提升滚动性能 |
| signal | el.addEventListener('click', fn, { signal: ctrl.signal }) | 通过 AbortController 批量解绑 |
| 解绑 | el.removeEventListener('click', fn) | 必须传同一个函数引用 |
| on 属性 | el.onclick = fn | 只能绑一个,覆盖旧的 |
// signal 批量解绑
const ctrl = new AbortController();
el1.addEventListener('click', fn1, { signal: ctrl.signal });
el2.addEventListener('click', fn2, { signal: ctrl.signal });
ctrl.abort(); // 一次解绑全部
2.2 事件对象
| 属性/方法 | 说明 |
|---|---|
event.target | 触发事件的元素 (点击的最深层元素) |
event.currentTarget | 绑定监听器的元素 (= this, 箭头函数除外) |
event.type | 事件类型: 'click', 'keydown'... |
event.preventDefault() | 阻止默认行为 (链接跳转、表单提交) |
event.stopPropagation() | 阻止冒泡 (不阻止当前元素的其他监听器) |
event.stopImmediatePropagation() | 阻止冒泡 + 阻止当前元素剩余监听器 |
event.eventPhase | 1=捕获, 2=目标, 3=冒泡 |
event.composedPath() | 返回事件经过的完整路径数组 |
2.3 常用事件类型速查
| 分类 | 事件 |
|---|---|
| 鼠标 | click dblclick mousedown mouseup mousemove mouseenter mouseleave mouseover mouseout contextmenu wheel |
| 键盘 | keydown keyup (通过 e.key e.code e.ctrlKey 获取) |
| 表单 | submit change input focus blur focusin focusout reset invalid |
| 文档/窗口 | DOMContentLoaded load beforeunload unload scroll resize error hashchange popstate |
| 剪贴板 | copy cut paste |
| 拖拽 | dragstart drag dragend dragenter dragover dragleave drop |
| 触摸 | touchstart touchmove touchend touchcancel |
| 指针 | pointerdown pointermove pointerup pointercancel |
| 动画 | animationstart animationend animationiteration transitionend |
事件委托
// 父元素监听,e.target 判断来源 — 避免给无数子元素各自绑事件
list.addEventListener('click', (e) => {
const btn = e.target.closest('.btn'); // 向上查找 .btn
if (!btn) return;
console.log('clicked:', btn.dataset.id);
});
自定义事件
// 派发
el.dispatchEvent(new CustomEvent('item-selected', {
detail: { id: 42, name: 'foo' },
bubbles: true, // 是否冒泡
cancelable: true, // 能否 preventDefault
}));
// 监听
el.addEventListener('item-selected', (e) => console.log(e.detail.id));
3. Fetch / 网络
3.1 fetch() 基础
| 操作 | 代码 |
|---|---|
| GET | fetch('/api/data').then(r => r.json()) |
| POST JSON | fetch('/api', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) }) |
| POST FormData | fetch('/upload', { method:'POST', body:new FormData(form) }) — 自动 multipart |
| 模 式 | mode: 'cors' | 'no-cors' | 'same-origin' |
| 凭据 | credentials: 'include' | 'same-origin' | 'omit' |
| 取消 | fetch(url, { signal: AbortController.signal }) |
// 标准 async/await 封装
async function request(url, options = {}) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
}
3.2 Response 对象
| 属性/方法 | 说明 |
|---|---|
res.ok | 布尔, 200-299 为 true |
res.status / res.statusText | HTTP 状态码 / 描述 |
res.headers | Headers 对象, get/set/has/forEach |
res.json() | 解析 JSON body → Promise |
res.text() | 读取为文本 |
res.blob() | 读取为 Blob |
res.arrayBuffer() | 读取为 ArrayBuffer |
res.formData() | 解析为 FormData |
res.body | ReadableStream, 可流式读取 |
res.clone() | 克隆响应 (body 只能读一次) |
res.redirected | 是否被重定向 |
res.url | 最终 URL (经过重定向后) |
3.3 其他网络 API
| API | 说明 |
|---|---|
navigator.sendBeacon(url, data) | 发送单向数据 (离开页面时仍可靠), 用于埋点/分析 |
XMLHttpRequest | 传统 AJAX, 可监听进度: xhr.upload.onprogress |
navigator.onLine | 是否联网 (不精确) |
online / offline 事件 | 网络状态变化时触发 |
4. WebSocket & SSE
WebSocket
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => console.log('connected');
ws.onmessage = (e) => console.log('received:', e.data);
ws.onerror = (e) => console.error(e);
ws.onclose = (e) => console.log('closed:', e.code, e.reason);
ws.send(JSON.stringify({ type: 'ping' }));
ws.close(1000, 'done'); // code: 1000=正常, 1001=离开
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
| 属性/方法 | 说明 |
|---|---|
ws.bufferedAmount | 队列中待发送的字节数 |
ws.binaryType | 'blob' (默认) 或 'arraybuffer' |
ws.protocol | 协商后的子协议 |
Server-Sent Events (SSE)
const es = new EventSource('/events');
es.onmessage = (e) => console.log(e.data); // 默认事件
es.addEventListener('custom', (e) => console.log(e.data));
es.onerror = (e) => {
if (es.readyState === EventSource.CLOSED) console.log('closed');
};
es.close();
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSED
| vs WebSocket | SSE | WebSocket |
|---|---|---|
| 方向 | 仅服务器 → 客户端 | 双向 |
| 协议 | HTTP(S) | ws:// / wss:// |
| 自动重连 | 内置 | 需手动实现 |
| 二进制 | 仅文本 | 支持 |
5. Storage / 存储
| 类型 | 容量 | 持久性 | 作用域 | 操作 |
|---|---|---|---|---|
| localStorage | ~5MB | 永久 | 同源 | 同步 API: setItem/getItem/removeItem/clearlocalStorage.length, localStorage.key(n) |
| sessionStorage | ~5MB | 标签页关闭即清 | 同源同标签 | API 同 localStorage |
| cookie | ~4KB | 可设过期 | 同源 + 路径 | document.cookie (读写均为字符串)document.cookie = 'k=v; max-age=3600; path=/; SameSite=Lax' |
| IndexedDB | > 100MB | 永久 | 同源 | 异步事务式 NoSQL; 使用 idb 库简化:idb.openDB(name, v, upgradeCb) |
| Cache API | 弹性 | 永久 | 同源 | Service Worker 缓存:caches.open('v1').then(c => c.put(req, res)) |
IndexedDB 速写
// 原生 IndexedDB 比较繁琐,推荐使用 idb 库: https://github.com/jakearchibald/idb
// 下面是原生速写:
function openDB(name, version, upgrade) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onupgradeneeded = (e) => upgrade(e.target.result); // 只有版本变化触发
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function dbPut(db, storeName, value) {
const tx = db.transaction(storeName, 'readwrite');
tx.objectStore(storeName).put(value);
await tx.done; // 等待事务完成
}
Storage 事件
// 同一源的其他 标签页修改 localStorage 时触发
window.addEventListener('storage', (e) => {
console.log(e.key, e.oldValue, e.newValue, e.url);
});
6. Timer & Animation
| API | 说明 | 精度 |
|---|---|---|
setTimeout(fn, ms) | 延迟执行一次 | ~4ms (嵌套 >5 层) |
setInterval(fn, ms) | 重复执行 | ~4ms |
clearTimeout / clearInterval | 取消定时器 | |
requestAnimationFrame(fn) | 下一帧渲染前执行 (60Hz → ~16.7ms) | 高精度 |
cancelAnimationFrame(id) | 取消 rAF | |
requestIdleCallback(fn, { timeout }) | 浏览器空闲时执行; 返回 { didTimeout, timeRemaining() } | 不固定 |
// rAF 动画循环模板
function animate(timestamp) {
// timestamp 是 DOMHighResTimeStamp
box.style.transform = `translateX(${Math.sin(timestamp * 0.001) * 100}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// 闲时执行非关键任务
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length) {
tasks.shift()();
}
}, { timeout: 2000 });
7. Observer API
7.1 IntersectionObserver (可见性检测)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
img.src = img.dataset.src; // 懒加载
observer.unobserve(img);
}
// entry.intersectionRatio: 0~1
// entry.boundingClientRect / rootBounds / intersectionRect
});
}, {
root: null, // null = 视口
rootMargin: '0px',
threshold: 0.1, // 0~1, 或 [0, 0.25, 0.5, 0.75, 1]
});
observer.observe(el);
observer.unobserve(el);
observer.disconnect(); // 停止所有观察
// 常用场景: 懒加载、无限滚动、曝光埋点
7.2 MutationObserver (DOM 变化检测)
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => {
console.log(m.type); // 'attributes' | 'characterData' | 'childList'
console.log(m.target, m.addedNodes, m.removedNodes);
console.log(m.attributeName, m.oldValue);
});
});
observer.observe(el, {
childList: true, // 子节点增删
attributes: true, // 属性变化
characterData: true, // 文本变化
subtree: true, // 监听所有后代
attributeOldValue: true, // 记录旧属性值
attributeFilter: ['class', 'style'], // 只监听这些属性
});
observer.disconnect();
// 常用场景: 富文本编辑器、动态内容监控
7.3 ResizeObserver (元素尺寸变化)
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`Element resized: ${width}x${height}`);
// entry.contentBoxSize: [{ blockSize, inlineSize }]
// entry.borderBoxSize / devicePixelContentBoxSize
});
});
observer.observe(el);
observer.unobserve(el);
observer.disconnect();
// 注意: 会在初始化时触发一次 (当前尺寸)
// 比 window.resize 精细: 监听单个元素而非整个视口
8. File & Blob
| API | 说明 |
|---|---|
new Blob(parts, { type }) | 二进制大对象: new Blob([jsonStr], { type:'application/json' }) |
blob.slice(start, end, type) | 切片 |
blob.text() / .arrayBuffer() / .stream() | 读取内容 (异步) |
URL.createObjectURL(blob) | 生成 blob 临时 URL (用完需 revoke) |
URL.revokeObjectURL(url) | 释放内存 |
File — 继承自 Blob
// <input type="file"> → input.files (FileList)
const file = input.files[0];
console.log(file.name, file.size, file.type, file.lastModified);
// FileReader: 读文件内容
const reader = new FileReader();
reader.onload = (e) => console.log(e.target.result); // base64/text
reader.onerror = (e) => console.error(e);
reader.readAsDataURL(file); // → data:image/png;base64,...
reader.readAsText(file); // → 文本
reader.readAsArrayBuffer(file); // → ArrayBuffer
// 下载文件
function download(blob, filename) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
9. Canvas 2D
| 步骤 | 代码 |
|---|---|
| 获取上下文 | const ctx = canvas.getContext('2d'); |
| 设置尺寸 | canvas.width = 800; canvas.height = 600; (不同于 CSS 尺寸) |
基础绘图
// 矩形 ctx.fillStyle = '#4285F4'; ctx.fillRect(10, 10, 100, 60); ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.strokeRect(10, 10, 100, 60); ctx.clearRect(20, 20, 80, 40); // 路径 ctx.beginPath(); ctx.moveTo(50, 50); ctx.lineTo(150, 50); ctx.lineTo(100, 100); ctx.closePath(); ctx.fill(); // 或 ctx.stroke() // 圆 / 弧 ctx.beginPath(); ctx.arc(100, 100, 50, 0, Math.PI * 2); ctx.fill(); // 圆角矩形 ctx.beginPath(); ctx.roundRect(10, 10, 100, 60, 8); ctx.fill();
文字
ctx.font = '24px sans-serif';
ctx.fillStyle = '#333';
ctx.textAlign = 'center'; // left/right/center/start/end
ctx.textBaseline = 'middle'; // top/bottom/middle/alphabetic/hanging
ctx.fillText('Hello Canvas', 100, 100);
// measureText: 预测量文本宽度
const metrics = ctx.measureText('Hello');
console.log(metrics.width);
变换 & 状态
ctx.save(); // 保存当前状态 ctx.translate(100, 100); ctx.rotate(Math.PI / 4); // 弧度 ctx.scale(2, 1); ctx.globalAlpha = 0.5; // 全局透明度 ctx.drawImage(img, 0, 0, 100, 100); // 绘制图片 ctx.restore(); // 恢复
图像处理
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// imageData.data: Uint8ClampedArray [R, G, B, A, R, G, B, A, ...]
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] = 255 - imageData.data[i]; // R 反色
imageData.data[i + 1] = 255 - imageData.data[i + 1]; // G
imageData.data[i + 2] = 255 - imageData.data[i + 2]; // B
}
ctx.putImageData(imageData, 0, 0);
10. Media & WebRTC
getUserMedia (摄像头/麦克风)
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720, facingMode: 'user' },
audio: true,
});
video.srcObject = stream;
// 枚举设备
const devices = await navigator.mediaDevices.enumerateDevices();
// 截帧到 canvas
canvas.getContext('2d').drawImage(video, 0, 0);
} catch (err) {
console.error('Permission denied or no device:', err);
}
屏幕共享
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: 'always' },
audio: false,
});
stream.getVideoTracks()[0].addEventListener('ended', () => {
console.log('User stopped sharing');
});
媒体录制 (MediaRecorder)
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
// play / download
};
recorder.start(); // 开始录制
recorder.pause(); // 暂停
recorder.resume(); // 恢复
recorder.stop(); // 停止 → onstop
Web Audio API 速写
const ctx = new AudioContext();
// 播放音频文件
const response = await fetch('sound.mp3');
const buffer = await ctx.decodeAudioData(await response.arrayBuffer());
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination); // → 扬声器
source.start();
// 振荡器
const osc = ctx.createOscillator();
osc.type = 'sine'; // sine/square/sawtooth/triangle
osc.frequency.value = 440; // Hz
osc.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 1); // 1 秒后停止
11. Web Workers
| 类型 | 说明 | 共享上下文 |
|---|---|---|
Worker (Dedicated) | 独立线程,只有创建者能通信 | 无 |
SharedWorker | 同源多页面共享 | 多个 tab/page |
ServiceWorker | 网络代理层,拦截请求、离线缓存、推送 | 注册范围内的所有页面 |
Dedicated Worker
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'compute', data: hugeArray });
worker.onmessage = (e) => console.log('result:', e.data);
worker.onerror = (e) => console.error(e.message, e.filename, e.lineno);
worker.terminate();
// worker.js
self.onmessage = (e) => {
const result = heavyCompute(e.data.data);
self.postMessage(result);
};
// Worker 也能使用 importScripts
importScripts('lib1.js', 'lib2.js');
Service Worker (注册 & 生命周期)
// main.js — 注册
if ('serviceWorker' in navigator) {
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
reg.onupdatefound = () => {
const installing = reg.installing;
installing.onstatechange = () => {
if (installing.state === 'installed') {
if (navigator.serviceWorker.controller) {
console.log('New SW available, refresh to update');
}
}
};
};
}
// sw.js — 缓存策略 (Cache First)
self.addEventListener('install', (e) => {
e.waitUntil(caches.open('v1').then(c => c.addAll(['/', '/app.js', '/style.css'])));
});
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached => cached || fetch(e.request))
);
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== 'v1').map(k => caches.delete(k)))
)
);
});
12. 跨上下文通信
postMessage (iframe / window.open)
// 发送 (父页面 → iframe)
iframe.contentWindow.postMessage({ type: 'greeting', text: 'hello' }, 'https://trusted-origin.com');
// 接收
window.addEventListener('message', (e) => {
if (e.origin !== 'https://trusted-origin.com') return; // 务必校验 origin!
console.log(e.data, e.source);
e.source.postMessage('got it!', e.origin);
});
BroadcastChannel (同源广播)
// 同源页面间广播, 比 storage 事件更灵活
const channel = new BroadcastChannel('my-channel');
channel.postMessage({ type: 'logout' }); // 发送
channel.onmessage = (e) => { // 接收
console.log(e.data);
};
channel.close(); // 关闭
SharedWorker
// main.js
const worker = new SharedWorker('shared.js');
worker.port.start();
worker.port.postMessage('hello');
worker.port.onmessage = (e) => console.log(e.data);
// shared.js
const ports = [];
self.onconnect = (e) => {
const port = e.ports[0];
ports.push(port);
port.onmessage = (e) => console.log(e.data);
};
13. URL & 编码
URL 构造函数
const url = new URL('https://example.com:8080/page?a=1&b=2#hash');
url.protocol // 'https:'
url.host // 'example.com:8080'
url.hostname // 'example.com'
url.port // '8080'
url.pathname // '/page'
url.search // '?a=1&b=2'
url.hash // '#hash'
url.origin // 'https://example.com:8080'
// searchParams — URLSearchParams
url.searchParams.get('a'); // '1'
url.searchParams.getAll('a'); // ['1']
url.searchParams.has('a'); // true
url.searchParams.set('c', '3'); // 替换/新增
url.searchParams.append('a', '4'); // 追加同名参数
url.searchParams.delete('b'); // 删除
url.searchParams.toString(); // 'a=1&c=3&a=4'
url.toString(); // 完整序列化
url.toJSON(); // 同 toString
编码 / 解码
encodeURIComponent('你好 世界') // '%E4%BD%A0%E5%A5%BD%20%E4%B8%96%E7%95%8C'
decodeURIComponent('%E4%BD%A0') // '你'
encodeURI('https://x.com/你好') // 'https://x.com/%E4%BD%A0%E5%A5%BD' (保留 :/)
btoa('hello') // 'aGVsbG8=' — 字符串 → Base64
atob('aGVsbG8=') // 'hello' — Base64 → 字符串
// TextEncoder / TextDecoder (UTF-8)
new TextEncoder().encode('你好'); // Uint8Array(6) [228, 189, 160, 229, 165, 189]
new TextDecoder().decode(uint8Arr); // '你好'
new TextDecoder('gbk').decode(buf); // 指定编码
14. Console API
| 方法 | 用途 |
|---|---|
console.log() | 通用日志 |
console.info() / .warn() / .error() | 级别日志 (带图标) |
console.debug() | 调试级别 (默认不显示) |
console.table(arr) | 表格形式打印数组/对象 |
console.group('label') / .groupEnd() | 可折叠分组 |
console.groupCollapsed('label') | 默认折叠分组 |
console.time('label') / .timeEnd('label') | 计时 (ms) |
console.timeLog('label') | 中途记录 |
console.trace() | 打印调用栈 |
console.assert(cond, msg) | 条件为 false 时输出 error |
console.count('label') / .countReset() | 计数 |
console.dir(obj) | 以交互式属性列表打印对象 |
console.clear() | 清空控制台 |
15. Performance API
高精度计时
const t0 = performance.now(); // DOMHighResTimeStamp (微秒精度)
doWork();
const t1 = performance.now();
console.log(`耗时: ${t1 - t0}ms`);
// 不要用 Date.now() 测性能 — 精度不够且受系统时间影响
PerformanceObserver (监听性能条目)
// 监控资源加载
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
console.log(entry.name, entry.duration, entry.startTime);
// entryType: 'resource' | 'navigation' | 'paint' | 'longtask' | 'mark' | 'measure' | ...
});
});
observer.observe({ type: 'resource', buffered: true });
observer.observe({ type: 'longtask', buffered: true }); // 需要 PerformanceLongTaskTiming
observer.disconnect();
// 监控 Core Web Vitals
observer.observe({ type: 'largest-contentful-paint', buffered: true }); // LCP
observer.observe({ type: 'first-input', buffered: true }); // FID
observer.observe({ type: 'layout-shift', buffered: true }); // CLS
// 自定义 mark / measure
performance.mark('start-task');
doTask();
performance.mark('end-task');
performance.measure('task-duration', 'start-task', 'end-task');
const m = performance.getEntriesByName('task-duration')[0];
console.log(m.duration);
Navigation Timing
const [entry] = performance.getEntriesByType('navigation');
const nav = entry.toJSON(); // 或 performance.timing (旧)
// 包含: domContentLoadedEventEnd, loadEventEnd, domInteractive, etc.
16. Crypto / 安全
crypto.randomUUID / getRandomValues
// 生成 UUID v4 crypto.randomUUID(); // '36b8f84d-df4e-4d49-b662-bcde71a8764f' // 生成随机值 const arr = new Uint8Array(16); crypto.getRandomValues(arr); // 密码学安全的随机数
crypto.subtle (Web Crypto API)
// 哈希 (SHA-256)
const data = new TextEncoder().encode('hello');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashHex = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0')).join('');
// HMAC
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = await crypto.subtle.sign('HMAC', key, data);
// 生成密钥对
const keyPair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['encrypt', 'decrypt']
);
// 支持的算法: AES-CTR/CBC/GCM, RSA-OAEP/PSS, ECDH, ECDSA, HKDF, PBKDF2
Content Security (SRI / nonce)
// <script src="..." integrity="sha384-..." crossorigin="anonymous"></script> // CSP header: Content-Security-Policy: script-src 'nonce-abc123'
17. 其他常用
Clipboard API
await navigator.clipboard.writeText('hello');
const text = await navigator.clipboard.readText();
// 注: 需要 HTTPS 或 localhost, 且通常在用户手势回调中使用
Fullscreen API
el.requestFullscreen(); // 进入全屏
document.exitFullscreen(); // 退出
document.fullscreenElement; // 当前全屏元素
document.addEventListener('fullscreenchange', () => console.log('changed'));
Notification API
if (Notification.permission === 'granted') {
new Notification('Title', { body: 'Message', icon: '/icon.png' });
} else if (Notification.permission !== 'denied') {
const result = await Notification.requestPermission(); // 'granted'|'denied'
}
Geolocation API
navigator.geolocation.getCurrentPosition(
(pos) => console.log(pos.coords.latitude, pos.coords.longitude),
(err) => console.error(err.code, err.message),
{ enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
);
// 持续监听
const id = navigator.geolocation.watchPosition(success, error, options);
navigator.geolocation.clearWatch(id);
Page Visibility API
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面被隐藏 (切换标签/最小化)
pauseVideo();
} else {
resumeVideo();
}
});
History API (SPA 路由)
history.pushState({ page: 2 }, '', '/page2'); // 添加记录
history.replaceState({ page: 1 }, '', '/page1'); // 替换当前记录
window.addEventListener('popstate', (e) => {
console.log(location.pathname, e.state);
});
history.back(); // 后退
history.forward(); // 前进
history.go(-1); // 后退 1 步
Vibration API 仅移动端
navigator.vibrate(200); // 震动200ms navigator.vibrate([100, 50, 100]); // 震动100ms→停50ms→震动100ms navigator.vibrate(0); // 停止震动
matchMedia (响应式 JS)
const mq = window.matchMedia('(max-width: 768px)');
if (mq.matches) { /* 移动端逻辑 */ }
mq.addEventListener('change', (e) => {
if (e.matches) console.log('entered mobile');
});
18. BOM (浏览器对象模型)
18.1 window 核心
| 属性/方法 | 说明 |
|---|---|
window.innerWidth / innerHeight | 视口尺寸 (含滚动条) |
window.outerWidth / outerHeight | 整个浏览器窗口尺寸 |
window.devicePixelRatio | 设备像素比 (Retina=2, 标准=1) |
window.scrollX / scrollY | 页面滚动偏移 (只读) |
window.screenX / screenY | 窗口左上角相对屏幕坐标 |
window.scrollTo({top, left, behavior}) | 滚动到指定位置; behavior: 'auto'|'smooth' |
window.scrollBy({top, left, behavior}) | 相对滚动 |
el.scrollIntoView({behavior, block}) | 滚动使元素可见; block: 'start'|'center'|'end' |
window.open(url, name, features) | 新窗口/标签; 返回 WindowProxy; 受弹窗拦截器限制 |
window.close() | 关闭窗口 (只能关闭自己 open 的) |
window.focus() / .blur() | 窗口获得/失去焦点 |
window.print() | 打开打印对话框 |
// 平滑滚动
window.scrollTo({ top: 500, left: 0, behavior: 'smooth' });
// 获取视口尺寸 (不含滚动条)
const w = document.documentElement.clientWidth;
const h = document.documentElement.clientHeight;
18.2 navigator — 浏览器环境信息
| 属性 | 类型 | 说明 |
|---|---|---|
navigator.userAgent | string | UA 字符串 (不可靠,推荐 feature detection) |
navigator.userAgentData | object | UA Client Hints API: .brands, .mobile, .platform |
navigator.language | string | 首选语言: 'zh-CN' |
navigator.languages | string[] | 语言偏好列表 |
navigator.platform | string | 'Win32' | 'MacIntel' | 'Linux x86_64' 已废弃 |
navigator.hardwareConcurrency | number | 逻辑 CPU 核心数 |
navigator.maxTouchPoints | number | 最大触摸点数 (检测触摸设备) |
navigator.onLine | boolean | 是否联网 (不精确,可能连上 wifi 但无 internet) |
navigator.cookieEnabled | boolean | Cookie 是否启用 |
navigator.pdfViewerEnabled | boolean | 浏览器是否支持行内 PDF 查看 |
navigator.serviceWorker | ServiceWorkerContainer | SW 注册入口: .register() .controller |
navigator.storage | StorageManager | 存储额度: .estimate().then(r => r.quota) |
navigator.connection | NetworkInformation | .effectiveType ('4g'|'3g'|'2g'|'slow-2g'), .downlink, .rtt, .saveData (省流量模式) Chrome |
// Feature detection 示例
const supportsWebP = await new Promise(r => {
const img = new Image();
img.onload = () => r(true);
img.onerror = () => r(false);
img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoCAAEAAQAcJaQAA3AA/v3AgAA=';
});
18.3 screen — 屏幕信息
| 属性 | 说明 |
|---|---|
screen.width / .height | 屏幕分辨率 (物理像素) |
screen.availWidth / .availHeight | 可用区域 (不含任务栏/Dock) |
screen.colorDepth | 色彩深度 (通常 24) |
screen.pixelDepth | 像素深度 (通常同 colorDepth) |
screen.orientation | ScreenOrientation 对象: .type ('portrait-primary'|'landscape-primary'), .angle, .lock()/.unlock() |
screen.isExtended | 是否多屏扩展模式 |
18.4 location — URL 操作
| 属性/方法 | 说明 |
|---|---|
location.href | 完整 URL; 赋值即跳转 |
location.protocol | 'http:' | 'https:' |
location.host | hostname:port (如 'example.com:8080') |
location.hostname | 域名 (如 'example.com') |
location.port | 端口号 |
location.pathname | 路径 (如 '/page') |
location.search | 查询串 (如 '?a=1&b=2') |
location.hash | hash 部分 (如 '#section') |
location.origin | 只读:协议+域名+端口 (如 'https://example.com:8080') |
location.assign(url) | 跳转 (可后退) |
location.replace(url) | 替换当前记录 (不可后退) |
location.reload() | 刷新; reload(true) = 强制忽略缓存 (已废弃参数) |
location.toString() | 同 location.href |
18.5 history — 会话历史管理
| 属性/方法 | 说明 |
|---|---|
history.length | 当前标签页的历史记录条数 |
history.state | 当前状态对象 (pushState/replaceState 传入的值) |
history.scrollRestoration | 'auto' (浏览器恢复滚动位置) | 'manual' |
history.back() | 后退一页 |
history.forward() | 前进一页 |
history.go(n) | 正=前进, 负=后退; go(0) 刷新 |
history.pushState(state, title, url) | 新增历史记录 (不刷新页面!) |
history.replaceState(state, title, url) | 替换当前历史记录 |
window.onpopstate | 前进/后退触发 (pushState/replaceState 不触发) |
// SPA 路由核心
function navigate(path) {
history.pushState({ path }, '', path);
render(path);
}
window.addEventListener('popstate', (e) => {
render(location.pathname);
// e.state 是 pushState 传入的 state
});
19. 表单 & 校验 (Constraint Validation)
| 属性/方法 | 说明 |
|---|---|
form.elements | 所有表单控件 HTMLFormControlsCollection |
form.elements['name'] | 按 name 访问控件 |
form.submit() | 提交表单 (不触发 submit 事件) |
form.reset() | 重置所有字段到初始值 |
form.reportValidity() | 校验并显示原生提示 (返回 boolean) |
form.checkValidity() | 校验不显示提示 (返回 boolean) |
input.validity | ValidityState: .valid, .valueMissing, .typeMismatch, .tooShort, .tooLong, .rangeUnderflow, .rangeOverflow, .stepMismatch, .badInput, .patternMismatch, .customError |
input.validationMessage | 浏览器生成的校验提示文本 |
input.setCustomValidity(msg) | 设置自定义错误; 设 '' 清除 |
input.willValidate | 该控件是否参与校验 (disabled/hidden 的不参与) |
HTML5 校验属性
<input required minlength="3" maxlength="20" min="0" max="100" step="5"
pattern="[A-Z]+" type="email" type="url" type="number">
禁用原生校验气泡,自定义 UI
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.checkValidity()) {
// 遍历所有无效控件
[...form.elements].forEach(el => {
if (!el.validity.valid) {
showError(el, el.validationMessage);
}
});
return;
}
submitData(new FormData(form));
});
// 实时清除错误
input.addEventListener('input', () => {
input.setCustomValidity('');
hideError(input);
});
FormData — 构建请求体
const fd = new FormData(form); // 从表单构建
const fd2 = new FormData();
fd2.append('name', 'value');
fd2.append('file', fileInput.files[0]);
fd2.set('name', 'newValue'); // 覆盖
fd2.delete('name');
fd2.has('name'); // boolean
fd2.get('name'); // 第一个值
fd2.getAll('name'); // 所有值
// 迭代
for (const [k, v] of fd) { console.log(k, v); }
// FormData → 普通对象
Object.fromEntries(fd.entries());
20. Drag & Drop
| 事件 (被拖元素) | 事件 (放置区) | 说明 |
|---|---|---|
dragstart | 开始拖拽; 这里设 dataTransfer | |
drag | 拖拽中持续触发 | |
dragend | 拖拽结束 (无论是否放下) | |
dragenter | 拖入放置区 | |
dragover | 在放置区上方移动; 必须 preventDefault 才允许 drop | |
dragleave | 离开放置区 | |
drop | 放下 |
// 被拖元素: 设 draggable="true"
<div draggable="true">Drag me</div>
el.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id);
e.dataTransfer.effectAllowed = 'move'; // move|copy|link
e.dataTransfer.dropEffect = 'move';
});
// 放置区
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须!
e.dataTransfer.dropEffect = 'move';
});
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; // FileList
[...files].forEach(file => {
console.log(file.name, file.type, file.size);
});
});
21. Streams API
ReadableStream (读取流)
// 从 fetch body 获取流
const res = await fetch('/large-file');
const reader = res.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
console.log(`Received ${value.length} bytes`);
}
reader.releaseLock(); // 或 reader.cancel()
// 异步迭代 (更简洁)
for await (const chunk of res.body) {
console.log(chunk); // Uint8Array
}
创建自定义 ReadableStream
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
pull(controller) { /* 被消费端拉取时调用 */ },
cancel(reason) { /* 流被取消 */ },
});
const reader = stream.getReader();
const { value } = await reader.read(); // Uint8Array('chunk1')
WritableStream (写入流) & pipeTo
// 写入流
const writable = new WritableStream({
write(chunk) { console.log('received:', chunk); },
close() { console.log('done'); },
abort(err) { console.error(err); },
});
const writer = writable.getWriter();
await writer.write('data1');
await writer.write('data2');
await writer.close();
// pipeTo — 流对接
readableStream.pipeTo(writableStream);
readableStream.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
}));
TextEncoderStream / TextDecoderStream
// 可读文本流 → 实时逐行处理
const res = await fetch('/api/stream');
res.body
.pipeThrough(new TextDecoderStream())
.pipeTo(new WritableStream({
write(chunk) { processLine(chunk); }
}));
// 或
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) { ... }
22. 设备 & 传感器 API
Web Share API
// 调起系统分享面板 (需用户手势触发)
if (navigator.canShare?.({ files: [...] })) { // 检测文件分享支持
await navigator.share({
title: 'Title',
text: 'Description',
url: 'https://example.com',
// files: [file], // 可选
});
}
Permissions API
// 查询权限状态
const status = await navigator.permissions.query({ name: 'geolocation' });
console.log(status.state); // 'granted' | 'denied' | 'prompt'
status.addEventListener('change', () => {
console.log('Permission changed to:', status.state);
});
// 支持的权限名: geolocation, notifications, camera, microphone,
// midi, push, accelerometer, gyroscope, magnetometer, clipboard-read, clipboard-write
Screen Wake Lock (防止锁屏)
let wakeLock = null;
async function keepAwake() {
try {
wakeLock = await navigator.wakeLock.request('screen');
wakeLock.addEventListener('release', () => console.log('wake lock released'));
} catch (err) { console.error(err); }
}
async function release() { await wakeLock?.release(); wakeLock = null; }
Screen Orientation (屏幕方向)
const { type, angle } = screen.orientation;
// type: 'portrait-primary' | 'landscape-primary' | 'portrait-secondary' | 'landscape-secondary'
screen.orientation.addEventListener('change', () => {
console.log('orientation changed:', screen.orientation.type);
});
// 锁定方向 (需全屏)
await screen.orientation.lock('portrait'); // 'any'|'natural'|'landscape'|'portrait'|...
screen.orientation.unlock();
Battery Status Chrome
const battery = await navigator.getBattery();
battery.level; // 0~1
battery.charging; // boolean
battery.chargingTime; // 秒
battery.dischargingTime;
battery.addEventListener('levelchange', () => console.log(battery.level));
Device Memory Chrome
navigator.deviceMemory; // 0.25, 0.5, 1, 2, 4, 8 (GB), 用于自适应降级
Gamepad API
window.addEventListener('gamepadconnected', (e) => {
console.log('Gamepad connected:', e.gamepad.id);
pollGamepad();
});
function pollGamepad() {
const gamepads = navigator.getGamepads();
const gp = gamepads[0];
if (gp) {
console.log(gp.axes); // 摇杆位置 [-1, 1]
console.log(gp.buttons.map(b => b.pressed)); // 按钮状态
}
requestAnimationFrame(pollGamepad);
}
Web MIDI
const access = await navigator.requestMIDIAccess();
for (const [id, input] of access.inputs) {
input.onmidimessage = (e) => console.log(e.data); // [status, note, velocity]
}
for (const [id, output] of access.outputs) {
output.send([0x90, 60, 127]); // Note On: C4, velocity 127
}
Selection API
const sel = window.getSelection(); sel.anchorNode; // 选区起点节点 sel.focusNode; // 选区终点节点 sel.anchorOffset; // 起点偏移 sel.rangeCount; // 选区片段数 (Firefox 支持多选区) sel.isCollapsed; // 是否光标 (无选中内容) sel.toString(); // 选中文本 sel.getRangeAt(0); // 获取 Range 对象 // 用 Range 编程式选中 const range = document.createRange(); range.selectNodeContents(el); range.setStart(el, 0); range.setEnd(el, 2); sel.removeAllRanges(); sel.addRange(range); sel.deleteFromDocument(); // 删除选中内容