Skip to content

三、异步、网络与安全

建议主题覆盖的核心知识点概览
1.事件循环揭秘:宏任务与微任务setTimeout, Promise, async/await 执行顺序搞懂调用栈、宏任务队列和微任务队列的交互。通过一个经典面试题,一步步追踪代码执行顺序。
2.HTTP 的演进之路HTTP/1.1, HTTP/2, HTTP/3 的改进讲清“为什么”。按照历史顺序,讲述每个版本解决了前一个版本的什么核心痛点(队头阻塞等),并用生动比喻解释多路复用等概念。
3.HTTP 缓存的力量——减少请求的艺术强缓存 (Cache-Control),协商缓存 (Etag, Last-Modified)减少请求的艺术。详细解释两种缓存机制的工作原理和区别,了解浏览器如何通过缓存大幅提升加载速度。
4.浏览器存储方案全解Cookie, LocalStorage, SessionStorage 的对比,了解 IndexedDB横向对比。从容量、生命周期、与服务器通信等多个维度对比几种常用存储方案,并明确各自最适合的应用场景。
5.多线程之 Web Worker 与 Service Worker两者的职责划分与应用场景,• Web Worker用于计算,Service Worker用于网络代理“分身术”与“代理”。清晰地区分两者:Web Worker解决主线程计算压力,防止UI卡顿; Service Worker作为网络代理,赋能离线应用(PWA)。
6.前端安全之 XSS 与 CSRFXSS 和 CSRF 的攻击原理与防范策略攻防演练。对每种攻击,先演示一个易受攻击的代码示例,再展示如何通过转义、CSP、Token等手段进行有效的防御。

1.事件循环揭秘:宏任务与微任务

1. 核心概念

为了在单线程模型中处理耗时的异步操作,JavaScript 引入了事件循环机制。理解事件循环的关键在于掌握以下几个概念:

概念描述
调用栈 (Call Stack)一个后进先出 (LIFO) 的数据结构,用于存储和管理函数调用。当执行一个函数时,它被推入栈顶;当函数返回时,它被弹出。所有同步代码都在调用栈中执行。
任务队列 (Task Queue)一个先进先出 (FIFO) 的数据结构,用于存放待处理的异步任务的回调函数。它分为两种类型:宏任务队列和微任务队列。
事件循环 (Event Loop)一个持续运行的进程,它不断地检查调用栈是否为空。如果为空,它会去检查微任务队列,然后是宏任务队列,并将队列中的任务回调函数推入调用栈中执行。

2. 宏任务 (Macrotask) 与微任务 (Microtask)

这是事件循环中最核心的区别,直接决定了代码的执行顺序。

  • 宏任务 (Macrotask):

    • 描述: 通常是较大、独立的任务块。每次事件循环只会从宏任务队列中取出一个任务来执行。

    • 常见例子:

      • setTimeout()

      • setInterval()

      • setImmediate() (Node.js 环境)

      -I/O 操作 (如文件读写、网络请求)

      • UI 渲染 (浏览器环境)
  • 微任务 (Microtask):

    • 描述: 通常是需要尽快执行的、较小的任务,用于在当前任务之后、下一次宏任务之前完成一些更新或清理工作。

    • 常见例子:

      • Promise.then(), Promise.catch(), Promise.finally()

      • async/await (其 await 之后的部分)

      • queueMicrotask()

      • MutationObserver

3. 执行顺序

事件循环的执行流程可以概括为以下步骤,并不断重复:

  1. 执行同步代码: 首先,将整个 script 脚本作为第一个宏任务,在调用栈中从头到尾执行所有同步代码。

  2. 清空微任务队列: 执行完同步代码后,检查微任务队列。如果队列不为空,则一次性执行所有的微任务,直到队列变空。

  • 注意: 如果在执行微任务的过程中又产生了新的微任务,那么这些新的微任务也会被添加到队列的末尾,并在此轮中一并执行。
  1. 执行一个宏任务: 微任务队列清空后,从宏任务队列中取一个任务,推入调用栈中执行。

  2. 重复: 重复步骤 2 和 3。

一句话总结:一次事件循环 = 执行完一个宏任务 -> 执行完所有微任务。


4. 经典面试题解析

让我们通过一个包含 setTimeout, Promise, 和 async/await 的经典例子来追踪代码的执行顺序。 代码示例:

javascript
async function async1() {
  console.log('2. async1 start');
  await async2();
  console.log('6. async1 end');
}

async function async2() {
  console.log('3. async2');
}

console.log('1. script start');

setTimeout(function() {
  console.log('8. setTimeout');
}, 0);

async1();

new Promise(function(resolve) {
  console.log('4. promise1');
  resolve();
}).then(function() {
  console.log('7. promise2');
});

console.log('5. script end');

逐步追踪执行流程:

  1. 同步代码执行:
  • console.log('1. script start') 执行。 输出: 1. script start

  • 遇到 setTimeout,将其回调函数放入 宏任务队列。

  • 调用 async1()

  • console.log('2. async1 start') 执行。 输出: 2. async1 start

  • 遇到 await async2()

    • async2() 函数立即执行,console.log('3. async2') 执行。 输出: 3. async2

    • await 关键字会暂停 async1 函数的执行,并将 await 后面的代码 (console.log('6. async1 end')) 放入 微任务队列

  • 遇到 new Promise:

    • Promise 的构造函数是同步执行的,console.log('4. promise1') 执行。 输出: 4. promise1

    • resolve() 被调用,Promise 状态变为 resolved,其 .then 的回调函数被放入 微任务队列

  • console.log('5. script end') 执行。 **输出: 5. script end

    此时,同步代码已全部执行完毕。

  • 调用栈: 空

  • 宏任务队列: [setTimeout 回调]

  • 微任务队列: [async1 end 回调, promise2 回调]

  1. 清空微任务队列:
  • 事件循环发现调用栈为空,开始处理微任务队列。

  • 取出第一个微任务 (async1 end 回调) 执行,console.log('6. async1 end')。 **输出: 6. async1 end

  • 取出第二个微任务 (promise2 回调) 执行,console.log('7. promise2')。 **输出: 7. promise2

    此时,微任务队列已清空。

  1. 执行一个宏任务:
  • 事件循环从宏任务队列中取出 setTimeout 的回调函数来执行。

  • console.log('8. setTimeout') 执行。 **输出: 8. setTimeout

最终输出顺序:

text
1. script start
2. async1 start
3. async2
4. promise1
5. script end
6. async1 end
7. promise2
8. setTimeout

通过这个例子,可以清晰地看到同步代码、async/await、Promise 和 setTimeout 之间复杂的交互关系,以及微任务优先于宏任务执行的核心原则。

2.HTTP 的演进之路

核心思想

技术的演进总是为了解决“效率”和“性能”的问题。HTTP 的发展史,本质上就是一部为了让浏览器更快地加载网页内容而不断优化的奋斗史。其核心目标始终是:降低延迟、提高并发。


一:HTTP/1.1

在 HTTP/1.1 之前,有一个叫 HTTP/1.0 的版本,每次请求资源(比如获取一个 CSS 文件、一个 JS 文件)都需要建立一个新的 TCP 连接,用完就关掉,效率极低。

  1. 解决了什么核心痛点?

解决了 HTTP/1.0 频繁建立和断开 TCP 连接 所带来的巨大性能开销。TCP 连接的建立需要“三次握手”,这是一个耗时的过程。

  1. 关键改进
  • 长连接 (Persistent Connection): 这是 HTTP/1.1 最核心的改进。默认情况下,一个 TCP 连接可以被多个 HTTP 请求复用,不必每次都重新建立。
  • 管道化 (Pipelining): 允许客户端在一个 TCP 连接上,连续发送多个请求,而不用等待前一个请求返回。

从“单人收银台”到“批量下单”

  • HTTP/1.0:想象一个效率极低的收银台。你必须把一件商品放到传送带上,收银员扫码、结账,你付完钱后,才能再放上第二件商品。
  • HTTP/1.1 的长连接 + 管道化:收银台升级了。你现在可以一次性把你购物车里所有的商品(请求)都放到传送带上,收银员(服务器)会按顺序逐个处理。你不用拿一件、等一次、再拿下一件。这大大减少了你和收银员之间的等待时间。
  1. 留下的核心问题:队头阻塞 (Head-of-Line Blocking)

虽然管道化允许一次性发送所有请求,但服务器必须按顺序返回响应。如果第一个请求(比如一个很大的 JS 文件)处理得很慢,那么后面的所有请求(哪怕是一些很小的图片)即使已经处理完了,也必须排队等着,直到第一个请求的响应发送完毕。

传送带上的“大件商品”

就像在超市传送带上,你虽然把所有商品都放上去了,但如果排在最前面的是一个需要复杂包装的“大件商品”(慢请求),收银员就会卡在这里,后面所有“小商品”(快请求)都得等着,无法先被处理。这就是“队头阻塞”。

二:HTTP/2

HTTP/1.1 的队头阻塞问题成为了性能瓶颈。无论我们开多少个 TCP 连接(浏览器通常限制为 6-8 个),每个连接内部的阻塞问题依然存在。为了从根本上解决这个问题,HTTP/2 诞生了。

  1. 解决了什么核心痛点?

解决了 HTTP/1.1 层的队头阻塞问题,大幅提高了单一连接的传输效率。

  1. 关键改进
  • 多路复用 (Multiplexing): 这是 HTTP/2 最革命性的改进。在一个 TCP 连接内,允许多个请求/响应双向、并行地传输,而不会互相阻塞。

  • 二进制分帧 (Binary Framing): 将所有传输的信息(HTTP 消息)分割为更小的消息和帧,并采用二进制格式编码。这就像把大货物拆分成标准尺寸的小包裹,便于管理和运输。

  • 头部压缩 (Header Compression): 使用 HPACK 算法压缩请求头,减少了每次请求的数据量。

从“单车道”到“多路复用快车道”

  • HTTP/1.1:是一条单向的乡间小路。一次只能跑一辆车,即使你想同时运送多批货物,也必须让它们排成一队,前车不走,后车只能等待。

  • HTTP/2 的多路复用:把这条路彻底改造成了一条拥有多个车道的高速公路。你的所有货物(请求)被拆分成一个个标准集装箱(二进制帧),并被贴上标签(Stream ID)。现在,所有集装箱可以同时在不同车道上飞驰,并行运输。到达目的地后,再根据标签重新组装成完整的货物。一个集装箱的延迟或丢失,不会影响其他车道的集装箱。

  1. 留下的核心问题:TCP 层的队头阻塞

HTTP/2 虽然解决了应用层的队头阻塞,但它的根基——TCP 协议本身,却存在着固有的队头阻塞问题。TCP 是一个可靠的协议,它要求数据包必须按顺序到达。如果在这个“多路复用高速公路”上,有一个集装箱(TCP 数据包)在运输途中丢失了,那么 TCP 协议会要求暂停所有车道,等待这个丢失的集装箱被重新发来,然后才能继续前进。这在网络不佳(如移动网络)的情况下,会造成所有 HTTP/2 的“车道”被卡住,性能急剧下降。


三:HTTP/3

为了彻底摆脱 TCP 的历史包袱,HTTP/3 做出了一个大胆的决定:抛弃 TCP,改用一个全新的基于 UDP 的协议——QUIC。

  1. 解决了什么核心痛点?

解决了 TCP 层的队头阻塞 问题,并在连接建立速度、网络切换支持等方面做了巨大优化。

  1. 关键改进
  • 基于 QUIC 协议: QUIC (Quick UDP Internet Connections) 是一个基于 UDP 的传输层协议。UDP 本身是非连接、不可靠的,但 QUIC 在 UDP 之上实现了可靠性、拥塞控制、流量控制等功能,集 TCP、TLS、HTTP/2 的优点于一身。

  • 真正的多路复用: QUIC 的每个“流”(Stream) 都是完全独立的。一个流的数据包丢失,不会影响其他任何流的传输。

  • 更快的连接建立: QUIC 将 TCP 的三次握手和 TLS 的加密握手(需要 1-3 个 RTT)合并,大大减少了连接建立的时间,理想情况下只需要 0-RTT。

从“高速公路系统”到“无人机物流网络”

  • HTTP/2 (基于 TCP):是一个统一调度的高速公路系统。虽然有多条车道,但整个系统共享同一个交通控制中心(TCP 拥塞控制)。一旦发生一次严重事故(丢包),控制中心会拉响警报,封锁整个高速公路,所有车道都动弹不得,直到事故处理完毕

  • HTTP/3 (基于 QUIC):彻底抛弃了公路系统,改用一个独立的无人机物流网络。每个集装箱(数据包)都由一架独立的无人机(QUIC Stream)负责运输。它们各自规划路线,独立飞行。即使某一架无人机在中途出现故障或丢失,也绝对不会影响其他无人机的飞行。这在信号不稳定的移动网络环境下,表现得尤为出色。

总结

版本核心改进解决了什么问题引入/留下的问题
HTTP/1.1长连接、管道化HTTP/1.0 的高延迟和连接开销应用层的队头阻塞
HTTP/2多路复用、二进制分帧HTTP/1.1 的队头阻塞,提升了单一连接效率TCP 层的队头阻塞
HTTP/3基于 QUIC 协议TCP 层的队头阻塞,优化了连接建立和网络切换QUIC 协议的普及和网络设备支持度

3.HTTP 缓存的力量——减少请求的艺术

核心思想

网络请求是昂贵的,它消耗时间和带宽。最高效的请求就是不发送请求。HTTP 缓存机制是浏览器的一种“记忆能力”,它让浏览器可以“记住”已经获取过的资源。当下次需要同一个资源时,浏览器可以根据一套规则,决定是从本地“记忆”中直接读取,还是需要去询问服务器资源是否更新。这就是减少请求的艺术。


类别一:强缓存 (Strong Cache)

强缓存是最高效的方式。如果一个资源命中了强缓存,浏览器会直接从本地副本加载,根本不会向服务器发送任何请求。这在开发者工具的 Network 面板中,通常表现为 200 OK (from memory cache)200 OK (from disk cache)

  1. 工作原理

当浏览器第一次请求一个资源时,服务器在返回资源的同时,会通过响应头 Cache-Control 告诉浏览器这个资源的“过期时间”。

  1. 核心响应头:Cache-Control

这是 HTTP/1.1 中用于控制缓存的核心字段,它有几个关键指令:

  • max-age=<seconds>: 最重要的指令。指定资源在被视为“过期”之前,可以被缓存的最大时间(秒)。例如,max-age=31536000 表示缓存一年。

  • public vs private:

    • public: 表明响应可以被任何中间人(如 CDN、代理服务器)缓存。

    • private: 表明响应是针对单个用户的,只能被最终用户的浏览器缓存。

  • no-cache: 注意!不是“不缓存”的意思。它的意思是“不要使用强缓存,但在使用缓存副本前,必须去服务器验证一下它是否过期”(即,强制进入协商缓存阶段)。

  • no-store: 这才是“完全不缓存”。浏览器和任何中间缓存都不会存储这个响应的任何部分。常用于涉及敏感信息的请求。


类别二:协商缓存 (Negotiation Cache)

当资源的强缓存“通行证”过期了,或者压根就没有设置强缓存(例如设置了 Cache-Control: no-cache),浏览器就必须向服务器发送一次请求来“协商”一下。浏览器会问:“我本地存的这个版本还能用吗?”

  1. 工作原理

协商缓存需要浏览器和服务器之间的一次通信。浏览器在请求头中携带本地副本的“版本信息”,服务器根据这个信息判断资源是否已更新。

  • 如果未更新:服务器返回一个 304 Not Modified 状态码,响应体为空。浏览器收到后,便安心地从本地加载缓存副本。这个过程虽然有一次请求,但由于没有下载资源本身,所以速度很快,极大地节省了带宽。

  • 如果已更新:服务器返回 200 OK 状态码,并在响应体中携带全新的资源内容,同时更新“版本信息”。

核对购物小票

  1. 保留小票:你(浏览器)买了一件衣服(资源),并保留了购物小票。小票上有两个重要信息:购买时间 (Last-Modified) 和 商品唯一码 (ETag)。

  2. 拿着小票去询问:过了几天,你想知道这件衣服有没有出新款。你拿着小票去问店员(服务器):“我是在这个时间点买的、商品码是这个的衣服,现在还是最新的吗?”(发送带有 If-Modified-SinceIf-None-Match 的请求)。

  3. 店员的回应:

  • 未出新款:店员看了一眼说:“没变,还是这个,你那个就是最新的。”(返回 304)。你便放心地继续穿旧衣服。

  • 已出新款:店员说:“哦,我们出新款了,你那个过时了。” 然后直接拿给你一件新款的衣服和一张新的小票。(返回 200 和新资源、新 ETag)。

  1. 核心“版本信息”头

协商缓存依赖于两组配对使用的头部字段:

  • 第一组:基于时间戳

    • Last-Modified (服务器响应头): 资源在服务器上最后被修改的时间。

    • If-Modified-Since (浏览器请求头): 当浏览器发起协商请求时,会带上这个头,其值就是上次收到的 Last-Modified 的值。

  • 第二组:基于唯一标识

    • ETag (Entity Tag, 服务器响应头): 服务器为当前版本的资源生成的唯一标识符,像一个“指纹”。只要资源内容有变动,ETag 就会改变。

    • If-None-Match (浏览器请求头): 浏览器发起协商请求时,带上这个头,其值就是上次收到的 ETag 的值。

为什么 ETag 通常优于 Last-Modified?

ETag 更精确。Last-Modified 只能精确到秒,如果在 1 秒内文件被多次修改,Last-Modified 无法感知。此外,有时文件内容没有变,只是保存了一下,修改时间也会变,这会导致不必要的重新下载。ETag 基于文件内容生成,完美避开了这些问题。因此,ETag 的优先级更高。


总结:缓存决策流程

当浏览器需要一个资源时,它会按照以下顺序决策:

  1. 检查强缓存:查看本地副本的 Cache-Control: max-age是否仍然有效。
  • 是 -> 命中强缓存。直接从本地加载,不发请求。 (最快)

  • 否 -> 进入协商缓存阶段。

  1. 发起协商缓存请求:向服务器发送一个请求,携带 If-Modified-Since 和/或 If-None-Match 头。
  • 服务器判断资源未改变 -> 返回 304 Not Modified。浏览器从本地加载。(较快)

  • 服务器判断资源已改变 -> 返回 200 OK 和新资源。浏览器使用新资源并更新本地缓存。(最慢,但必要)

特性强缓存 (Strong Cache)协商缓存 (Negotiation Cache)
是否联系服务器
HTTP状态码200 OK (from cache)304 Not Modified 或 200 OK
服务器响应304时响应体为空,200时为新资源
核心请求头(无)If-Modified-Since, If-None-Match
核心响应头Cache-ControlETag, Last-Modified
性能极高,无网络延迟较高,有网络延迟但节省带宽
适用场景长期不变的静态资源 (JS, CSS, 图片)可能会频繁更新的资源,或需要保证新鲜度的资源

4.浏览器存储方案全解

横向对比:四大存储方案

这张表格从关键维度对 Cookie、LocalStorage、SessionStorage 和 IndexedDB 进行了详细对比。

123123123123123123123123123123123
容量大小约 4KB约 5-10MB约 5-10MB巨大 (通常 > 1GB,取决于磁盘空间)
生命周期可设置过期时间,否则随浏览器会话结束永久性,除非用户或应用主动清除会话级,标签页或浏览器关闭后即清除永久性,除非用户或应用主动清除
与服务器通信每次 HTTP 请求都会自动携带仅在客户端,不与服务器自动通信仅在客户端,不与服务器自动通信仅在客户端,不与服务器自动通信
作用域同源下的所有标签页和窗口同源下的所有标签页和窗口仅限当前标签页同源下的所有标签页和窗口
API 易用性较差,需手动封装 document.cookie非常简单 (setItem, getItem)非常简单 (setItem, getItem)复杂,异步 API,基于事务
存储数据类型字符串字符串 (需用 JSON.stringify 存对象)字符串 (需用 JSON.stringify 存对象)任何类型 (字符串, 对象, 文件, Blob等)
性能影响高 (随请求发送,增加请求体积)低 (不影响网络请求)低 (不影响网络请求)低 (异步操作,不阻塞主线程)

适用场景总结 (When to use what?)

根据上述对比,可以清晰地为每种技术找到最适合它的“舞台”。

Cookie 的核心使命是维持状态,特别是让无状态的 HTTP 协议能够“记住”用户。因为它的数据会自动附加在 HTTP 请求头中发送给服务器,所以它最适合存储那些需要前后端共享的信息。

  • 最典型的应用场景:

    • 用户身份认证: 存储 Session IDToken,服务器通过它来识别用户。

    • 用户行为跟踪: 例如 Google Analytics 用它来追踪用户来源和浏览路径。

    • 购物车: 早期的购物车功能(现在更多使用 LocalStorage + 后端同步)。

2. LocalStorage:持久化的前端“仓库”

当你想在客户端存储一些不敏感、量不大、且需要长期保留的数据时,LocalStorage 是最佳选择。它是纯粹的前端存储,不会无故拖慢你的网络请求。

  • 最典型的应用场景:

    • 用户偏好设置: 如网站的主题(深色/浅色模式)、语言选择等。

    • 持久化应用状态: 缓存不常变化但需要长期保留的应用数据,减少不必要的 API 请求。

    • 存储 JWT Token: 与 Cookie 存储 Session ID 类似,前端拿到 Token 后可以存入 LocalStorage,在后续请求中手动添加到请求头。

3. SessionStorage:一次性会话的“临时备忘录”

SessionStorage 的特性决定了它非常适合存储一次性会话中的临时数据。它的数据“阅后即焚”,标签页一关,数据就消失,且不会在不同标签页之间“串门”。

  • 最典型的应用场景:

    • 多步骤表单: 用户正在填写一个复杂的注册或申请表单,将已填写的信息存入 SessionStorage,即使用户刷新了页面,数据也不会丢失。一旦用户提交表单或关闭标签,这些临时数据自动销毁。

    • 单页应用中,临时保存某个页面的状态,确保用户返回时能恢复之前的浏览位置或筛选条件。

4. IndexedDB:客户端的“重型数据库”

当你需要在浏览器端存储大量、结构化的数据,甚至需要进行查询、索引和事务管理时,IndexedDB 是唯一的选择。它是为构建强大的离线 Web 应用(PWA)而生的。

  • 最典型的应用场景:

    • 离线应用数据存储: 如一个在线文档编辑器,可以将整篇文档内容、历史版本等存储在 IndexedDB,让用户在没有网络时也能查看和编辑。

    • 大型静态资源缓存: 缓存 Web 应用的 JS/CSS 文件、图片、字体等,实现“秒开”体验。

    • 复杂应用的状态管理: 对于一些功能极其复杂的单页应用,可以用它来存储和管理整个应用的状态树。

5.多线程之 Web Worker 与 Service Worker

核心思想

JavaScript 本质上是单线程的,这意味着所有任务(UI 渲染、用户交互、JS 代码执行)都在同一个主线程上排队执行。如果一个任务耗时过长,整个页面就会被“冻结”,无法响应用户操作,造成卡顿。为了解决这个问题,浏览器引入了 "Workers",允许我们在后台创建新的线程来执行脚本,从而解放主线程。Web Worker 和 Service Worker 就是这套机制下的两种核心角色,但它们的分工和使命截然不同。


角色一:Web Worker - 主线程的“分身术”

主线程是一位分身乏术的火影忍者,他既要接待村民(响应用户交互),又要维护村容村貌(渲染 UI)。现在,他接到了一个需要耗费大量查克拉去完成的S级任务(复杂的计算)。如果他亲力亲为,那么在他完成任务期间,整个村子将无人管理,陷入停滞。

Web Worker 就是他的“分身术”(影分身)。主线程可以创造一个或多个分身(Web Worker 线程),把这个S级任务交给他。

  • 分身(Web Worker) 在一个完全独立的地方埋头计算,不打扰本体。

  • 本体(主线程) 则继续轻松地处理日常事务,确保村子(页面)的流畅运行。

  • 当分身完成任务后,通过“飞鸽传书”(postMessage API)将结果告诉本体。

1. 核心职责与应用场景
  • 职责:分担主线程的计算压力。专门处理那些 CPU 密集型或高延迟的任务,防止主线程被阻塞。

  • 应用场景:

    • 大量数据处理:解析巨大的 JSON 文件、处理大规模数据。

    • 复杂计算:图像/视频处理(滤镜、算法分析)、数据加密/解密、复杂的数学运算。

    • 实时数据预处理:在接收到大量 WebSocket 数据后,先在 Worker 中进行整理和计算,再将结果发送给主线程用于渲染。

2. 关键特性
  • 生命周期与页面绑定:Web Worker 随页面的创建而创建,随页面的关闭而销毁。它完全附属于创建它的页面。

  • 无法操作 UI:分身无法直接去修缮村子的房屋,即 Web Worker 不能访问 window、document 等 DOM API。它活在一个隔离的环境里。

  • 通信方式:通过 postMessage() 发送消息,通过 onmessage 事件接收消息,以此与主线程进行通信。


角色二:Service Worker - 浏览器的“智能网络代理”

如果说 Web Worker 是为了解决“内部计算压力”,那么 Service Worker 就是为了解决“外部网络依赖”。它不是一个简单的计算分身,而是一个更高阶的存在,扮演着 浏览器、应用与网络之间的“代理服务器”。

它就是一个存在你的应用和互联网之间的智能管家。所有从应用发出的网络请求,都会先经过他手。

  • 当网络正常时:他会忠实地去网络上取回资源,并顺手将一些重要资源(如 CSS、JS、图片)缓存在自己的小仓库里。

  • 当网络断开时:用户再次请求资源,虽然无法连接互联网,但这位管家会说:“别担心,上次你要的东西我都给你存着呢”,然后直接从自己的仓库里把资源返回给应用。这就实现了离线访问。

  • 超越页面:这位管家是独立于任何页面的。即使你关闭了应用的所有页面,他依然在后台待命,可以接收来自服务器的推送通知,或者在后台执行数据同步。

1. 核心职责与应用场景
  • 职责:拦截和处理网络请求,充当网络代理。它是实现渐进式网络应用(PWA)的基石。

  • 应用场景:

    • 离线缓存:缓存应用的“外壳”(App Shell)和核心资源,让应用在离线时也能瞬时加载和使用。

    • 消息推送 (Push Notifications):即使浏览器关闭,也能接收并向用户显示来自服务器的推送消息。

    • 后台同步 (Background Sync):在网络不佳时,允许用户提交表单,等网络恢复后,Service Worker 会在后台自动完成发送。

    • 网络请求劫持:可以自定义网络请求的返回内容,例如根据不同条件返回不同的缓存版本,或者直接在 Service Worker 中生成响应。

2. 关键特性
  • 独立生命周期:Service Worker 的生命周期与页面完全分离。它需要经历安装(install)、激活(activate)等阶段,并且可以活在浏览器后台,不受页面关闭的影响。

  • 强大的代理能力:可以拦截作用域范围内的所有 fetch 事件。

  • 无法操作 UI:和 Web Worker 一样,它也不能直接访问 DOM。

  • 安全性要求:必须运行在 HTTPS 协议下,以防止中间人攻击。


核心思想

JavaScript 本质上是单线程的,这意味着所有任务(UI 渲染、用户交互、JS 代码执行)都在同一个主线程上排队执行。如果一个任务耗时过长,整个页面就会被“冻结”,无法响应用户操作,造成卡顿。为了解决这个问题,浏览器引入了 "Workers",允许我们在后台创建新的线程来执行脚本,从而解放主线程。Web Worker 和 Service Worker 就是这套机制下的两种核心角色,但它们的分工和使命截然不同。


角色一:Web Worker - 主线程的“分身术”

主线程是一位分身乏术的火影忍者,他既要接待村民(响应用户交互),又要维护村容村貌(渲染 UI)。现在,他接到了一个需要耗费大量查克拉去完成的S级任务(复杂的计算)。如果他亲力亲为,那么在他完成任务期间,整个村子将无人管理,陷入停滞。

Web Worker 就是他的“分身术”(影分身)。主线程可以创造一个或多个分身(Web Worker 线程),把这个S级任务交给他。

  • 分身(Web Worker) 在一个完全独立的地方埋头计算,不打扰本体。
  • 本体(主线程) 则继续轻松地处理日常事务,确保村子(页面)的流畅运行。
  • 当分身完成任务后,通过“飞鸽传书”(postMessage API)将结果告诉本体。
  1. 核心职责与应用场景
  • 职责:分担主线程的计算压力。专门处理那些 CPU 密集型或高延迟的任务,防止主线程被阻塞。
  • 应用场景:
    • 大量数据处理:解析巨大的 JSON 文件、处理大规模数据。
    • 复杂计算:图像/视频处理(滤镜、算法分析)、数据加密/解密、复杂的数学运算。
    • 实时数据预处理:在接收到大量 WebSocket 数据后,先在 Worker 中进行整理和计算,再将结果发送给主线程用于渲染。
  1. 关键特性
  • 生命周期与页面绑定:Web Worker 随页面的创建而创建,随页面的关闭而销毁。它完全附属于创建它的页面。
  • 无法操作 UI:分身无法直接去修缮村子的房屋,即 Web Worker 不能访问 window、document 等 DOM API。它活在一个隔离的环境里。
  • 通信方式:通过 postMessage() 发送消息,通过 onmessage 事件接收消息,以此与主线程进行通信。

角色二:Service Worker - 浏览器的“智能网络代理”

如果说 Web Worker 是为了解决“内部计算压力”,那么 Service Worker 就是为了解决“外部网络依赖”。它不是一个简单的计算分身,而是一个更高阶的存在,扮演着 浏览器、应用与网络之间的“代理服务器”。

它就是一个存在你的应用和互联网之间的智能管家。所有从应用发出的网络请求,都会先经过他手。

  • 当网络正常时:他会忠实地去网络上取回资源,并顺手将一些重要资源(如 CSS、JS、图片)缓存在自己的小仓库里。
  • 当网络断开时:用户再次请求资源,虽然无法连接互联网,但这位管家会说:“别担心,上次你要的东西我都给你存着呢”,然后直接从自己的仓库里把资源返回给应用。这就实现了离线访问。
  • 超越页面:这位管家是独立于任何页面的。即使你关闭了应用的所有页面,他依然在后台待命,可以接收来自服务器的推送通知,或者在后台执行数据同步。
  1. 核心职责与应用场景
  • 职责:拦截和处理网络请求,充当网络代理。它是实现渐进式网络应用(PWA)的基石。
  • 应用场景:
    • 离线缓存:缓存应用的“外壳”(App Shell)和核心资源,让应用在离线时也能瞬时加载和使用。
    • 消息推送 (Push Notifications):即使浏览器关闭,也能接收并向用户显示来自服务器的推送消息。
    • 后台同步 (Background Sync):在网络不佳时,允许用户提交表单,等网络恢复后,Service Worker 会在后台自动完成发送。
    • 网络请求劫持:可以自定义网络请求的返回内容,例如根据不同条件返回不同的缓存版本,或者直接在 Service Worker 中生成响应。
  1. 关键特性
  • 独立生命周期:Service Worker 的生命周期与页面完全分离。它需要经历安装(install)、激活(activate)等阶段,并且可以活在浏览器后台,不受页面关闭的影响。
  • 强大的代理能力:可以拦截作用域范围内的所有 fetch 事件。
  • 无法操作 UI:和 Web Worker 一样,它也不能直接访问 DOM。
  • 安全性要求:必须运行在 HTTPS 协议下,以防止中间人攻击。

核心思想

JavaScript 本质上是单线程的,这意味着所有任务(UI 渲染、用户交互、JS 代码执行)都在同一个主线程上排队执行。如果一个任务耗时过长,整个页面就会被“冻结”,无法响应用户操作,造成卡顿。为了解决这个问题,浏览器引入了 "Workers",允许我们在后台创建新的线程来执行脚本,从而解放主线程。Web Worker 和 Service Worker 就是这套机制下的两种核心角色,但它们的分工和使命截然不同。


角色一:Web Worker - 主线程的“分身术”

主线程是一位分身乏术的火影忍者,他既要接待村民(响应用户交互),又要维护村容村貌(渲染 UI)。现在,他接到了一个需要耗费大量查克拉去完成的S级任务(复杂的计算)。如果他亲力亲为,那么在他完成任务期间,整个村子将无人管理,陷入停滞。

Web Worker 就是他的“分身术”(影分身)。主线程可以创造一个或多个分身(Web Worker 线程),把这个S级任务交给他。

  • 分身(Web Worker) 在一个完全独立的地方埋头计算,不打扰本体。
  • 本体(主线程) 则继续轻松地处理日常事务,确保村子(页面)的流畅运行。
  • 当分身完成任务后,通过“飞鸽传书”(postMessage API)将结果告诉本体。
  1. 核心职责与应用场景
  • 职责:分担主线程的计算压力。专门处理那些 CPU 密集型或高延迟的任务,防止主线程被阻塞。
  • 应用场景:
    • 大量数据处理:解析巨大的 JSON 文件、处理大规模数据。
    • 复杂计算:图像/视频处理(滤镜、算法分析)、数据加密/解密、复杂的数学运算。
    • 实时数据预处理:在接收到大量 WebSocket 数据后,先在 Worker 中进行整理和计算,再将结果发送给主线程用于渲染。
  1. 关键特性
  • 生命周期与页面绑定:Web Worker 随页面的创建而创建,随页面的关闭而销毁。它完全附属于创建它的页面。
  • 无法操作 UI:分身无法直接去修缮村子的房屋,即 Web Worker 不能访问 window、document 等 DOM API。它活在一个隔离的环境里。
  • 通信方式:通过 postMessage() 发送消息,通过 onmessage 事件接收消息,以此与主线程进行通信。

角色二:Service Worker - 浏览器的“智能网络代理”

如果说 Web Worker 是为了解决“内部计算压力”,那么 Service Worker 就是为了解决“外部网络依赖”。它不是一个简单的计算分身,而是一个更高阶的存在,扮演着 浏览器、应用与网络之间的“代理服务器”。

它就是一个存在你的应用和互联网之间的智能管家。所有从应用发出的网络请求,都会先经过他手。

  • 当网络正常时:他会忠实地去网络上取回资源,并顺手将一些重要资源(如 CSS、JS、图片)缓存在自己的小仓库里。
  • 当网络断开时:用户再次请求资源,虽然无法连接互联网,但这位管家会说:“别担心,上次你要的东西我都给你存着呢”,然后直接从自己的仓库里把资源返回给应用。这就实现了离线访问。
  • 超越页面:这位管家是独立于任何页面的。即使你关闭了应用的所有页面,他依然在后台待命,可以接收来自服务器的推送通知,或者在后台执行数据同步。
  1. 核心职责与应用场景
  • 职责:拦截和处理网络请求,充当网络代理。它是实现渐进式网络应用(PWA)的基石。
  • 应用场景:
    • 离线缓存:缓存应用的“外壳”(App Shell)和核心资源,让应用在离线时也能瞬时加载和使用。
    • 消息推送 (Push Notifications):即使浏览器关闭,也能接收并向用户显示来自服务器的推送消息。
    • 后台同步 (Background Sync):在网络不佳时,允许用户提交表单,等网络恢复后,Service Worker 会在后台自动完成发送。
    • 网络请求劫持:可以自定义网络请求的返回内容,例如根据不同条件返回不同的缓存版本,或者直接在 Service Worker 中生成响应。
  1. 关键特性
  • 独立生命周期:Service Worker 的生命周期与页面完全分离。它需要经历安装(install)、激活(activate)等阶段,并且可以活在浏览器后台,不受页面关闭的影响。
  • 强大的代理能力:可以拦截作用域范围内的所有 fetch 事件。
  • 无法操作 UI:和 Web Worker 一样,它也不能直接访问 DOM。
  • 安全性要求:必须运行在 HTTPS 协议下,以防止中间人攻击。

6.前端安全之 XSS 与 CSRF

核心思想

前端安全的核心在于“信任”问题。我们永远不能信任任何来自用户(或第三方)的输入。XSS 和 CSRF 是两种最常见、也最具代表性的 Web 攻击方式,它们都巧妙地利用了网站对用户或浏览器行为的“信任”。


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

1. 攻击原理

攻击者将恶意的 JavaScript 脚本注入到网页中,当其他用户访问该网页时,这些恶意脚本就会在用户的浏览器中执行,从而窃取用户信息(如 Cookie)、操作用户行为、或在页面上展示恶意内容。

本质: 恶意代码在受害者的浏览器上“无中生有”地执行了。

2. 攻防演练:一个简单的评论区

【攻】攻击演示

假设一个网站的评论区,后端直接将用户提交的评论内容插入到页面的 HTML 中。

易受攻击的代码 (前端渲染)

javascript
// 从后端获取评论列表
const comments = [
  { author: '小明', text: '这是一条正常的评论。' },
  { author: 'Hacker', text: '<script>alert("XSS Attack! Your cookie is: " + document.cookie)</script>' }
];

// 前端直接将内容渲染到页面
const commentContainer = document.getElementById('comment-list');
comments.forEach(comment => {
  // 危险操作:直接使用 innerHTML 渲染未经过处理的用户输入
  commentContainer.innerHTML += `<div><strong>${comment.author}:</strong> ${comment.text}</div>`;
});

攻击效果

当任何用户浏览这个评论区时:

  1. 浏览器在渲染 Hacker 的评论时,会遇到 <script> 标签。

  2. 浏览器会立即执行其中的 JavaScript 代码。

  3. 页面会弹出一个警告框,显示当前用户的 Cookie。攻击者可以将 alert 换成更复杂的代码,将 Cookie 发送到自己的服务器,从而盗用用户身份。

【防】防御策略

核心防御思路是:“数据”永远是“数据”,绝不能让它变成“代码”。

策略一:HTML 转义 (Output Encoding) - 主要防御手段

在将用户输入的内容输出到页面时,对具有特殊含义的 HTML 字符(如 <, >, &, ", ')进行转义。

修复后的防御代码

javascript
function escapeHTML(str) {
  if (!str) return '';
  return str.replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
}

// ... 获取评论列表的代码 ...

comments.forEach(comment => {
  // 安全的做法:先转义,再渲染
  const safeText = escapeHTML(comment.text);
  // 或者使用 textContent 属性,它会自动处理转义
  const div = document.createElement('div');
  const strong = document.createElement('strong');
  strong.textContent = comment.author + ': ';
  div.appendChild(strong);
  div.appendChild(document.createTextNode(safeText)); // 使用 createTextNode 也是绝对安全的
  commentContainer.appendChild(div);
});

防御效果

经过转义后,Hacker 的恶意评论在 HTML 源码中会变成:

html
<div>
  <strong>Hacker:</strong> &lt;script&gt;alert("XSS Attack! Your cookie is: " + document.cookie)&lt;/script&gt;
</div>

浏览器只会将这段代码作为纯文本显示在页面上,而不会执行它。用户看到的就是<script>alert(...)</script>这段字符串本身。

策略二:内容安全策略 (Content Security Policy, CSP) - 深度防御

CSP 通过设置 HTTP 头部,告知浏览器一个“白名单”,规定页面只允许加载和执行来自特定来源的脚本。即使攻击者成功注入了脚本,由于脚本来源不在白名单内,浏览器也会拒绝执行。

示例 HTTP 头部

javascript
Content-Security-Policy: script-src 'self' https://trusted.cdn.com;

这个策略告诉浏览器:只允许执行来自当前域名(self)和 https://trusted.cdn.com 的脚本。所有内联脚本(inline script)和来自其他域的脚本都将被阻止。


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

1. 攻击原理

攻击者诱导一个已经登录的受害者,在不知情的情况下,从一个第三方恶意网站向被攻击网站发送一个伪造的请求(如转账、修改密码等)。由于这个请求是受害者浏览器发出的,会自动带上被攻击网站的 Cookie,因此服务器会误以为这是用户的真实意图并执行。

本质: 攻击者冒用你的身份,发送了一个你并非本意的请求。请求是合法的,但并非出自你的意愿。


2. 攻防演练:一个简单的转账操作

【攻】攻击演示

假设 bank.com 有一个通过 GET 请求就能完成转账的功能。

易受攻击的 API (bank.com)

text
// GET /transfer?to=Hacker&amount=1000
// 只要用户登录了 bank.com,访问这个 URL 就会立刻转账。
// 服务器仅通过 Cookie 判断用户身份。

恶意网站的代码 (hacker.com)

攻击者在自己的网站上放置一个看不见的图片标签。

html
<!-- hacker.com/malicious_page.html -->
<h1>劲爆!点击查看!</h1>
<!-- 用户看不到这个图片,但浏览器会尝试加载它 -->
<img src="http://bank.com/transfer?to=Hacker&amount=1000" width="1" height="1" style="display:none;">

攻击效果

  1. 用户登录了 bank.comCookie 已保存在浏览器中。

  2. 用户被诱导访问了 hacker.com 的恶意页面。

  3. 浏览器看到 <img> 标签,会立即尝试加载其 src 指向的 URL,也就是向 bank.com 发起一个 GET 请求。

  4. 根据同源策略,浏览器在发送这个跨站请求时,会自动带上 bank.com 域下的 Cookie

  5. bank.com 的服务器收到请求,验证 Cookie 发现是合法用户,于是执行转账操作。用户在毫不知情的情况下损失了 1000 元。

【防】防御策略

核心防御思路是:增加一个攻击者无法伪造的、与用户会话绑定的“凭证”。

策略一:Anti-CSRF Token (同步令牌模式) - 主要防御手段

  1. 当用户访问表单页面时,服务器生成一个随机的、唯一的 token,并将其嵌入到表单的隐藏字段中。同时,服务器在 session 中也保存这个 token。

  2. 当用户提交表单时,这个 token 会随表单一起发送到服务器。

  3. 服务器比较表单提交的 token 和 session 中保存的 token。如果两者一致,则为合法请求;如果不一致,则拒绝请求。

修复后的防御代码 (以 Node.js/Express 为例)

  1. 渲染表单时,加入 token
javascript
// GET /show_transfer_form
app.get('/transfer', (req, res) => {
  // 生成 token 并存入 session
  req.session.csrfToken = generateRandomToken(); 
  res.render('transfer_form', { csrfToken: req.session.csrfToken });
});
  1. HTML 表单中嵌入 token
html
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <input type="text" name="to" placeholder="收款人">
  <input type="text" name="amount" placeholder="金额">
  <button type="submit">转账</button>
</form>
  1. 处理请求时,验证 token
javascript
// POST /transfer
app.post('/transfer', (req, res) => {
  if (req.body._csrf !== req.session.csrfToken) {
    return res.status(403).send('Invalid CSRF Token');
  }
  // Token 验证通过,执行转账逻辑...
  // ...
  res.send('转账成功!');
});

防御效果

攻击者在 hacker.com 无法得知 bank.com 在用户 session 中生成的 csrfToken 是什么,因此他们无法伪造一个带有正确 _csrf 值的请求。当伪造请求到达服务器时,会因为 token 验证失败而被拒绝。

通过在设置 Cookie 时增加 SameSite 属性,可以告诉浏览器在跨站请求中是否要携带这个 Cookie。

text
Set-Cookie: session_id=...; HttpOnly; SameSite=Strict
  • SameSite=Strict: 最严格。任何跨站请求(包括从收藏夹打开、地址栏输入)都不会携带 Cookie。CSRF 攻击完全失效。
  • SameSite=Lax: 较为宽松。允许在一些安全的顶级导航 GET 请求中携带 Cookie(如点击链接跳转),但对于 POST<img><iframe> 等高风险的跨站请求,则不会发送 Cookie。这也能有效防御绝大多数 CSRF 攻击。

这是目前浏览器层面最简单、最有效的 CSRF 防御手段。


总结对比

特征XSS (跨站脚本)CSRF (跨站请求伪造)
攻击目标窃取用户在客户端的信息和权限冒用用户身份,执行服务端的操作
信任利用网站信任了用户输入的内容网站信任了用户的浏览器(的Cookie)
代码执行地受害者浏览器中攻击者无法注入或执行代码
核心防御输入过滤和输出转义,不让数据变代码Token 或 SameSite Cookie,验证请求来源

不知道说啥了很无语了