浏览器原理(前端视角)
从输入 URL 到页面展示的完整旅程,以及浏览器底层运作机制。覆盖面试高频考点。
1. 进程架构
1.1 多进程架构(Chrome)
| 进程类型 | 职责 | 数量 |
|---|---|---|
| 浏览器主进程(Browser) | 界面管理、地址栏、书签、导航栏、进程管理 | 1 |
| 渲染进程(Renderer) | HTML/CSS/JS 解析、渲染、事件处理(沙箱隔离) | 每个 tab 通常 1 个 |
| GPU 进程 | 3D 渲染、合成加速、GPU 资源管理 | 1 |
| 网络进程(Network) | 网络请求、资源加载 | 1 |
| 插件进程(Plugin) | Flash / PDF 等插件(已逐步废弃) | 每个插件 1 个 |
| 工具进程(Utility) | 解码、代理配置等辅助任务 | 按需 |
架构演进:
单进程(IE6)→ 多进程(Chrome 1)→ 面向服务架构(Chrome 10+ SOA)
↓
Site Isolation(Chrome 67+)
每个 iframe 可能独立进程
Site Isolation(站点隔离):Chrome 67+ 默认启用,不同站点的 iframe 分配到不同渲染进程,防止 Spectre 等侧信道攻击窃取跨站数据。
1.2 渲染进程内部线程
渲染进程内部是多线程架构:
| 线程 | 职责 |
|---|---|
| 主线程(Main Thread) | 解析 HTML/CSS、执行 JS、样式计算、布局、绘制记录 |
| 工作线程(Worker Thread) | Service Worker、Web Worker |
| 光栅化线程(Raster Thread) | 将绘制指令转换为位图 |
| 合成器线程(Compositor Thread) | 管理图层、接收帧、执行合成、响应滚动 |
2. 导航流程:从 URL 到页面展示
输入 URL → ① URL 解析 → ② DNS 解析 → ③ TCP 连接 → ④ TLS 握手(HTTPS)
→ ⑤ HTTP 请求/响应 → ⑥ 解析 HTML → ⑦ 加载子资源 → ⑧ 渲染 → ⑨ 页面展示
2.1 步骤详解
① URL 解析:
- 浏览器进程判断是否合法 URL,否 → 转为搜索
- HSTS 列表检查(强制 HTTPS)
② DNS 解析:
浏览器 DNS 缓存 → 操作系统 DNS 缓存 → hosts 文件 → 本地 DNS 服务器 → 递归/迭代查询
③ TCP 连接:三次握手(SYN → SYN+ACK → ACK)
④ TLS 握手(HTTPS):TLS 1.2 需要 2-RTT,TLS 1.3 只需 1-RTT
⑤ HTTP 请求:
请求: GET /index.html HTTP/1.1
Host: example.com
User-Agent: ...
Accept-Encoding: gzip, br
响应: HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: br
...
⑥ 解析 HTML → 构建 DOM 树:边解析边下载子资源(CSS、JS、图片等)
⑦ 加载子资源:
- CSS:不阻塞 HTML 解析,但阻塞渲染(首屏需要 CSSOM)
- JS:阻塞 HTML 解析(
<script>无defer/async) - 图片/字体:不阻塞 HTML 解析,但可能触发 Layout/重排
2.2 资源加载优先级
| 类型 | 加载优先级 | 说明 |
|---|---|---|
| CSS(首屏) | 最高 | 渲染阻塞 |
<script>(同步) |
高 | 解析阻塞 |
<script defer> |
低 | 文档解析完执行 |
<script async> |
低 | 下载完立即执行(不保证顺序) |
| 图片(视口内) | 高 | 延迟更长时间 |
| 图片(视口外) | 低 | 根据滚动加载 |
preload |
高 | 显式声明 |
prefetch |
最低 | 未来页面使用 |
3. 渲染流程(渲染管线)
3.1 完整渲染流水线
DOM 树 + CSSOM 树
↓ 合并
渲染树(Render Tree,只含可见节点)
↓ 布局 / 重排(Layout / Reflow)
计算每个节点的几何信息(位置、尺寸)
↓ 分层(Layer Tree)
选择合适的图层(Layer)
↓ 绘制记录(Paint)
记录绘制指令序列(顺序)
↓ 栅格化(Rasterization)
将绘制指令转换为位图(GPU/CPU)
↓ 合成(Composite)
将各个图层的位图合成为最终屏幕图像
↓ 显示(Display)
3.2 DOM 树构建
<html>
<body>
<p>Hello</p>
<img src="a.jpg">
</body>
</html>
经过 HTML 解析器(分词 → 词法分析 → 树构建)转换为:
Document
|
html
|
body
/ \
p img
|
"Hello"
3.3 CSSOM 构建
CSS 字节流 → 词法分析 → 语法分析 → CSSOM 树。
CSS 解析规则:从右到左匹配选择器,因此避免深嵌套选择器:
/* 浏览器匹配:先找到 div,再回溯到 .nav,再回溯到 body */
body .nav > div {}
CSS 优化建议:
- 避免通配符
* - 避免深层嵌套
- 类选择器优先于标签选择器
- 使用 BEM 命名保持扁平
3.4 渲染树构建
渲染树 = DOM 树过滤 + CSSOM 树样式计算后的结果。
过滤掉不可见节点(display: none、<head> 子元素等)。
注意 visibility: hidden 不在渲染树中移除(占位),而 display: none 完全移除。
3.5 布局(Layout / Reflow)
布局作用:计算每个可见节点的几何信息(x、y、width、height)。
布局过程:
- 父节点计算自身尺寸
- 父节点遍历子节点,计算子节点位置
- 递归执行
布局类型:
- 全局布局:整个渲染树重新计算(如窗口 resize)
- 增量布局:局部 dirty 标记,只重算受影响的节点
3.6 分层(Layer Tree)
浏览器将页面分成多个图层,独立绘制和合成:
- 拥有
z-index的定位元素会创建新层 transform、opacity、will-change会创建新层<video>、<canvas>、<iframe>独立成层
/* 触发分层 */
.element {
will-change: transform;
/* 或 */
transform: translateZ(0);
/* 或 */
backface-visibility: hidden;
}
分层过多会增加内存占用(每层独立位图)。
3.7 绘制(Paint)
将布局结果转换为像素绘制指令序列:
绘制背景 → 绘制边框 → 绘制文本 → 绘制子元素 → 绘制 outline
绘制记录存储为显示列表(Display List),后续光栅化时逐项执行。
3.8 合成(Composite)
合成器线程将各个图层的位图按照层级关系合并为最终帧。
优势:合成器线程独立于主线程,滚动和 transform 动画不阻塞主线程。
4. 重排(Reflow)与重绘(Repaint)
4.1 触发条件
| 操作 | 重排 | 重绘 | 合成 |
|---|---|---|---|
修改 width/height |
✓ | ✓ | |
修改 padding/margin/border |
✓ | ✓ | |
修改 font-size、font-family |
✓ | ✓ | |
display: none → block |
✓ | ✓ | |
| 添加/删除 DOM 元素 | ✓ | ✓ | |
| 修改文本内容 | ✓ | ✓ | |
window.resize |
✓ | ✓ | |
读取 offsetHeight/scrollTop/getComputedStyle |
✓ | ✓ | |
修改 color/background-color |
✓ | ||
修改 visibility |
✓ | ||
修改 transform |
✓ | ||
修改 opacity |
✓ | ||
| 滚动 | ✓ |
关键原则:transform、opacity 只触发合成(Composite),不触发重排和重绘,性能最好。
4.2 浏览器批量优化
浏览器会将连续的样式变更缓存,在下一帧统一处理:
// 不好的写法:强制刷新队列导致多次重排
element.style.width = '100px';
const h = element.offsetHeight; // ← 强制刷新布局队列
element.style.height = '200px';
const w = element.offsetWidth; // ← 再次强制刷新
// 好的写法:先读后写
const h = element.offsetHeight;
const w = element.offsetWidth;
element.style.width = '100px';
element.style.height = '200px';
强制触发布局的属性:
offsetTop,offsetLeft,offsetWidth,offsetHeightscrollTop,scrollLeft,scrollWidth,scrollHeightclientTop,clientLeft,clientWidth,clientHeightgetComputedStyle(),getBoundingClientRect()
4.3 重排优化策略
// 1. 批量修改样式(减少重排次数)
element.style.cssText = 'width: 100px; height: 200px; color: red;';
// 或使用 class
element.className = 'new-styles';
// 2. DocumentFragment 批量操作 DOM
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); // 一次重排
// 3. 使用 display: none 离线操作
element.style.display = 'none';
// ... 大量 DOM 操作 ...
element.style.display = 'block'; // 两次重排(hide + show)
// 4. 动画使用 transform 和 opacity
// 不好
element.style.left = `${x}px`;
// 好
element.style.transform = `translateX(${x}px)`;
// 5. requestAnimationFrame 合并写操作
requestAnimationFrame(() => {
element.style.width = '200px';
});
// 6. 缓存布局信息(避免重复读取)
const rect = element.getBoundingClientRect();
// 后续使用 rect 而非反复读取
// 7. 复杂动画独立成层
element.style.willChange = 'transform';
// 或创建独立合成层
4.4 性能对比
操作 成本 主线程阻塞
重排(Reflow) 最高 是
重绘(Repaint) 中等 是
合成(Composite) 最低 否(合成器线程)
5. 事件循环(Event Loop)
5.1 浏览器事件循环
┌──────────────────────────────┐
│ 宏任务队列 │ ← 取一个宏任务执行
└──────────────┬───────────────┘
↓
执行宏任务(一个)
↓
清空整个微任务队列 ← 微任务在宏任务之间执行
↓
是否需要更新渲染? ← 浏览器约 60fps 检查一次
↓ 是
样式计算 + 布局 + 绘制 + 合成
↓
回到宏任务队列
5.2 宏任务(Macro Task)
| 来源 | 示例 |
|---|---|
| 脚本执行 | 整体 <script> |
| DOM 事件 | click、keydown 等 |
| 定时器 | setTimeout、setInterval |
| I/O | fetch 完成、文件读写 |
| UI 渲染 | 浏览器的渲染任务 |
requestAnimationFrame |
严格的渲染前回调(优先级高于 setTimeout) |
5.3 微任务(Micro Task)
| 来源 | 示例 |
|---|---|
| Promise | .then()、.catch()、.finally() |
async/await |
await 之后的代码 |
MutationObserver |
DOM 变化回调 |
queueMicrotask() |
手动添加微任务 |
process.nextTick() |
仅 Node.js |
5.4 经典面试题
console.log('1'); // ① 同步
setTimeout(() => {
console.log('2'); // ⑥ 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // ④ 微任务
});
queueMicrotask(() => {
console.log('4'); // ⑤ 微任务
});
requestAnimationFrame(() => {
console.log('5'); // ⑦ 宏任务(渲染前)
});
console.log('6'); // ② 同步
// 输出:1, 6, 3, 4, 2, 5
// 注意:requestAnimationFrame 在宏任务队列中优先级高于 setTimeout
// 更复杂的嵌套
Promise.resolve().then(() => {
console.log('A');
setTimeout(() => console.log('B'), 0);
});
setTimeout(() => {
console.log('C');
Promise.resolve().then(() => console.log('D'));
}, 0);
// 输出:A, C, D, B
// 第一个宏任务执行 → 注册微任务 A → 第二个宏任务 C → 微任务 D → 宏任务 B
5.5 Node.js 事件循环差异
Node.js 事件循环有 6 个阶段:
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout/setInterval 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← I/O 回调延迟到下一轮
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 轮询 I/O 事件(核心阶段)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │ ← close 事件
│ └───────────────────────────┘
关键区别:
- 微任务在每个阶段之后清空(
process.nextTick优先级高于 Promise.then) - Node 没有
requestAnimationFrame setImmediate和setTimeout(fn, 0)在timers阶段执行时机不同
// Node.js 下
setImmediate(() => console.log('setImmediate'));
setTimeout(() => console.log('setTimeout'), 0);
// 输出顺序不确定(取决于 poll 阶段到 timers 阶段的耗时)
5.6 渲染时机
浏览器约 60fps(16.67ms)执行一次渲染流程,但不是每个宏任务后都渲染。
渲染触发条件:
- 宏任务完成 && 微任务队列清空
- 距离上次渲染超过 16.67ms(约)
- 页面有可见的样式变化
// 下面的操作会合并为一次渲染
element.style.width = '100px';
element.style.height = '200px';
// 浏览器在一次渲染帧中统一处理样式变更
// 强制分帧渲染
element.style.width = '100px';
requestAnimationFrame(() => {
element.style.height = '200px';
// 下一次渲染帧
});
6. V8 引擎
6.1 架构概览
V8 是 Google 开发的开源 JavaScript 引擎,用于 Chrome 和 Node.js。
JavaScript Source
↓
词法分析 → 语法分析(Parser)
↓
AST (抽象语法树)
↓ ↓
Ignition (解释器) ←--- 快速启动,执行字节码
↓ ↓ |
字节码 (Bytecode) | 热点代码(Hot Code)
↓ ↓ |
TurboFan (优化编译器) --→ 机器码(Optimized Machine Code)
↓
执行
↓
若假设不成立 → 去优化(Deoptimization)→ 回退到字节码
6.2 Ignition 解释器
- 将 AST 编译为字节码
- 字节码大小约为机器码的 1/4 到 1/8
- 执行字节码的同时收集类型反馈(Feedback)
function add(a, b) {
return a + b;
}
// 第一次 add(1, 2) → 类型反馈:a,b 都是整数,返回整数
// 之后 add("a", "b") → 类型变更 → 去优化
6.3 TurboFan 优化编译器
- 将热点代码(Hot / Warm)编译为高度优化的机器码
- 基于类型假设生成优化代码
- 假设不成立时执行去优化(回退到字节码)
优化策略:
- 内联缓存(Inline Cache)
- 内联展开(Inlining)
- 循环展开
- 逃逸分析(Escape Analysis)→ 栈上分配
6.4 隐藏类(Hidden Class / Map)
V8 使用隐藏类优化对象属性访问:
function Point(x, y) {
// 第一阶段:this → C0(空对象)
this.x = x; // → C1(添加属性 x)
this.y = y; // → C2(添加属性 y)
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4); // 与 p1 共享隐藏类 C2
优化建议:
// 好:构造函数内初始化所有属性(隐藏类一致)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// 不好:动态添加属性导致隐藏类变更
const user = {};
user.name = 'Alice'; // 隐藏类 C0 → C1
user.age = 25; // C1 → C2(慢!)
// 不好:不同类型混用属性
const arr = [
{ x: 1, y: 2 }, // C1: x,y
{ x: 3, y: 4 }, // C1: x,y(共享)
{ a: 1, b: 2 }, // C2: a,b(不同隐藏类,去优化)
];
// 好:保持属性赋值顺序一致
function getPoint(x, y) {
return { x, y }; // 总是先 x 后 y
}
6.5 内联缓存(Inline Cache / IC)
V8 在属性访问处插入缓存,记录类型信息。
function getX(obj) {
return obj.x; // 第一次调用后,这里记录 obj 的隐藏类
}
getX({ x: 1 }); // 缓存 {x: 1} 的隐藏类 → 单态
getX({ x: 2 }); // 命中缓存,快速
getX({ a: 1, x: 3 }); // 不同隐藏类 → 多态(Morphic)
| 状态 | 性能 |
|---|---|
| 单态(Monomorphic) | 最快 |
| 多态(Polymorphic) | 较慢 |
| 巨态(Megamorphic) | 最慢(超过 4 种类型) |
6.6 垃圾回收(GC)
V8 采用分代式垃圾回收(Generational GC)。
内存分区:
V8 堆内存
├── 新生代(Young Generation)—— 空间小,存活短
│ ├── From-Space
│ ├── To-Space
│ └── (Scavenge 算法)
└── 老生代(Old Generation)—— 空间大,存活久
├── 标记 - 清除(Mark-Sweep)
├── 标记 - 整理(Mark-Compact)
└── 增量标记 + 并发标记
Scavenge(新生代):
From Space (对象) To Space (存活对象)
[A][B][C] ──→ [A][C]
[D] 已死 清空 From
交换 From/To 角色
- 使用 Cheney 算法
- 复制存活对象到 To-Space
- 晋升条件:经过两次 Scavenge 仍存活 或 To-Space 空间使用超过 25%
Mark-Sweep & Mark-Compact(老生代):
Mark-Sweep:
标记 → 存活对象标记 → 清除未标记对象 → 产生碎片
Mark-Compact:
标记 → 存活对象标记 → 清除 → 整理(碎片压缩)
三色标记法:
白色:未被标记(可回收)
灰色:标记中(自身已标记,引用的对象未标记)
黑色:标记完成(自身和引用都标记完)
增量标记:将标记过程拆分成小段,与 JS 执行交替进行,避免全停顿。
并发标记:在辅助线程上并行标记,不阻塞主线程。
优化建议:
// 避免产生大量临时对象(给 GC 压力)
function bad() {
for (let i = 0; i < 10000; i++) {
const tmp = { a: i, b: i }; // 每次循环创建新对象
arr.push(tmp.x);
}
}
// 复用对象
function good() {
const tmp = { a: 0, b: 0 };
for (let i = 0; i < 10000; i++) {
tmp.a = i;
tmp.b = i;
arr.push(tmp.a);
}
}
7. 内存管理
7.1 常见内存泄漏场景
// 1. 全局变量(意外挂到 window 上)
function leak() {
leaked = 'I am global'; // 严格模式下会报错
}
// 或 this 指向 window
function Leak() {
this.leaked = true;
}
new Leak(); // 无 new 则 this → window
// 2. 定时器未清除
const element = document.getElementById('btn');
const timer = setInterval(() => {
element.innerHTML = Date.now();
// element 可能已被移除,但定时器仍持有引用
}, 1000);
// clearInterval(timer); // ← 别忘了
// 3. 闭包引用
function outer() {
const bigData = new Array(1000000).fill('x');
return function inner() {
// 闭包持有 bigData 的引用
console.log('hello');
};
}
// inner 不使用 bigData 但闭包仍然持有它
// 4. DOM 引用未释放
const elements = [];
document.querySelectorAll('.item').forEach(el => {
elements.push(el); // 即使 DOM 被移除,数组仍持有引用
});
// 5. 事件监听器未卸载
class Component {
mount() {
window.addEventListener('resize', this.handleResize);
}
// 未调用 removeEventListener? → 泄露
}
7.2 WeakMap / WeakSet
弱引用:不影响垃圾回收,键必须是对象。
// 场景:DOM 节点关联数据(节点移除后自动回收)
const cache = new WeakMap();
function processElement(el) {
let data = cache.get(el);
if (!data) {
data = { computed: expensiveCalc(el) };
cache.set(el, data);
}
return data;
}
// el 从 DOM 移除后,对应的数据和 cache 条目自动回收
// WeakSet 类似:集合中的对象不影响 GC
const processed = new WeakSet();
function markProcessed(el) {
processed.add(el);
}
function isProcessed(el) {
return processed.has(el);
}
7.3 Chrome DevTools Memory 分析
Heap Snapshot(堆快照):
- 打开 DevTools → Memory → Heap Snapshot
- 对比快照:Take Snapshot → 操作页面 → Take Snapshot → Comparison 视图
- 搜索 Detached DOM(从 DOM 树移除但 JS 仍持有引用的节点)
Allocation Timeline(分配时间线):
- 打开 DevTools → Memory → Allocation instrumentation on timeline
- 开始记录 → 操作 → 停止
- 查看蓝色柱状图(内存分配),筛选可疑的持续增长
Performance Monitor:
- JS Heap Size:周期性增长的曲线表示可能泄漏
- DOM Nodes 数量:只增不减 → 泄露
8. 安全策略
8.1 同源策略
定义:协议 + 域名 + 端口 三者一致。
https://example.com:443
↑ ↑ ↑
协议 域名 端口
同源限制:
- DOM 访问:不同源的 iframe / 弹出窗口不能互操作
- 网络请求:XHR / fetch 默认被拦截
- 存储:Cookie / localStorage / IndexedDB 隔离
例外:
<script src="...">、<img src="...">、<link href="...">可加载跨域资源form可提交到跨域地址(但无法读取响应)
8.2 XSS(跨站脚本攻击)
三种类型:
| 类型 | 触发 | 原理 |
|---|---|---|
| 存储型 | 评论区/留言板 | 恶意代码存入数据库,用户访问时执行 |
| 反射型 | URL 参数 | 恶意代码在请求中,服务端反射到页面 |
| DOM 型 | 客户端 | 恶意代码通过 DOM 操作注入(如 innerHTML) |
防御手段:
// 1. 输出转义(最基础)
function escapeHTML(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 2. 不安全的操作
element.innerHTML = userInput; // ❌
element.outerHTML = userInput; // ❌
document.write(userInput); // ❌
// 3. 安全的操作
element.textContent = userInput; // ✅
element.setAttribute('title', escapeHTML(input)); // ✅
document.createTextNode(input); // ✅
// 4. 使用 DOMPurify 清理 HTML
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
CSP(内容安全策略):
<!-- 仅允许同源资源 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<!-- 仅允许特定来源的脚本 -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://trusted.cdn.com">
<!-- 禁止内联脚本(防止 DOM 型 XSS) -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self'">
<!-- 报告违规 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; report-uri /csp-report">
CSP 常用指令:
default-src:所有资源类型的兜底script-src:脚本来源style-src:样式来源img-src:图片来源connect-src:API 请求来源frame-ancestors:谁可以嵌入页面(防点击劫持)upgrade-insecure-requests:自动升级 HTTP 到 HTTPS
XSS 其他防御:
HttpOnlyCookie:防止 JS 读取 Cookie- 输入长度限制:减少攻击面
- 富文本过滤:白名单标签和属性
- React / Vue 默认转义(但
dangerouslySetInnerHTML和v-html仍需小心)
8.3 CSRF(跨站请求伪造)
原理:用户在 A 站已登录 → 诱导访问恶意 B 站 → B 站发请求到 A 站 → Cookie 自动携带 → A 站执行操作。
防御:
| 方案 | 说明 |
|---|---|
| CSRF Token | 服务端生成随机 token,请求时验证,攻击者无法获取 |
| SameSite Cookie | SameSite=Strict 或 Lax 阻止跨站 Cookie |
| Referer / Origin 验证 | 检查请求来源是否合法 |
| 二次确认 | 敏感操作要求输入密码 / 验证码 |
| 自定义 Header | X-Requested-With: XMLHttpRequest(需预检) |
// CSRF Token 实现
// 服务端生成并注入页面
<meta name="csrf-token" content="随机token值">
// 前端在请求头携带
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({ amount: 100 })
});
8.4 CORS 头部详解
# 服务端 CORS 响应头
# 允许的源(必选)
Access-Control-Allow-Origin: https://example.com
# 允许的方法(预检响应)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# 允许的请求头(预检响应)
Access-Control-Allow-Headers: Content-Type, Authorization
# 暴露给前端的响应头
Access-Control-Expose-Headers: X-Total-Count, X-RateLimit
# 允许携带凭证
Access-Control-Allow-Credentials: true
# 预检缓存时间(秒)
Access-Control-Max-Age: 86400
前端请求头:
# 简单请求自动带
Origin: https://frontend.com
# 预检请求
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
8.5 其他安全威胁
| 攻击类型 | 原理 | 防御 |
|---|---|---|
| 点击劫持 | 透明 iframe 覆盖在诱饵按钮上 | X-Frame-Options: DENY / frame-ancestors 'none' |
| 中间人攻击 | 拦截通信链路 | HSTS / 证书验证 |
| SQL 注入 | 恶意 SQL 拼接 | 参数化查询 / ORM |
| SSRF | 服务端发起内网请求 | 白名单 / URL 验证 |
9. 进程间通信
9.1 postMessage
跨窗口 / iframe 通信:
// 发送方(父窗口或 iframe 内)
const targetWindow = document.getElementById('iframe').contentWindow;
// 或 window.open() 返回的引用
targetWindow.postMessage({
type: 'THEME_CHANGE',
payload: { theme: 'dark' }
}, 'https://trusted-origin.com');
// 接收方
window.addEventListener('message', (event) => {
// ⚠️ 必须验证 origin!
if (event.origin !== 'https://trusted-origin.com') return;
const { type, payload } = event.data;
switch (type) {
case 'THEME_CHANGE':
applyTheme(payload.theme);
break;
}
});
安全要点:
- 永远验证
event.origin - 使用
postMessage的targetOrigin参数限制接收方 - 不要用
postMessage传递未经验证的数据给innerHTML
9.2 BroadcastChannel
同源页面间广播通信:
// 页面 A
const channel = new BroadcastChannel('shared_channel');
channel.postMessage({ type: 'LOGOUT' });
// 页面 B(同源)
const channel = new BroadcastChannel('shared_channel');
channel.onmessage = (event) => {
if (event.data.type === 'LOGOUT') {
clearUserSession();
}
};
特点:
- 同源即可,无需引用目标窗口
- 发送方也会收到自己发的消息(可通过
event.target !== channel判断) - 最多 1MB 数据
9.3 SharedArrayBuffer
多线程共享内存:
// 主线程
const sab = new SharedArrayBuffer(1024);
const view = new Int32Array(sab);
const worker = new Worker('worker.js');
worker.postMessage(sab);
// worker.js
self.onmessage = (e) => {
const view = new Int32Array(e.data);
Atomics.store(view, 0, 123); // 原子写入
Atomics.notify(view, 0, 1); // 通知等待线程
};
注意:SharedArrayBuffer 需要设置 COOP / COEP 头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
9.4 Service Worker 通信
// SW → 页面
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'CACHE_UPDATED' });
});
});
// 页面 → SW(通过 MessageChannel 或 postMessage)
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
navigator.serviceWorker.addEventListener('message', event => {
console.log('来自 SW:', event.data);
});
9.5 通信方式对比
| 方式 | 范围 | 数据量 | 方向 |
|---|---|---|---|
postMessage |
任意窗口(跨域) | ≤ 1MB | 双向 |
BroadcastChannel |
同源页面 | ≤ 1MB | 广播 |
SharedArrayBuffer |
同源 Worker | 共享内存 | 双向(原子操作) |
Service Worker postMessage |
SW ↔ 页面 | ≤ 1MB | 双向 |
MessageChannel |
任意 | ≤ 1MB | 管道 |
fetch + SSE |
服务端 ↔ 页面 | 无限制 | 单/双向 |
相关:Chrome DevTools 是调试和分析浏览器行为的主要工具,详见 z-浏览器DevTools