一、概述

Chrome(基于开源项目 Chromium)是由 Google 开发的跨平台 Web 浏览器。其核心设计围绕三大原则展开:

Chrome 的渲染引擎 Blink 于 2013 年 4 月从 Apple 主导的 WebKit 项目分叉而来。此前 Chrome(自 2008 年发布起)一直使用 WebKit 作为渲染引擎,与 Safari 共享大量代码。分叉的核心原因是 Chrome 的多进程架构与 WebKit 的单进程设计在技术上难以调和,Google 希望独立发展更高性能的渲染管线。

Browser Engine Evolution
图 0:浏览器引擎演进与分叉关系

Chrome 代码库规模庞大(约 3000 万行 C++ 代码),主要子系统包括:

子系统功能核心代码路径
BlinkHTML/CSS 渲染引擎third_party/blink/
V8JavaScript/WebAssembly 引擎v8/
ccChrome Compositor(合成器)cc/
Mojo跨进程 IPC 框架mojo/
net网络栈(HTTP/3, TCP, TLS)net/
Skia2D 图形库third_party/skia/
ANGLEOpenGL ES → D3D/Vulkan 翻译层third_party/angle/
//content多进程嵌入层(Content API)content/
//chromeChrome 浏览器 UI + 功能chrome/

用例图:浏览器核心功能

从用户角度,Chrome 浏览器主要提供以下核心功能。

Chrome Use Case Diagram
图 1:Chrome 浏览器核心用例

二、渲染管线

完整渲染流程(从 HTML 到像素)

渲染管线分为两个阶段:Paint(在 Main Thread 完成,产出 SkPicture 显示列表)和 Composite(在 Compositor Thread 完成,产出 GL 命令发给 GPU)。Tiling(分块)策略确保只对可见区域进行栅格化。

Rendering Pipeline
图 3:Chrome 渲染管线(HTML → 屏幕像素)

三、GPU 加速合成

Chrome 的合成器(cc)是管理 GraphicsLayer 树和协调帧生命周期的软件库。自 Chrome 引入 Impl-side Painting 后,合成器运行在独立的 Compositor Thread,可以独立于 Main Thread 响应滚动和动画。

GPU Accelerated Compositing
图 5:GPU 加速合成流程(Impl-side Painting)

关键机制:

GPU Process 架构带来四大收益:安全(沙箱隔离)、鲁棒(GPU 崩溃不拖垮浏览器)、统一(所有平台统一用 GL ES 2.0)、并行(CPU 与 GPU 流水线工作)。

四、V8 JavaScript 引擎

V8 是 Google 开源的 C++ 高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。采用多层编译策略,在启动速度与峰值性能间取得平衡。

V8 JavaScript Engine
图 6:V8 多层编译管线

编译层级说明:

层级编译器产出特点
1ParserAST词法/语法分析
2Sparkplug非优化机器码快速基线编译,无需类型反馈
3IgnitionBytecode(字节码)解释器,启动快,收集类型反馈
4Maglev中等优化机器码2023 年新增,介于 Ignition 和 TurboFan 之间
5TurboFan高度优化机器码针对热点函数做推测优化(Speculative Optimization)

如果推测失败(类型变化),TurboFan 编译的代码会 Deoptimize 回退到 Ignition 字节码执行,重新收集类型信息后再优化。

V8 的 Orinoco 垃圾回收器采用分代(Generational)、并发(Concurrent)、并行(Parallel)设计,极大减少了 Stop-the-World 停顿时间。

五、网络栈

Chrome 的所有网络通信都由 Browser 进程处理。这确保浏览器可以全局控制网络访问、维持 HTTP 连接池、统一管理 Cookie/缓存。Renderer 进程通过 IPC 发送资源请求给 Browser 的 ResourceDispatcherHost,后者通过 URLRequest 执行真正的 HTTP 请求。

Multi-Process Network Loading
图 7:多进程网络资源加载

关键设计点:

六、多进程架构

6.1 架构总览图

Chrome 采用多进程架构:每个标签页、扩展、插件都在独立进程中运行,进程间通过 Mojo IPC 框架通信。GPU 进程通过命令缓冲区(Command Buffer,位于共享内存)接收 Renderer 的 GL 命令。

Multi-Process Architecture
图 1:Chrome 多进程架构总览

6.2 Browser 进程内部架构

Browser 进程是系统的"大脑",运行 UI 线程和 IO 线程。每个标签页对应一个 WebContents 对象,它管理 RenderFrameHost(跨进程与 Renderer 通信)和若干 Tab Helper(如 Favicon、Infobar 等)。

Browser Process Internal
图 2:Browser 进程内部架构

6.3 进程类型总结

进程类型职责数量
BrowserUI、网络、存储、权限管理、进程调度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++ 类的继承与组合关系。

Content API Class Hierarchy
图 2.5:Content API 核心类层次

七、模块依赖

源码模块依赖关系

Chromium 源码树各模块之间有严格的依赖层级。下层为上层提供基础能力,避免循环依赖。

Chromium Source Module Dependencies
图 0:Chromium 核心模块依赖关系

八、Blink 四棵并行树

树结构与映射关系

Blink 内部维护四棵并行的树结构,逐步从 DOM 转换到可供合成器(cc)使用的 GraphicsLayer:

Blink Four Trees
图 4:Blink 四棵并行树结构

合成触发条件(满足以下任一条件,RenderLayer 会提升为合成层):

九、Site Isolation

Site Isolation 是 Chrome 最核心的安全架构之一。它将不同站点(scheme + registered domain)放入不同 Renderer 进程,利用操作系统进程边界隔离攻击面。自 Chrome 67 起桌面端默认启用,Chrome 77 起 Android 端(2GB+ RAM 设备)启用。

Site Isolation
图 8:Site Isolation 进程模型

Site 定义:scheme + registered domain(含 public suffix),忽略子域名、端口和路径。例如 a.example.com 和 b.example.com 属于同一个 site。

核心机制:

Site Isolation 防御的威胁:

十、扩展系统

Chrome 扩展系统基于 Manifest V3 架构,核心组件包括 Service Worker(后台事件处理)、Content Script(注入页面操作 DOM)、以及丰富的 chrome.* API。

Extension System
图 9:Chrome Extension 系统架构

Extension 可实现的浏览器控制能力:

十一、安全沙箱

Chrome 的安全模型基于最小权限原则:每个进程类型只被授予完成其职责所需的最小系统调用集。底层使用 Linux seccomp-bpf / Windows Restricted Token / macOS Sandbox 实现。

Security Sandbox
图 10:Chrome 安全沙箱模型

各进程沙箱策略:

进程允许的操作禁止的操作
RendererIPC 通信、有限地址空间直接文件/网络/设备访问
GPUGPU API 调用(GL/Vulkan/D3D)文件系统、网络
Networksocket 系统调用文件系统、显示设备
Browser全部系统调用无(最高权限)

十二、部署架构

各进程以独立沙箱形式部署在操作系统上,通过 Mojo IPC 和共享内存通信。

Chrome Deployment Architecture
图 11:Chrome 进程部署拓扑

十三、总结

Chrome 是一个高度复杂的分层、多进程、多线程系统。其架构可归纳为以下核心层次:

层次组件职责
UI 层Views / Aura / Ash浏览器窗口管理、标签栏、书签栏
Content API 层WebContents / RenderViewHost多进程嵌入抽象,隔离 chrome/ 和 content/
IPC 层Mojo / Legacy IPC跨进程通信,Channel + ChannelProxy 线程代理
渲染层Blink / V8 / SkiaHTML/CSS 解析、JS 执行、2D 绑定
合成层cc图层管理、帧调度、Tile 化、GPU 合成
平台适配层ANGLE / Skia / GL ES跨平台 3D API 抽象
安全层Sandbox / Site Isolation进程隔离、系统调用过滤、跨站防护

Chrome 的架构演进始终围绕安全、稳定、性能三原则持续迭代。理解其多进程模型、Blink 渲染管线、V8 编译策略、cc 合成器、网络栈和安全沙箱,是深入理解现代浏览器工作原理的关键。

十四、Browser Web API 速查手册

核心浏览器 JS API 参考 — 不含语言本身,只含浏览器宿主提供的 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.childrenel.childNodes子元素 / 所有子节点(含文本注释)
el.firstElementChildel.firstChild第一个子
el.lastElementChildel.lastChild最后一个子
el.previousElementSiblingel.previousSibling前一个兄弟
el.nextElementSiblingel.nextSibling后一个兄弟
el.parentElementel.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.innerHTMLHTML 内容 (有 XSS 风险)
内容el.outerHTML包含自身标签的 HTML
内容el.textContent纯文本,保留空格换行
内容el.innerText纯文本,触发 reflow,忽略隐藏元素
内容el.insertAdjacentHTML(pos, html)比 innerHTML 快; pos: beforebegin/afterbegin/beforeend/afterend
classel.classList.add/remove/toggle('c')toggle 可传第二个参数 force
classel.classList.contains('c')是否包含
classel.classList.replace('a','b')替换
styleel.style.prop = 'val'设置内联样式 (驼峰命名: backgroundColor)
styleel.style.cssText = 'color:red;'批量设置内联样式
stylegetComputedStyle(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 事件绑定

方式代码特点
addEventListenerel.addEventListener('click', fn)最推荐,支持多个监听器
onceel.addEventListener('click', fn, { once: true })触发一次后自动解绑
passiveel.addEventListener('scroll', fn, { passive: true })承诺不 preventDefault,提升滚动性能
signalel.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.eventPhase1=捕获, 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() 基础

操作代码
GETfetch('/api/data').then(r => r.json())
POST JSONfetch('/api', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) })
POST FormDatafetch('/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.statusTextHTTP 状态码 / 描述
res.headersHeaders 对象, get/set/has/forEach
res.json()解析 JSON body → Promise
res.text()读取为文本
res.blob()读取为 Blob
res.arrayBuffer()读取为 ArrayBuffer
res.formData()解析为 FormData
res.bodyReadableStream, 可流式读取
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 WebSocketSSEWebSocket
方向仅服务器 → 客户端双向
协议HTTP(S)ws:// / wss://
自动重连内置需手动实现
二进制仅文本支持

5. Storage / 存储

类型容量持久性作用域操作
localStorage ~5MB 永久 同源 同步 API: setItem/getItem/removeItem/clear
localStorage.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.userAgentstringUA 字符串 (不可靠,推荐 feature detection)
navigator.userAgentDataobjectUA Client Hints API: .brands, .mobile, .platform
navigator.languagestring首选语言: 'zh-CN'
navigator.languagesstring[]语言偏好列表
navigator.platformstring'Win32' | 'MacIntel' | 'Linux x86_64' 已废弃
navigator.hardwareConcurrencynumber逻辑 CPU 核心数
navigator.maxTouchPointsnumber最大触摸点数 (检测触摸设备)
navigator.onLineboolean是否联网 (不精确,可能连上 wifi 但无 internet)
navigator.cookieEnabledbooleanCookie 是否启用
navigator.pdfViewerEnabledboolean浏览器是否支持行内 PDF 查看
navigator.serviceWorkerServiceWorkerContainerSW 注册入口: .register() .controller
navigator.storageStorageManager存储额度: .estimate().then(r => r.quota)
navigator.connectionNetworkInformation.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.orientationScreenOrientation 对象: .type ('portrait-primary'|'landscape-primary'), .angle, .lock()/.unlock()
screen.isExtended是否多屏扩展模式

18.4 location — URL 操作

属性/方法说明
location.href完整 URL; 赋值即跳转
location.protocol'http:' | 'https:'
location.hosthostname:port (如 'example.com:8080')
location.hostname域名 (如 'example.com')
location.port端口号
location.pathname路径 (如 '/page')
location.search查询串 (如 '?a=1&b=2')
location.hashhash 部分 (如 '#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.validityValidityState: .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(); // 删除选中内容