CC 咖啡猫的工作空间 Coding Space

浏览器原理(前端视角)

从输入 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)

布局作用:计算每个可见节点的几何信息(xywidthheight)。

布局过程

  1. 父节点计算自身尺寸
  2. 父节点遍历子节点,计算子节点位置
  3. 递归执行

布局类型

  • 全局布局:整个渲染树重新计算(如窗口 resize)
  • 增量布局:局部 dirty 标记,只重算受影响的节点

3.6 分层(Layer Tree)

浏览器将页面分成多个图层,独立绘制和合成:

  • 拥有 z-index 的定位元素会创建新层
  • transformopacitywill-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-sizefont-family
display: noneblock
添加/删除 DOM 元素
修改文本内容
window.resize
读取 offsetHeight/scrollTop/getComputedStyle
修改 color/background-color
修改 visibility
修改 transform
修改 opacity
滚动

关键原则transformopacity 只触发合成(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, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle(), 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 事件 clickkeydown
定时器 setTimeoutsetInterval
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
  • setImmediatesetTimeout(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(堆快照)

  1. 打开 DevTools → Memory → Heap Snapshot
  2. 对比快照:Take Snapshot → 操作页面 → Take Snapshot → Comparison 视图
  3. 搜索 Detached DOM(从 DOM 树移除但 JS 仍持有引用的节点)

Allocation Timeline(分配时间线)

  1. 打开 DevTools → Memory → Allocation instrumentation on timeline
  2. 开始记录 → 操作 → 停止
  3. 查看蓝色柱状图(内存分配),筛选可疑的持续增长

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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
}

// 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 其他防御

  • HttpOnly Cookie:防止 JS 读取 Cookie
  • 输入长度限制:减少攻击面
  • 富文本过滤:白名单标签和属性
  • React / Vue 默认转义(但 dangerouslySetInnerHTMLv-html 仍需小心)

8.3 CSRF(跨站请求伪造)

原理:用户在 A 站已登录 → 诱导访问恶意 B 站 → B 站发请求到 A 站 → Cookie 自动携带 → A 站执行操作。

防御

方案 说明
CSRF Token 服务端生成随机 token,请求时验证,攻击者无法获取
SameSite Cookie SameSite=StrictLax 阻止跨站 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
  • 使用 postMessagetargetOrigin 参数限制接收方
  • 不要用 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


参考