浏览器与网络

浏览器

浏览器概述

一、 浏览器架构演进:从单进程到多进程

现代浏览器采用多进程架构,主要是为了解决单进程模式下资源共享带来的致命缺陷。

架构类型 表现 核心痛点 / 解决方案
单进程 ❌ 不稳定、卡顿、不安全 任何一个插件或脚本的崩溃/死循环都会拖垮整个浏览器;且脚本有权限任意访问操作系统资源。
多进程 ✅ 稳定、流畅、安全 进程隔离:单页面/插件崩溃不影响全局,脚本互不阻塞。

安全沙箱:为“渲染进程”上锁,剥夺其直接读写OS数据的权限,必须通过 IPC(进程间通信)找浏览器内核代办。

二、 浏览器的五大主要进程

浏览器是一个庞大的协作系统,主要由以下进程各司其职:

  • 浏览器主进程 (1个):总控大脑。负责页面管理(创建/销毁)、网络资源管理、下载、本地存储等。
  • 浏览器渲染进程 (多个):即浏览器内核,通常每个 Tab 页一个。核心任务是解析 HTML/CSS/JS 并渲染页面。出于安全考虑,运行在沙箱模式下。
  • GPU 进程 (最多1个):负责 3D 绘制和页面的硬件加速渲染。
  • 网络进程:独立处理页面的网络资源加载。
  • 第三方插件进程 (多个):按需创建,隔离 Flash 等插件,防止其崩溃影响主程序。

    Google Chrome的task manager可以查看

三、 渲染进程(浏览器内核)的内部线程

渲染进程是我们前端开发者最关心的部分,它内部是多线程协作的:

  1. Main Thread(主线程):绝对的核心。它集大权于一身,既跑 V8 引擎执行 JS,又跑 Blink 引擎解析 HTML/CSS、构建 DOM 树、计算布局(Layout)、事件触发等。
  2. Compositor Thread(合成器线程):主线程的“高级副手”。主线程把页面计算好之后,交给它。它负责把页面分层(Layer),并且独立接管页面的滚动(Scroll)和某些 CSS 动画(如 transform 和 opacity。即使主线程被卡死,它也能让页面继续顺滑滚动。
  3. Raster Threads(光栅化线程池):合成器线程手下的“底层画师”。它们通常有多个(线程池),负责把分好层的图块,真正转换成显示器能看懂的像素点矩阵(位图)。
  4. Worker Threads(工作线程):如果你在前端代码里主动创建了 Web Worker 或 Service Worker,它们就会在这里独立运行,专门帮你做那些极其耗时的 JS 数学计算,绝不抢占主线程。
  • GUI由主线程、合成器线程负责;异步请求由网络进程负责;定时触发由底层的 C++ 定时器机制

    Google Chrome的Performance可以查看

四、 异步运行机制与内核拓展

  • 异步运行逻辑 (Event Loop 原理):JS 引擎执行栈空闲时 → 检查并读取任务队列 → 执行队列中的回调任务。这会导致一个问题:如果主线程一直忙碌,定时器任务可能不会按时执行
  • 常见浏览器内核 (渲染引擎)
    • Trident:IE
    • Gecko:Firefox
    • Webkit:Safari
    • Blink:Chrome, Opera (基于 Webkit 分支)
  • 环境/版本检测
    • UserAgent 检测:不可靠,极易被篡改或伪装。
    • 功能/特性检测:推荐做法。通过判断浏览器是否支持某个独有 API(如 IE 的 ActiveXObject)来得出结论。

浏览器:事件循环 Event Loop

一、 核心大前提:为什么 JS 是单线程?

  • 根本原因:JS 的主要用途是与用户交互和操作 DOM。如果设计成多线程,一个线程给节点添加内容,另一个线程同时删除该节点,浏览器会直接崩溃。为了避免这种复杂的同步问题,JS 必须是单线程。
  • 单线程的致命伤:一旦遇到耗时任务(如死循环),后面的代码全被阻塞,网页直接卡死。
  • 破局之道(异步实现)
    • 宏观层面浏览器多线程。JS 引擎把耗时任务(定时器、网络请求等)交接给浏览器的其他线程(如定时触发线程、异步 HTTP 请求线程)去后台默默执行。
    • 微观层面Event Loop(事件循环)。这是 JS 协调这些异步任务回调何时执行的机制。

二、 任务的分类:宏任务与微任务

当异步操作在后台完成后,它们的回调函数会被放进不同的“队列”里排队,等待 JS 主线程(执行栈)空闲时去取。

任务类型 英文名称 特点与规则 常见场景
宏任务 Macrotask (Task) 也就是普通任务队列

可以有多个队列。每次事件循环只取出一个执行。
整体代码 <script>

setTimeout / setInterval

setImmediate

I/O 操作(fs.readFile)

UI 交互事件

ajax请求
微任务 Microtask 也就是VIP 加急队列

只有一个队列。每次清空时,必须一次性全部执行完(包含执行期间产生的新微任务)。
Promise.then/catch/finally

async/await 的后续代码

Node 环境中的 process.nextTick

MutationObserver
axios、fetch
  1. 一个 Event Loop 有一个或多个 task queue(任务队列)
  2. 每个 Event Loop 有一个 microtask queue(微任务队列)
  3. requestAnimationFrame 不在任务队列也不在微任务队列,**在渲染阶段执行

三、 Event Loop 的标准运转流程(核心法则)

Event Loop 的唯一任务,就是将任务队列和**调用栈(执行栈)**连接起来。它的运转遵循极其严格的“1-2-3-4”循环:

  1. 执行一个宏任务:从宏任务队列中取出一个执行(最开始时,这个宏任务就是执行整个 <script> 同步代码)。执行宏任务的过程中遇到微任务,依次加入微任务队列
  2. 清空所有微任务:执行栈一旦为空,立刻检查微任务队列。如果有,则全部执行,直到微任务队列彻底为空。
    • 注意:如果在执行微任务时又生成了新的微任务,也会在这步一并执行完。
  3. 渲染 UI(如有必要):在一个宏任务结束,且微任务彻底清空后,浏览器会在此间隙进行页面渲染。(正常的 forEach 会阻塞渲染,例如循环一万次因为没有间隙,可以化整为零)。
  4. 开启下一轮:回到步骤 1,去宏任务队列里拿下一个任务(例如到期的 setTimeout 回调)进入执行栈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
return Promise.resolve().then(_ => {
console.log('async2 promise');
});
}
console.log('start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
第一阶段:执行主线程同步代码(第一轮宏任务)

1. console.log('start')

  • 同步代码,直接打印。
  • 📝 屏幕输出:start
    2. 遇到 setTimeout(...)
  • 它是浏览器定时器 API,属于宏任务。把它扔进宏任务队列,0毫秒后准备好(但必须排队)。
  • 📓 宏任务队列[setTimeout的回调]
    3. 调用 async1()
  • 进入 async1 函数内部。
  • 遇到 console.log('async1 start'),同步代码,直接打印。
  • 📝 屏幕输出:start -> async1 start
  • 遇到 await async2()执行机制开始发威
    • 首先,立即同步执行 await 右边的 async2() 函数。
    • 进入 async2 内部,遇到 return Promise.resolve().then(...)
    • 看到 .then(),这是一个微任务!立刻把它扔进微任务队列。
    • 🌟 微任务队列[打印 async2 promise]
    • async2 执行完毕,返回了一个处于 pending(等待)状态的 Promise(因为它在等里面那个 .then执行完)。
    • 此时 await 开始工作:它会暂停 async1 内部后续代码的执行,也就是把 console.log('async1 end') 彻底冻结。必须等到 async2 返回的 Promise 真正 resolved 后,这行代码才能解冻并进入微任务队列。
      4. 退出 async1,继续执行主程序,遇到 new Promise(...)
  • 注意:new Promise 传入的回调函数是同步立即执行的!
  • 执行 console.log('promise1')
  • 📝 屏幕输出:start -> async1 start -> promise1
  • 执行 resolve(),这个 Promise 状态立刻变为 resolved。
  • 遇到后面的 .then(...),属于微任务,扔进微任务队列末尾。
  • 🌟 微任务队列[打印 async2 promise,打印 promise2]

⏳ 第一阶段结束时的状态盘点:

  • 📝 屏幕输出start -> async1 start -> promise1
  • 🌟 微任务队列[打印 async2 promise,打印 promise2]
  • 📓 宏任务队列[setTimeout的回调]
  • 🧊 被冻结的代码async1 里的 console.log('async1 end'),它在苦苦等待 async2 里的 .then 执行完毕。
第二阶段:清空微任务队列(Event Loop 核心步骤)

主程序(第一个宏任务)执行完毕,调用栈空了。Event Loop 引擎立刻去检查微任务队列。
1. 取出微任务队列的第一个任务:执行 async2 里面的 .then 回调

  • 执行 console.log('async2 promise')
  • 📝 屏幕输出追加:async2 promise
  • 💥 魔法瞬间(解冻):因为这个 .then 执行完了,async2() 返回的那个 Promise 终于变成了 resolved 状态!
  • await 收到了完工的信号,立刻将之前冻结的代码(console.log('async1 end'))打包成一个新的微任务,推入微任务队列的最末尾
  • 🌟 微任务队列更新为[打印 promise2,打印 async1 end]
    2. 取出微任务队列的第二个任务:执行 new Promise 后面的 .then 回调
  • 执行 console.log('promise2')
  • 📝 屏幕输出追加:promise2
  • 🌟 微任务队列剩下[打印 async1 end]
    3. 取出刚刚解冻进入队列的新微任务:执行 async1 剩余的代码
  • 执行 console.log('async1 end')
  • 📝 屏幕输出追加:async1 end
  • 🌟 微任务队列彻底清空:[]
    (此时微任务全部清空,如果页面需要渲染,浏览器会在这里进行 UI 渲染)
第三阶段:执行下一个宏任务

Event Loop 引擎发现微任务队列空了,于是去宏任务队列拿任务。
1. 取出宏任务队列里的第一个任务:执行 setTimeout 回调

  • 执行 console.log('setTimeout')
  • 📝 屏幕输出追加:setTimeout
  • 📓 宏任务队列清空[]
🏆 最终结果总结

按照刚才分析的顺序,完全一致:

  1. start (主程序同步)
  2. async1 start (主程序里的函数同步)
  3. promise1 (Promise构造函数同步)
  4. async2 promise (第一波微任务)
  5. promise2 (第一波微任务)
  6. async1 end (由 await 解冻后动态加入的第二波微任务)
  7. setTimeout (下一轮的宏任务)

2. Node事件循环(以来C++ libuv)

1. 重点关注3 大阶段(车间)

  • Timers(定时器阶段):只负责执行到期的 setTimeout 和 setInterval
  • Poll(轮询阶段 / I/O 阶段)整个事件循环的核心中枢。绝大多数实干的代码(读写文件、网络请求等 I/O 回调)都在这里执行。
  • Check(检查阶段):专门为 setImmediate 设立的专属执行阶段。

2. Poll(轮询阶段)的“智能路由”逻辑 当引擎来到 Poll 阶段,如果队列里有活儿,就一直干到空为止。如果队列空了,它面临三个选择:

  • 如果有 setImmediate 在等候 → 立刻结束 Poll,前往下一站 Check 阶段。
  • 如果没有 setImmediate,但有定时器快到期了 → 跑回起点 Timers 阶段去执行定时器。
  • 如果上面俩都没有 → 它干脆就停在 Poll 阶段死等新的 I/O 事件进来。

3. setTimeout vs setImmediate 的恩怨

  • 如果在全局同步代码里同时调用,它俩谁先执行看老天爷(系统性能抖动)。
  • 如果在 I/O 回调(Poll 阶段)里同时调用,setImmediate 永远先执行(因为列车往前开,下一站必然是 Check 阶段,而 Timers 阶段在起点)。

4. 🚨 微任务的执行时机

  • 旧观念(Node 10 及以前):如你文本中所说,“一个阶段执行完毕,才会去执行 microtask”。
  • 新现实(Node 11 及以后,与现代浏览器完全一致)执行完任何一个单独的宏任务(回调)后,立刻就会去清空微任务队列! 并且在微任务内部,process.nextTick 永远排在 Promise.then 前面。
  • 宏任务就是nodejs里6个阶段的回调

为了把上面的理论吃透,我们来看一个经典的“大乱斗”实例,这段代码完美诠释了你总结里的每一句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
console.log('0. [同步代码] 主线程开始');
// 发起一个 I/O 操作(读文件)
fs.readFile(__filename, () => {
console.log('1. [Poll 阶段] I/O 回调执行');
// 在 Poll 阶段内部,注册四个不同类型的任务
setTimeout(() => {
console.log('4. [Timers 阶段] setTimeout 执行');
}, 0);
setImmediate(() => {
console.log('3. [Check 阶段] setImmediate 执行');
});
Promise.resolve().then(() => {
console.log('2.5 [微任务] Promise.then 执行');
});
process.nextTick(() => {
console.log('2. [微任务] process.nextTick 执行');
});
});
console.log('0.5 [同步代码] 主线程结束,准备进入事件循环');
  1. 执行同步代码:首先打印 0。遇到 fs.readFile,把读文件的任务交给系统后台线程(此时不阻塞),继续往下走,打印 0.5(此时主线程清空,事件循环启动,开始一圈圈转)。
  2. 停靠 Poll 阶段:系统后台读完了文件,把这个回调函数扔进了 Poll 队列。事件循环刚好转到 Poll 阶段,拿出来执行,打印 1
  3. 登记任务:在这个回调内部,你一口气登记了四个任务。setTimeout 丢进 Timers 队列,setImmediate 丢进 Check 队列,Promise 和 nextTick 丢进微任务 VIP 队列。
  4. 清空微任务(Node 11+ 新特性):Poll 里的这个回调执行完毕了!在进入下一个阶段前,立刻清空微任务
    • 最高优:先打印 2 (nextTick)。
    • 次高优:再打印 2.5 (Promise.then)。
  5. 前往下一站(Check 阶段):微任务清空完毕,列车从 Poll 阶段开出。按照你总结的顺序,Poll 的下一站必然是 Check!所以此时取出刚才登记的 setImmediate 并执行,打印 3
  6. 回到起点(Timers 阶段):Check 走完了,列车继续转圈,回到了第一站 Timers。终于轮到了在寒风中等待多时的 setTimeout,打印 4

最终稳定输出顺序: 0 → 0.5 → 1 → 2 → 2.5 → 3 → 4

浏览器内核

一、 浏览器的组成

  • 外壳:指菜单、工具栏等,主要是为用户界面操作、参数设置等提供的。它调用内核来实现各种功能。
  • 内核是浏览器的核心,内核是基于标记语言显示内容的程序或模块。(可以写插件对外壳进行定义,以及调用一些内核API)

二、浏览器作用

  • “拿来”:向服务器发出请求获取资源(HTML 文档,也可以是 PDF、图片或其他的类型)
  • “翻译”:在浏览器窗口中展示您选择的网络资源,浏览器根据HTML规范进行解释

三、浏览器内核

现代语境:渲染引擎和Js V8引擎

四、浏览器使用的内核分类

  • Trident 内核:IE、MaxThon、TT、The World、360、搜狗浏览器等(当年的大哥了,没落后,IE都被淘汰了,不过国内一些老的机构的页面还是基于IE的,比如教师资格考试就要在IE上报名)
  • Gecko 内核:Netscape6 及以上、FF、MozillaSuite/SeaMonkey 等(什么鬼东西,要不是搜了,都没听过)
  • Presto 内核:Opera7 及以上
  • Webkit 内核:Safari、Chrome 等

浏览器同源策略

跨域

  • 同源策略定义:当且仅当协议、域名和端口全部相同时才属于“同源”。非同源的 JS 脚本在未经允许时,无法访问对方域下的内容。注意和同站Same-site的区别
  • 跨域的本质机制
    • 跨域请求一定会被服务端真实执行。因为 HTTP 请求头可以被篡改,服务端无法仅凭请求头准确判断是否跨域并进行拦截。
    • 跨域报错实际上是浏览器的保护机制。请求发出并执行后,在数据返回时,浏览器发现跨域限制,从而拦截并隐藏了返回值。
  • 预检请求 (OPTIONS)
    • 作用是提前向服务端“询问”是否允许当前的跨域请求,以及支持哪些 HTTP 方法。
    • 为了减少服务端开销,浏览器只对“复杂请求”发送预检。
  • 简单请求
    • 如果被判定为简单请求,浏览器不会发送预检
    • 请求会直接发往服务端并被执行,但如果受到跨域限制,浏览器依然会隐藏其返回值。
  • 前端解决方案
    • 大部分前端跨域问题出现在开发环境(生产环境通常会配置好同源或 CORS)。生产环境通常可以利用 Nginx 把前端页面和后端接口挂在同一个域名下,从而在物理上消灭了跨域现象。
    • 主流解决方案是在开发环境中配置开发代理 (Proxy) 来绕过浏览器的同源策略限制。

解决跨域

核心规范:CORS (跨域资源共享)

CORS 是目前最正统的 HTTP 跨域解决方案,完全依靠后端配置(核心是设置 Access-Control-Allow-Origin响应头)。根据请求的复杂程度,浏览器会将其分为三种模式:

1. 简单请求

  • 判定条件:请求方法仅限 GET/POST/HEAD;且只包含基础安全请求头;如果包含 Content-Type,值只能是文本text/plain或表单格式multipart/form-data或application/x-www-form-urlcoded(不能是 JSON)。
  • 执行流程:浏览器直接发出请求,并在请求头带上 Origin(说明自己是谁)。服务器若允许,就在响应头加上 Access-Control-Allow-Origin,浏览器看到后就会将数据放行给前端。

2. 需要预检的复杂请求

  • 判定条件:不符合简单请求条件的(比如带了自定义请求头,或者传了 JSON 数据)。
  • 执行流程
    1. 浏览器会先发一个 OPTIONS 预检请求,没有请求体,像是在“探路”,询问服务器是否允许此次跨域。
      请求例如:
1
2
3
Origin: http://my.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type
2. 服务器若允许,会返回可用的方法、请求头,以及缓存时间(`Access-Control-Max-Age`,告诉浏览器这段时间内不用再重复探路了)。
    响应例如:
1
2
3
4
Access-Control-Allow-Origin: http://my.com 
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: a, b, content-type
Access-Control-Max-Age: 86400
3. 预检通过后,浏览器才会发送真正的请求。

3. 附带身份凭证的请求 (Cookie)

  • 配置要求:前端发请求时需手动开启携带凭证的开关(如 withCredentials = true)。
  • 严格限制:服务器不仅要明确返回允许凭证(Access-Control-Allow-Credentials: true),而且 Allow-Origin 绝对不能设置为通配符 *,必须写明具体的源域名。

4.在跨域访问时,虽然可以在浏览器的网络面板看到各种响应头及其对应的值,但前端js有的并不能直接访问,例如自定义的Authorization等。浏览器默认js可以访问的是Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma,如果要访问其他头,则需要服务器设置本响应头。例如:Access-Control-Expose-Headers: authorization, a, b

其他主流跨域解决方案

除了 CORS,还有以下几种常见的跨域方式,适用于不同的业务场景:

方案名称 原理与特点 适用场景与局限
开发代理 (Dev Proxy) 本地启动一个代理服务器。前端发请求给同源的本地代理,代理再转发给跨域的真实服务器,避开浏览器的同源策略。 仅限开发环境使用。
Nginx 反向代理 服务端方案。Nginx 冒充服务端拦截请求,通过重写 URL 转发到真实的物理服务器,让跨域变成同域。
三大作用:静态web服务器、反向代理、负载均衡;
正向代理隐藏客户端,反向代理隐藏服务端;
生产环境主流方案之一。
JSONP 利用 <script> 等标签的 src 属性不受跨域限制的漏洞,通过 URL 传参并执行回调函数来拿数据。 仅支持 GET 请求,且存在安全隐患,属于老旧方案。
WebSocket 一种与 HTTP 同级的全双工通信协议。它天然不受同源策略SOP限制,服务器靠 Origin 字段来判断是否放行。 适用于需要长连接、实时通信的场景。
postMessage HTML5 提供的跨文档消息机制,允许不同源的窗口进行数据传输。 常用于页面与嵌套的第三方 iframe 之间通信。

边缘/老旧方案 (基于 Iframe 的 Hack 技巧)

这些方案主要是利用浏览器页面嵌套机制进行的偏门处理,现代开发中已经很少使用:

  • document.domain:用于主域名相同、子域名不同的页面。只要把两边的 document.domain 设置成一样就能跨域。
  • location.hash:利用 URL 的 hash(#号后面的值)改变但页面不刷新的特性,结合中间页面实现跨域通信。
  • window.name:利用这个属性在页面跳转甚至跨域时,值依然保留的特性来共享数据。

输入URL回车后发生了什么

简易版

  1. URL解析
  2. 查找缓存
  3. 域名解析:浏览器缓存>系统缓存>本地hosts>根域名>顶级域名>二级域名>三级域名
  4. TCP三次握手
  5. 发送HTTP请求
  6. 服务器处理请求并返回报文
  7. 浏览器解析渲染页面
  8. TCP四次挥手 关闭TCP连接

从宏观是上理解从输入URL到页面渲染的过程,主要分为导航阶段和渲染阶段.

1. 用户输入URL

(1). 浏览器进程检查url,组装协议,构成完整的url,这时候有两种情况:

  • 输入的是搜索内容:地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的URL。
  • 输入的是请求URL:地址栏会根据规则,给这段内容加上协议,合成为完整的URL;

(2). 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程;
URL一般包括几大部分:

  • protocol,协议头,譬如有http,ftp等
  • host,主机域名或IP地址
  • port,端口号
  • path,目录路径
  • query,即查询参数
  • fragment,即 #后的hash值,一般用来定位到某个位置

二、网络进程

2. URL请求过程

(1). 网络进程接收到url请求后检查本地是否缓存了该请求资源。

  • 浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。
  • 没有命中强缓存规则,浏览器会发送请求,根据请求头的If-Modified-Since(last_modified)和If-None-Match(ETag)判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。
  • 如果前两步都没有命中,则直接从服务端获取资源。

简单说一下强缓存和协商缓存:

维度 / 场景 强缓存 (Strong Cache) 协商缓存 (Negotiated Cache) 普通 API 接口缓存 (生产实战)
核心表现 不发网络请求,极速秒开。



状态码:200 (from cache)
发极其轻量的请求问服务器。



状态码:304 (没变,用本地) 或 200 (变了,下发新文件)
每次都发真实请求拿数据。



状态码:通常都是 200
核心标签 (Headers) Cache-Control: max-age=31536000



(注:旧时代用 Expires)
服务器下发暗号ETag / Last-Modified



浏览器下次提问If-None-Match / If-Modified-Since
无特殊 HTTP 缓存头,或者由后端自定义业务级标识。
关键配置辨析 Cache-Control: no-store:**绝对禁止缓存!**连本地都不准存,永远重新下载全量文件。(极敏感数据使用) Cache-control: no-cache强制走协商缓存!可以存本地,但每次用之前必须带上 ETag 问服务器。 视业务而定,通常也是动态获取,默认不缓存。
前端职责 (JS代码) 纯躺平 (0配置)。浏览器底层自动拦截请求,直接掏本地文件。 纯躺平 (0配置)。浏览器底层可以把上一次的ETag自动塞入 If-None-Match 前端正常发 Axios/Fetch 请求即可。
Nginx职责 (静态文件) 绝对主力



专门给打包带 Hash 的静态资源 (JS/CSS/图片) 设置 max-age=1年
绝对主力



专门给单页应用的入口 index.html 设置 no-cache。Nginx 底层全自动比对 ETag。
一般只负责做反向代理/负载均衡,把 API 请求直接透传给后端服务器。
后端职责 (Node/Java等) 基本不参与静态文件处理。 若针对静态文件,由后端框架底层的 static 模块自动处理,不用手写。 **如果要给 API 做协商缓存,后端全责!**需纯手写读头、查库、算 Hash、判断并返回 res.status(304).end()
生产环境主流做法 **带 Hash 的文件无脑强缓存。**内容一变,文件名必变,天然避开缓存更新痛点。 **名字不变的文件 (如 index.html) 无脑协商缓存。**保证用户永远能拉到最新版本的壳子。 极少用 HTTP 协商缓存!(因为省带宽但不省数据库性能)。

👉 生产方案:前端发正常请求,后端在服务器内使用 Redis 等内存数据库做业务层缓存,扛住高并发。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server {
listen 80;
server_name yourdomain.com;
root /usr/share/nginx/html; # 你的前端 dist 目录

# 开启 ETag (协商缓存的基石)
etag on;

# 1. 核心大门:index.html
# 策略:不走强缓存,每次都必须向服务器确认有没有发新版(走协商缓存或直接拉新)
location = /index.html {
# no-cache 的意思不是“不缓存”,而是“必须先跟服务器确认 (协商) 后才能使用缓存”
add_header Cache-Control "no-cache, no-store, must-revalidate";
# 顺手解决 SPA 路由刷新的 404 问题
try_files $uri $uri/ /index.html;
}

# 2. 静态资产:带 Hash 值的 JS / CSS 文件
# 策略:霸道总裁模式,强缓存直接拉满 1 年!
location ~* \.(js|css)$ {
# max-age=31536000 (365天的秒数)
# immutable (告诉浏览器这个文件绝对不会变,连刷新的验证都省了)
add_header Cache-Control "public, max-age=31536000, immutable";
}

# 3. 媒体资源:图片、字体等
# 策略:常规强缓存 30 天
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public";
}
}

(2). 准备IP地址和端口:进行DNS解析时先查找缓存,没有再使用DNS服务器解析,查找顺序为:

  • 浏览器缓存;
  • 本机缓存;
  • hosts文件;
  • 路由器缓存;
  • ISP DNS缓存;
  • DNS递归查询(本地DNS服务器 -> 权限DNS服务器 -> 顶级DNS服务器 -> 13台根DNS服务器)

3. HTTP请求与响应

(1). 等待TCP队列:浏览器会为每个域名最多维护6个TCP连接,如果发起一个HTTP请求时,这 6个 TCP连接都处于忙碌状态,那么这个请求就会处于排队状态,解决方案:

  • 采用域名分片技术:将一个站点的资源放在多个(CDN)域名下面。
  • 升级为HTTP2,就没有6个TCP连接的限制了;

(2). 通过三次握手建立TCP连接、构建并发送HTTP请求信息;

  • 建立TCP连接后,http请求
  • 扯一点http:Http1.0是一个请求一个TCP连接,http1.1是长连接,但是同一个TCP连接的多个http请求遵循在应用层FIFO的原则,所以浏览器为了变快,也会在同一个域名开辟6个这样的TCP连接;http2引入多路复用,一个TCP连接可以同时传递多个http请求

(3). 服务器端处理请求、客户端处理响应,首先检查服务器响应报文的状态码:

  • 如果是301/302表示服务器已更换域名需要重定向,这时网络进程会从响应头的Location字段里面读取重定向的地址,然后再发起新的HTTP或者HTTPS请求,跳回第4步。
  • 如果是200,就检查Content-Type字段,值为text/html说明是HTML文档,是application/octet-stream说明是文件下载;

(4). 请求结束,当通用首部字段Conection不是Keep-Alive时,即不为TCP长连接时,通过四次挥手断开TCP连接

4. 渲染
渲染步骤大致可以分为以下几步:

  1. 解析HTML,构建DOM树
  2. 解析CSS,生成CSS规则树
  3. 合并DOM树和CSS规则,生成render树
  4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  5. 绘制render树(paint),绘制页面像素信息
  6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上

(1). 准备阶段(交接任务)

  • 分配车间:浏览器主进程会判断新打开的网页是否和上一个网页同根域名,如果是就复用渲染进程,否则开启新进程。
  • 运送材料:浏览器进程把网络请求进程拿到的 HTML 数据流,“提交”给渲染进程,准备开工。

(2). 解析与建树阶段(看懂图纸)

  • 构建 DOM 树:HTML 解析器把 HTML 文本拆解,生成浏览器能看懂的 DOM 树。期间有安全员(XSSAuditor)检查有没有恶意脚本,同时会有个“预解析线程”提前去下载外链的 JS 和 CSS。(HTML解析器将其中的HTML字节流通过分词器拆分为一个个Token,然后生成节点Node,最后解析成浏览器识别的DOM树结构)
  • 构建 CSSOM 树:把 CSS 文本解析成样式树,并把属性“标准化”(比如把 em 换算成 px,把单词 red 换算成 rgb)。
  • ⚠️ 优化痛点:JS 和 CSS 的下载与执行会阻塞 DOM 解析,导致“白屏”。所以要做优化(比如压缩文件、给 script 加 async/defer、内联关键 CSS 等)。

(3). 布局阶段(排版定位/重排)

  • 生成布局树 (Layout Tree):把 DOM 树和 CSSOM 树合并。**注意:这棵树只包含屏幕上看得见的元素!**像 <head> 标签或者加了 display: none 的元素都会被无情剔除。
  • 计算位置:计算每个可见元素在屏幕上的确切宽、高和几何坐标。

(4). 分层与绘制指令阶段(画师打草稿)

  • 分层 (Layer Tree):为了防止牵一发而动全身,浏览器会像用 Photoshop 一样把页面“分层”。使用了 3D 变换(translate3d)、fixed 定位或 video 标签的元素,会被单独拎出来作为一个独立图层。
  • 生成绘制列表:为每个图层生成一系列的“绘画指令”(比如“画一个红色的矩形”、“在坐标 X,Y 写一段文字”)。到了这一步,主线程的工作就干完了。

(5). 栅格化与合成显示(GPU 终极渲染)

  • 切图与栅格化:合成线程接管工作,把大图层切成小图块,然后利用 GPU 把这些图块快速转换成屏幕上的真实像素点(位图)。
  • 合成上屏:GPU 把所有图块拼接好,浏览器主进程的 viz 组件接收到指令,最终把画面投射到你的显示器上。

注意了:合成的过程是在渲染进程的合成线程中完成的,不会影响到渲染进程的主线程执行;

Layout回流 [重排]:通过 JavaScript 或者 CSS 修改元素几何位置属性,会触发重新布局,解析后面一系列子阶段

重绘:跳过了布局阶段,直接进入绘制,然后再分块、生成位图及其以后子阶段;Painting 根据渲染树及回流得到的几何信息,得到节点的绝对像素.

合成:渲染引擎跳过布局和绘制阶段,执行的后续操作,发生在合成线程,非主线程;

HTTP

HTTP缓存

一、 核心总览:强缓存 vs 协商缓存

缓存的执行永远是有先后顺序的:先检查强缓存,若失效或被跳过,再进入协商缓存。

维度 强缓存 (Strong Cache) 协商缓存 (Negotiated Cache)
触发时机 第一次请求后,后续请求首先触发。 强缓存失效,或设置了 no-cache 时触发。
是否请求服务器 。直接从本地取。 。带上“凭证”去问服务器还能不能用。
命中时的状态码 200 OK (from cache) 304 Not Modified
HTTP/1.0 字段 Expires 响应:Last-Modified / 请求:If-Modified-Since
HTTP/1.1 字段 Cache-Control 响应:ETag / 请求:If-None-Match

二、 强缓存详解 (浏览器自行决定)

强缓存的核心在于**“过期时间”**,目前绝对的主力是 HTTP/1.1 提出的 Cache-Control

  • Expires (HTTP/1.0 产物)
    • 原理:服务端返回一个绝对时间(如 2026年10月1日过期)。
    • 致命缺点:极度依赖客户端本地时间。如果用户修改了电脑系统时间,或者跨时区,缓存控制直接崩溃。
  • Cache-Control (HTTP/1.1 绝对核心,优先级 > Expires)
    • 原理:使用相对时间(过期时长)来控制,彻底解决本地时间不准的问题。
    • 核心取值一览表
      • max-age=xxx:在 xxx 秒内有效,直接走本地缓存。
      • no-cache不是不缓存! 而是跳过强缓存,强制进入协商缓存阶段,每次用前必须问服务器。
      • no-store绝对禁止缓存! 既不走强缓存也不走协商缓存,每次老老实实下载全量新数据。
      • public:客户端和中间代理服务器(如 CDN)都可以缓存。
      • private:(默认值)仅客户端浏览器可以缓存。

三、 协商缓存详解 (与服务器对暗号)

当强缓存没命中,浏览器就会带着上次存下的“标识(Tag)”去请求服务器。

标识类型 工作流程 优缺点对比
按时间比对



(Last-Modified + If-Modified-Since)
1. 服务器首次返回文件的最后修改时间



2. 浏览器再次请求时带上这个时间。



3. 服务器对比:若时间一致,返回 304;若文件较新,返回 200 和新文件。
⚡ 性能更好:只需读取文件时间点。



❌ 精度较差:只能精确到秒(1秒内改多次识别不出);如果打开文件保存了但内容没变,也会导致缓存失效。
按内容比对



(ETag + If-None-Match)
1. 服务器根据文件内容生成一个唯一标识符(哈希值)。



2. 浏览器再次请求时带上这个哈希值。



3. 服务器对比哈希值:一致返回 304;不一致返回 200 和新文件。
🎯 精度极高:只要内容变了标识必变,内容没变标识绝对不变。



🐢 性能略耗:服务器需要消耗计算资源来生成内容的哈希值。

💡 补充重点:ETag 的强弱之分

  • 强 ETag:苛刻的字节级比对,只要差一个字节(哪怕是空格),ETag 就会改变。
  • 弱 ETag (以 W/ 开头):表示语义上的相同。比如开启 gzip 压缩前后,文件字节变了,但内容本质没变,此时用弱 ETag (W/12345) 就可以继续复用缓存。

四、 终极大比拼与结论

  1. 同时存在听谁的? 如果响应头同时存在 Cache-Control 和 Expires,听 Cache-Control 的。 如果同时存在 ETag 和 Last-Modified,先看 ETagETag 优先级更高
  2. 大白话总结: 强缓存就是给文件设个保质期,没过期直接吃;协商缓存就是过期了但没变质,拿去给质检员(服务器)看看,质检员盖个章(304)说还能吃,那就继续吃。

HTTP 2.0

一、 前世今生与部署门槛(与 SPDY 和 HTTPS 的关系)

  • 前世:HTTP/2 是基于 Google 开发的 SPDY 协议升级而来的。相比 SPDY 强制要求 HTTPS 和使用 DEFLATE 压缩,HTTP/2 标准其实允许明文传输,并且换了更高效的 HPACK 压缩算法。
  • 现实门槛:虽然标准允许明文,但目前所有主流浏览器(Chrome/Firefox)都强制要求必须在 HTTPS(TLS)环境下才能开启 HTTP/2
  • 兼容性:你只需要在 Nginx 里开启配置即可,Nginx 会自动“看人下菜碟”:支持 HTTP/2 的浏览器就用 2.0,不支持的老浏览器就自动降级回 HTTP/1.1。

二、 核心革命:把文本碾碎成“二进制帧”(彻底解决排队问题)

这是 HTTP/2 最牛的底层改造。

1. 二进制分帧 (Binary Framing)
HTTP/1.1 传输的是我们肉眼能看懂的纯文本(容易出错且解析慢)。HTTP/2 直接在底层加了一个“二进制分帧层”,把数据全切成机器最喜欢的 0 和 1。

  • 一个请求被切分成两类帧:Headers Frame(头部帧) 和 Data Frame(数据帧)

2. 多路复用 (Multiplexing) 与 数据流 (Stream)
前面我们聊过 HTTP/1.1 哪怕有长连接,请求也得排队(队头阻塞)。HTTP/2 完美解决了这个问题!

  • 数据流 (Stream):在同一个 TCP 连接里,每一个完整的请求/响应被称为一个 Stream。
  • 乱序发送,按 ID 组装:HTTP/2 允许把不同请求的二进制帧全部打乱混在一起同时发送。为什么不怕乱?因为每个帧都有一个Stream ID(流编号)。接收端只要把相同 ID 的帧挑出来拼在一起,就能还原出完整的消息。
  • ID 规则:客户端主动建立的流 ID 必须是奇数,服务端建立的必须是偶数
  • 请求优先级:浏览器还可以给这些流指定优先级,告诉服务器“先给我传 CSS,再传图片”,不用傻等。怎么具体实现的呢?例如往传送带上放:[CSS-1][CSS-2][图片-1][CSS-3][CSS-4]。传输层TCP其实分不清哪个是CSS,但是一旦把这一批交付,应用层可以判别,所以实现了CSS优先交付;但是万一这一批的图片帧丢失,TCP就不能交付,必须予以等待。实际应用中,各种中间层例如nginx可能会过滤掉浏览器带来的优先级特性

三、 性能压榨三板斧(省流量、省时间)

1. 首部压缩 (HPACK 算法)

  • 痛点:HTTP/1.1 只压缩 Body,但每次请求都要带一堆重复的 Header(比如 Cookie、User-Agent),极其浪费流量。
  • 解法:客户端和服务器共同维护一本“密码本”(头信息表)。双方把长字符串用极短的索引号代替。第一次发全量,后面重复的字段只发一个数字索引号,大大减少了传输体积。

2. 服务端推送 (Server Push,已经废弃)

  • 打破常规:以前必须是客户端要什么,服务器给什么。现在服务器可以“预判”你的需求。比如你请求了 index.html,服务器知道你肯定还需要 style.css,于是趁着网络空闲,主动用 PUSH_PROMISE 帧 把 CSS 提前塞进浏览器的缓存里。
  • 约束:必须遵守同源策略,且客户端有权拒绝(比如本地已经有缓存了)。
  • 已经废弃

3. 流量控制 (Flow Control)

  • 类似 TCP 的窗口控制,但它是针对单个 HTTP 连接的每一跳的。接收方可以根据自己的处理能力,告诉发送方“我还能收多少数据”,防止被海量数据淹没(仅对 Data 帧有效)。

四、 终极缺陷:TCP 层面的“队头阻塞” (面试必问大坑!)

HTTP/2 看似完美解决了 HTTP 层的排队问题,但它把所有的鸡蛋都放进了一个篮子里——所有并发请求都共用这唯一的一个 TCP 连接

  • TCP 的死板机制:TCP 是一个保证数据绝对连续完整的底层协议。如果发送了 100 个数据包,哪怕只丢了中间的第 3 号包,TCP 也会把已经到达的 4 到 100 号包全部扣押在操作系统的内核缓冲区里,绝不交给上层应用,直到第 3 号包重传成功。
  • HTTP/2 的惨烈后果:因为所有请求的帧都混在这个 TCP 连接里,只要网络稍微一抖动,丢了一个微小的包,整个网页所有的并发请求就会瞬间全部卡死!
  • 在网络环境极差的情况下,HTTP/2 的表现甚至可能不如建立了 6 个独立 TCP 连接的 HTTP/1.1。**

浏览器存储

前端缓存

前端缓存技术方法主要分为http缓存和浏览器缓存。

  • HTTP缓存:强缓存、协商缓存
  • 浏览器缓存:storage 前端数据库(localStorage、sessionStorage、indexDB)和应用缓存(manifest现已废弃)

Http 缓存存储

  • 内存缓存:快速读取和实效性
  • 硬盘缓存:写入硬盘文件,需要I/O操作,重新解析改缓存内容,读取复杂,速度慢

浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:

Service Worker(前端自定义代理)

Service workers 本质上充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。旨在创建有效的离线体验,拦截网络请求并基于网络是否可用,以及更新的资源是否驻留,在服务器上来采取适当的动作。还允许访问推送通知和后台同步 API。

运行在浏览器背后的独立线程, 通常用来做缓存文件,提高首屏速度 。

不仅仅是cache,还通过worker的方式进一步优化,基于H5的web worker,所以不会阻塞当前JS线程的执行。

SW最重要的是

  1. 后台线程:独立于当前网络线程
  2. 网络代理:在网页发起请求时代理,缓存文件

使用Service Worker的话,传输协议必须是HTTPS。因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议保障安全。它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker中获取的内容。

Memory Cache

内存

Disk Cache

硬盘

Push Cache

推送缓存,已废弃

Service Worker 缓存 → Memory Cache (内存) → Disk Cache (强缓存) → 网络请求 (协商缓存 304 / 真实请求 200)

一、 为什么必须要有 Cookie?(没有它会怎样)

因为 HTTP 协议是**无状态(Stateless)**的。TCP是有状态的(Stateful)

  • 没有 Cookie 的世界:你往购物车里加了一件衣服,跳转到结账页面时,服务器根本不知道你是谁,购物车又空了。你每点一个按钮,都要重新输一遍账号密码。
  • Cookie 的作用:解决“记录你是谁”的问题。主要用于保持登录状态记住你的个性化设置(比如网页夜间模式)、以及跟踪用户行为(比如给你精准推送广告)。
  • Set-Cookie(发证):这是服务器放在 HTTP 响应头里的指令。意思是:“小子,这是你的身份证,拿去存好!”
  • Cookie(亮证):这是浏览器放在 HTTP 请求头里的字段。意思是:“保安大哥,我又来了,这是我的身份证(里面装着之前发我的状态码)。

服务器在发证(Set-Cookie)时,可以在身份证背面写上一堆限制条款,防止别人偷用或者滥用你的身份。这也是最容易出 Bug 的地方:

属性字段 通俗解释 核心作用与安全防御
ExpiresMax-Age 有效期。没写就是“临时访客”(关掉浏览器就失效);写了就是“常驻卡”(存进电脑硬盘里)。 控制登录状态能保持多久。
Domain 通用范围。比如设为 .baidu.com,那你访问 tieba.baidu.com 也能带上这张卡。 解决子域名之间共享 Cookie 的问题。
Path 适用目录。比如设为 /admin,那你访问 /user时,就不允许带这张卡。 细化作用域。
Secure 押运车专送。只有在 HTTPS 加密协议下,浏览器才愿意发送这个 Cookie。 防止身份证在 HTTP 明文传输时被黑客在半路截获。
HttpOnly 防复印涂改彻底禁止 JavaScript 代码(document.cookie)读取或修改这个 Cookie! ⚔️ 终极防御 XSS 攻击! 黑客就算往你页面里注入了恶意 JS 代码,也偷不走你的登录凭证。
SameSite 禁止外借。限制你的卡能不能在别的网站上被借用。有三个值:

Strict (绝对不借)

Lax (部分放宽,比如点链接跳转时带上)

None (随便借,但必须配合 Secure)
⚔️ 终极防御 CSRF 攻击! 防止黑客在恶意的第三方网站上,诱导你拿着身份去原网站执行转账等危险操作。
  • XSS:跨站脚本攻击(黑客注入代码,读取cookie)
  • CSRF:跨站请求伪造(跨站携带cookie,伪造发送请求)
  • Cookie 的规范里没有端口(Port)的概念。如果你在 http://localhost:8080 种下了一个 Cookie,你向 http://localhost:3000 发请求时,默认也是会带上的!(这和浏览器的同源策略 CORS 严格区分端口是完全不同的)。
  • Cookie是否自动携带,主要看的是目标是否相同,而不是看是不是来自同一个标签页、同一个源

文本中提到了通过 JS 来管理 Cookie:

  • 读写方法:前端可以使用 document.cookie = "username=John" 来创建和修改 Cookie。读取时也是用 var x = document.cookie(会拿到一串字符串,需要自己用 JS 去截取解析)。
  • 致命矛盾:如果你的登录 Token 存放在 Cookie 里,并且没有设置 HttpOnly,前端 JS 确实可以很方便地读取它。但这极度不安全!
  • 最佳实践:凡是涉及用户权限和安全相关的 Cookie,后端必须加上 HttpOnly 属性。这意味着前端代码对它是完全“盲”的(读不到也改不了),只需要浏览器在底层默默地、自动地帮你携带在请求头里发给服务器即可。
  • 走的是DOM API,不受CORS响应头6个影响

五、 跨域与 SSO (单点登录) 难题

痛点:Cookie 是极其认死理的,它绝对不允许“跨根域名”共享! 比如你在 taobao.com 登录了,拥有了淘宝的 Cookie。当你跳转到 tmall.com 时,浏览器出于安全机制(同源策略),绝对不会把淘宝的 Cookie 传给天猫。

怎么解决多系统共享登录态?(文本提到的 SSO) 因为跨域读不到 Cookie,企业通常会搭建一个专门的 SSO(单点登录)中心(比如 login.company.com)。 你在系统 A 登录时,其实是跳去 SSO 中心拿了“通用通行证”,SSO 会通过 URL 参数重定向或者在后台通过 JS 跨域通信的方式,把身份凭据(通常是 Token)分别种到系统 A 和系统 B 的域下,从而实现“一次登录,处处通行”。

鉴权

  • 工作原理:服务器把用户信息直接写在 Cookie 里发给浏览器,浏览器以后每次请求都带着它。
  • 致命缺陷
    • 容量极小:最多只能存 4KB。
    • 数量受限:单域名一般不超过 20 个,总数约 300 个。
    • 极不安全:存在客户端,纯文本(需序列化),黑客或者用户自己可以随意篡改。
    • 兼容性差:移动端(原生 App)对 Cookie 的支持非常不友好。

2. 服务端记忆时代:Cookie + Session 模式

因为 Cookie 存不下也存不安全,所以把核心数据转移到了服务器端。

  • 工作原理:服务器生成一个毫无规律的 sessionId 放进浏览器的 Cookie 里。真正的用户信息存在后端的内存或 Redis 缓存数据库中。每次请求,后端拿着 sessionId 去数据库里查你是谁。
  • 局限与痛点
    • 强依赖 Cookie:如果用户禁用了 Cookie,或者在跨域/原生 App 场景下,这套机制就容易瘫痪(需要手动改写传参)。
    • 内存开销大:系统如果有 100 万活跃用户,服务器缓存里就要存 100 万条 Session 数据,频繁查询数据库极耗性能。
    • 分布式/集群挑战(单点故障):如果存 Session 的那台服务器挂了,所有用户瞬间全掉线。虽然可以通过 Redis 集群做负载均衡来解决,但增加了系统的架构复杂度。

3. 跨系统时代:SSO (单点登录) 与 CAS

当公司业务变大,有了天猫、淘宝、飞猪等多个不同的系统(不同的顶级域名),原来的 Session 就不能跨域共享了。

  • 核心目标:一处登录,处处通行。
  • CAS (中央认证服务):建一个专门负责登录的独立系统(比如 login.alibaba.com)。所有子系统的登录验证全部交给这个“中央枢纽”来统一签发和校验票据。底层逻辑依然是 Session 的变体,只是把验证中心化了。

例如:

  1. 触发拦截(准备去往业务系统) https://www.aliyun.com/product/res
  • 发生了什么:你作为游客访问了阿里云官网,或者你想直接进入 ECS(云服务器)控制台。系统底层的网关拦截了你,发现你没有本地的 Session/Cookie。
  1. 重定向到“签证大厅”(带上回家的路)https://account.aliyun.com/login/login.htm?spm=...&oauth_callback=https%3A%2F%2Fwww.aliyun.com%2Fproduct%2Fecs
  • 发生了什么:阿里云主站无情地把你踢到了统一的认证中心(域名变成了 account.aliyun.com)。
  • 核心参数破译
    • oauth_callback=https://www.aliyun.com/product/ecs:这就是极其经典的 Redirect URI(重定向回调地址)!主站告诉签证大厅:“大哥,这哥们儿等下验完明正身之后,请把他送回 ECS 控制台这个地址,别送错了。”
  1. 在“签证大厅”提交证件(核验身份) https://account.aliyun.com/login/login_aliyun.htm?accounttraceid=e164590...&stEncrypt=oQDlq...&params=%7B%22ru%22%3A%22https%3A%2F%2Fwww.aliyun.com%2Fproduct%2Fecs%22%7D&login_method=pwd_login...
  • 发生了什么:这是你在登录框输入账号密码后,点击“登录”按钮发出的核心请求。
  • 核心参数破译
    • login_method=pwd_login:暴露了你这次使用的是密码登录(如果是扫码,这里会变成类似 qrcode_login)。
    • stEncrypt=...:这极其长的一大串,是认证中心生成的高强度加密票据(Token/Ticket)的中间形态。里面包含了你的安全验证信息,防止中间人篡改。
    • params=...:注意看里面 URL 解码后有一段 "ru":"https://www.aliyun.com/product/ecs",它还在死死记着你最终要回哪去(ru 通常是 Return URL 的缩写)。
  1. 拿着“门票”凯旋(重定向回业务系统)https://www.aliyun.com/product/ecs?accounttraceid=e164590cc26a4c38b4dfe2110d63834ezmbt
  • 发生了什么:签证大厅(account)验证你的密码正确后,立刻触发了一个 HTTP 302 重定向,把你送回了第 2 步里指定的那个 oauth_callback 地址(www)。
  • 核心参数破译
    • accounttraceid=e164590...这就是那张大名鼎鼎的“一次性门票(Ticket / Code)”!

4. 无状态革命:JWT (JSON Web Token)

为了解决 Session 狂占后端内存、不好扩展的问题,JWT 诞生了。它把服务器的压力全部甩给了 CPU 的密码学计算。

  • 结构:由三部分组成,用 . 隔开 (Header.Payload.Signature)。
    • Header (头部):声明加密算法(如 HS256)。
    • Payload (负载):存放用户基础信息(UID、签发时间、过期时间等)。
    • Signature (签名)这是防伪造的核心! 用服务器才知道的“防伪秘钥(Secret)”对前两部分进行哈希加密,生成防伪签名。
  • 工作原理:服务器不存任何状态(无状态)。每次收到 JWT,服务器只需用自己的秘钥重新算一遍签名,如果和传过来的签名一致,说明这个 Token 就是自己签发的,且没被篡改过。
  • 优点
    • 彻底摆脱 Cookie 限制(通常放在请求头 Authorization: Bearer <token> 里),原生 App 极其友好。
    • 服务端“零存储”,横向扩展(加服务器)极其容易。
  • 痛点(必考坑点)
    • 假注销/无法主动撤销:因为服务器不存状态,只要 JWT 没过期,它就一直是合法的。用户点击“退出登录”,仅仅是前端删除了 Token,如果黑客早就截获了这个 Token,他依然可以继续使用,直到过期。
    • 体积较大:每次请求都要带这么长一串字符串,增加网络带宽开销。
    • 秘钥就是生命:一旦后端的加密秘钥泄露,整个系统的防线瞬间崩溃,黑客可以自己伪造任意用户的身份。

Webstorage

比较维度 localStorage (本地存储) sessionStorage (会话存储) IndexedDB (前端数据库)
生命周期 永久持久化。除非用户手动清除浏览器缓存或通过代码删除,否则永远不过期。 页面级(极其短暂)。当前浏览器窗口或标签页关闭后,数据立刻被销毁。 永久持久化。除非手动清除或代码删除。
存储容量 一般约为 5MB(各浏览器厂商略有差异)。 一般约为 5MB 理论上无上限(通常可达 50MB 甚至占据硬盘可用空间的很大比例)。
数据类型 仅限字符串 (String)。存入对象必须先 JSON.stringify(),读取时再 JSON.parse() 仅限字符串 (String)。存取对象的处理方式同上。 原生支持 JS 对象(包括文件、Blob 等复杂格式),无需序列化。
读写机制 同步操作 (Synchronous)。如果在主线程读写极大块的字符串,会导致页面卡顿(阻塞渲染)。 同步操作 (Synchronous) 异步操作 (Asynchronous)。性能极高,处理庞大数据量时不会阻塞主线程渲染。
功能定位 简单的键值对(Key-Value)缓存仓库。 简单的键值对(Key-Value)临时缓存仓库。 真正的非关系型数据库(支持索引、事务、游标等高级数据库操作)。
同源策略 严格受限(仅限同协议、同域名、同端口访问)。 严格受限,且作用域更小(不仅要求同源,还必须在同一个标签页内)。 严格受限。
优缺点 :API 简单,存储空间比 Cookie 大。



:无法设置过期时间,只能存字符串,大文件操作卡顿。
:非常适合用完即走的临时敏感数据,安全性稍高。



:跨标签页无法共享数据。
:容量极大,异步高性能,能干真数据库的活。



:原生 API 操作极其繁琐复杂,学习成本高。
典型应用场景 长期保存的用户偏好设置(如夜间模式)、持久化的 Token、降低网络请求的静态资源缓存。 表单分步填写的临时草稿、一次性的敏感账号登录验证、单次会话的浏览记录。 离线 Web 应用(PWA)、大型前端在线文档/表格(如语雀/飞书文档)、缓存大量音视频文件。

浏览器执行机制

🗂️ 代码的翻译官(编译 vs 解释 vs JIT)

无论什么语言,最终都要变成 CPU 能懂的机器码。主流的翻译策略有三种:

语言阵营 翻译策略 执行特点
编译型 (C/C++, Go) 运行前,编译器一次性将源码全量翻译成二进制可执行文件。 启动慢,但运行时极快(直接跑二进制)。
解释型 (早期 JS, Python) 运行时,解释器看一行代码,动态翻译一行并执行。 跨平台好,启动快,但执行效率低。
V8 引擎 (现代 JS) JIT 即时编译(字节码 + 机器码)。结合了前两者的优点。 先生成轻量的字节码快速启动;遇到执行频繁的“热点代码”,JIT 编译器瞬间将其转为高效的机器码

🌳 AST(抽象语法树)的诞生流程

AST 是前端工程化(Babel、ESLint、Webpack)的基石。代码转化为 AST 必经两大关卡:

  1. 分词 / 词法分析 (Tokenizing)
    • 动作:将代码字符串“切碎”,剥离空格和注释。
    • 产物:不可再分的最小语义单元数组(Tokens)。
    • 举例var a = 2 ➡️ [var, a, =, 2]
  2. 解析 / 语法分析 (Parsing)
    • 动作:扫描 Tokens 数组,根据 JS 语法规则进行嵌套组装。
    • 产物:一棵代表程序结构的树(AST)。
    • 异常:如果在此阶段发现代码不符合规则(如少个括号),引擎直接抛出 SyntaxError(语法错误)。还有RefernceError、is not defined等

🔍 引擎的灵魂查询(LHS 与 RHS)

代码跑起来后,引擎遇到变量就会向“作用域”发起查询。千万别被“左”和“右”迷惑,要看核心目的

查询类型 目的 灵魂拷问 典型场景
LHS (Left) 找容器(赋值) “我要把值存进去,这个变量的容器在哪?” a = 2; (为 2 找个家)
RHS (Right) 找容器里面的值(取值) “我要用这个变量,它的具体值是什么?” console.log(a); (看看 a 里装了啥)

👑 终极实战拆解:

1
2
3
4
function foo(a) {        // 2. 隐式 LHS 查询:a = 2 (为传入的参数 2 找容器 a)
console.log(a); // 3. RHS 查询:提取 a 的值准备打印
}
foo(2); // 1. RHS 查询:查找 foo 这个函数的值以便执行

性能优化

一、 优化什么?

为了**“聚焦用户体验”**。

  • 四大体感指标:网站打开要快(首屏)、动画要丝滑(不掉帧)、表单提交要迅速、列表滚动和页面切换绝对不能卡顿。
  • 四大优化策略:尽快响应用户输入、确保动画流畅、最大化主线程空闲时间(别让 JS 把浏览器卡死)、提升网页可交互性。

二、 HTML / CSS / JS 优化

技术栈 核心优化原则 具体操作与避坑手段
HTML



(骨架)
精简结构,加速解析 1. 减少 DOM 层级嵌套,避免冗余标签(解析和遍历极其耗时)。

2. 必须使用语义化标签。

3. 严禁直接写内联 CSS(无法缓存,因为html动态,且难以维护)。

4. 避免 src 和 href 为空(会导致浏览器无效请求)。

5. 减少 DNS 查询数;(dns-prefetch与preconnect)使用 Viewport 加速渲染。
CSS



(皮囊)
规范选择器,减少重绘 1. 选择器避坑:避免过深的后代选择器,禁用通配符 *(CSS 是从右向左解析的,*会遍历整棵 DOM 树!),避免链式 ~

2. 加载避坑:坚决用 <link> 代替 @import@import 会导致请求串行变慢)。

3. 样式避坑:少用 float,精简属性(用 padding-left 代替 padding: 0 0 0 10px),0 值去单位。

4. 慎用昂贵属性box-shadowborder-radiusfilter 等渲染成本极高。

5. 最佳实践:用 Flexbox 替代老式布局,开启 Gzip 压缩(前端预压,nginx开启gzip配置),使用骨架屏/Loading。
JS



(肌肉)
减少 DOM 操作,异步加载 1. DOM 屠宰场:绝对避免在循环里操作 DOM!尽量在 JS 里拼好数据,一次性插入。

2. 事件委托:利用事件冒泡,把监听器绑在父元素上(省内存,且动态新增的子节点自动生效)。

3. 样式批处理:不要用 JS 逐个修改 style 属性,提前写好 CSS Class,通过切换 Class 名来集中改变样式,大幅减少重排(Reflow)。

4. 精简负担:按需加载,压缩文件,少用极其耗资源的 iframe
会造成阻塞吗
  1. CSS加载不会阻塞DOM树解析
  2. CSS加载会阻塞DOM树渲染
  3. CSS加载会阻塞后面JS执行

代码问题

  1. 频繁使用JSON.parse/JSON.stringify大对象
  2. 正则灾难性回溯
  3. 内存泄漏(闭包滥用、定时器忘了 clear、绑在全局 window 上的事件没解绑)

网络相关优化

DNS预解析、预加载、预渲染

标签 (Tag) 核心行为 作用范围 执行时机与网络优先级 典型应用场景
dns-prefetch



(DNS 预解析)
仅在后台提前解析目标域名的 IP 地址。 跨域/外部资源 立即执行



开销极小。
第三方 API 域名、CDN 图床、外部统计埋点。
preload



(预加载)
强制建立 TCP 连接并全速下载资源。 当前页面 立即执行



最高优先级 (Highest),抢占当前主线程带宽。
首屏强依赖的核心 CSS/JS、首屏大图、自定义字体。
prefetch



(预提取)
静默下载完整静态资源并存入本地磁盘缓存,但不执行。 未来页面 空闲时延后执行



最低优先级 (Lowest),须等当前页面完全加载完毕 (onload)。
按需加载的路由组件代码、极大概率点击的下一页资源。
prerender



(预渲染)
在后台开辟隐形标签页,完成下载、解析、JS 执行和完整的 DOM 渲染。 未来页面 空闲时延后执行



极低优先级,极度消耗设备 CPU、内存和网络带宽。
确定性极高会访问的下一步操作(注:因开销过大,现代浏览器标准已逐渐废弃此原生标签)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>极速电商首页</title>

<link rel="dns-prefetch" href="//img.alicdn.com">
<link rel="dns-prefetch" href="//api.tracking.com">
<link rel="preload" href="https://img.alicdn.com/banner-1111.jpg" as="image">
<link rel="preload" href="/fonts/custom-font.woff2" as="font" type="font/woff2" crossorigin>

<link rel="prefetch" href="/js/product-detail-chunk.js">

<link rel="prerender" href="/checkout.html">

<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<img src="https://img.alicdn.com/banner-1111.jpg" alt="双十一大促">
<script src="/js/main.js"></script>
</body>
</html>

SSR - Server-Side Rendering

  • 痛点(传统 SPA):用户请求页面时,服务器只返回一个“空荡荡的 HTML 骨架”。浏览器必须等几十数百 KB 的 JS 下载完并执行后,再去请求接口,最后才能把页面画出来(首屏白屏时间极长,且搜索引擎爬虫抓不到内容)。
  • SSR 的解法:服务器端直接运行 JS 框架(如 Vue/React),去查数据库或调接口,把数据塞进组件里,直接拼接出一个完整、带有内容的 HTML 字符串返回给浏览器。
  • 核心收益:浏览器拿到 HTML 就能直接显示(极大地优化了 FCP 首屏绘制时间),并且对 SEO(搜索引擎优化)绝对友好。

避免重定向 (Avoid Redirects)

  • 核心原理:每一次网络重定向(无论是 301、302 还是 307),都会强制浏览器中断当前动作,拿着新的 URL 重新走一遍“DNS 解析 -> TCP 握手 -> 发送请求”的完整流程。
  • 性能代价:在移动端或弱网环境下,一次无意义的重定向会白白浪费 100ms 到 300ms 的网络往返时间(RTT),严重拖慢页面加载。

重定向陷阱

1. Nginx:HTTP 重定向到 HTTPS 的标准姿势
  • Hack 做法 (error_page 497):这是用来处理“用户极度奇葩地用 HTTP 协议去访问了 443 加密端口”时的强制纠错。它有用,但不能作为常规的强转手段。error_page 497 https://$http_host$request_uri;
  • 行业标准做法 (return 301):专门监听 80(HTTP)端口,只要收到请求,立刻返回 301 永久重定向到 HTTPS。
1
2
3
4
5
server {
listen 80 http;
server_name localhost;
return 301 https://$host$request_uri;
}
  • 为什么标准做法更好? 因为浏览器收到官方的 301 状态码后,会在本地死死记住这个规则。下次用户再敲 HTTP,浏览器在本地直接转换,根本不会把旧请求发到网上去,从物理层面上消灭了网络延迟。
2. 致命的尾斜杠 / (Trailing Slash) 陷阱

这是一个极其容易引发二次请求和报错的隐蔽 BUG:

场景分类 尾斜杠的影响 底层逻辑与最终结论
1. 纯 SPA 前端路由

(Vue / React 客户端渲染)

如 /about vs /about/
极其安全,没影响

(0 性能损耗)
Nginx 硬盘里没这些目录。核心配置 try_files $uri $uri/ /index.html; 会直接把请求丢给 index.htmlNginx 的物理目录检测机制被绕过,由前端 JS 路由接管,不触发任何重定向。
2. SSG / MPA 物理目录 / 微前端

(Next.js 或 Nuxt.js 静态导出 / 传统多页)

(导出:/abc 路由 生成 /app/dist/abc/index.html;微前端同一个域名下的“子应用”划分)

如硬盘真有个 about 文件夹
性能刺客,白白多等 100ms

(触发 Nginx 301 重定向)
如果用户请求 xxx.com/about (没加斜杠),Nginx 发现硬盘里这是一个文件夹而不是文件。为了防止该文件夹里的相对路径(如 ./style.css)加载错乱,Nginx 会强制返回 301 重定向,让浏览器去请求 xxx.com/about/。这会白白浪费一次完整的网络请求时间!
3. 后端 API 接口请求

(Axios 发送 POST/GET)

如 /api/loginvs /api/login/
极其致命,直接导致接口报错

(触发后端 307 或 301 降级)
后端框架(如 FastAPI、Spring)对路由定义极其严格。如果后端定义带有斜杠,前端漏写了:

1. 后端直接返回 307/301。

2. 浏览器被迫重新发请求。

3. 最恐怖的是:POST 请求在重定向时大概率会被强制降级为 GET 请求,并彻底丢弃 Payload(请求体数据),直接导致 405 Method Not Allowed

渲染优化

概念 核心目标 最佳业务场景 底层动作
懒执行 节省首屏 JS 执行时间和 CPU。 复杂组件、首屏看不见的重度计算。 按需触发执行。
懒加载 节省网络带宽和并发请求数。 长列表中的商品图、文章配图。 视口检测IntersectionObserver + src 替换。
防抖 (Debounce) 确保高频动作只在最终停止时执行一次。 输入框搜索联想、提交表单防重复点击。 “只要你一直动,我就不执行。”
节流 (Throttle) 确保高频动作在规定时间内匀速地降频执行 监听滚动位置 (scroll)、拖拽元素 (mousemove)。
例如CF游戏射击,不管点多快一段时间内射出的子弹不太多的
“不管你动多快,我按我的节奏来。”

图片优化

  • 请求拥堵
  • 体积超载

方案一:精准选型(常见图片格式对比表)

格式 压缩机制 优缺点对比 最佳适用场景
JPEG / JPG 有损压缩 优点:体积小,色彩表现丰富。

缺点:不支持透明底,压缩率过高会导致边缘模糊(锯齿)。
首页轮播图、大背景图、Banner、高清实拍照片。
PNG 无损压缩 优点:质量极高,边缘清晰,完全支持透明度。

缺点:同等分辨率和色彩下,体积通常比 JPG 大。
页面 Logo、需要透明底的复杂图形、带文字说明的插画。
WebP 支持有损/无损 优点:支持透明,同等画质下体积比 PNG/JPG 小 25%~34%

缺点:旧版 Safari/IE 兼容性较差。
现代 Web 的默认首选

(通常配合 <picture> 标签为旧浏览器做 JPG 降级处理)
SVG 矢量代码计算 优点:体积极小,无论放大多少倍都不失真,可用 CSS/JS 直接修改颜色。

缺点:渲染极其复杂的图形时极其耗费 CPU 算力。
纯色或简单多色的 UI 图标、各类矢量数据图表。
GIF 动图



(无损/有损)
优点:支持动画和透明底,兼容性无敌。

缺点:色彩表现力极弱(最多 256 色),画质差,动图体积容易爆炸。
极其简单的 loading 动画、小型动图表情包。

方案二:网络减负(减少 HTTP 请求数的战术表)

优化战术 核心工作原理 核心优势 劣势与使用边界
雪碧图

(CSS Sprites)
将几十个小图标拼合成 1 张大图,利用 CSS background-position 定位切图显示。 化零为整:将几十次 HTTP 请求合并为 1 次,减轻服务器压力,且能解决 Hover 切换时的图片闪烁 Bug。 维护极其痛苦,改一个图标可能要重拼整张图;容易导致加载了整张图中根本没用到的多余图标。
Iconfont

(字体图标)
将纯色矢量图标打包成特定的 Web 字体文件(如 .woff)。 极其轻量:一次请求搞定全站图标,且能像文字一样用 CSS 随意修改颜色、大小、阴影。 只能是单色(或利用特定技术实现简单多色),完全无法胜任复杂的彩色图片表现。
Base64 内联 将图片转换为 Base64 编码的字符串,直接写死在 HTML/CSS/JS 代码文件中。 彻底消灭请求:无需发起额外的网络请求,图片随代码文件一起下载解析。 字符串体积会比原二进制图片大 33% 左右,且严重阻塞代码解析。

👉 仅限极小、更新极低、首屏急需的图标使用。

方案三:工程压榨(自动化与动态裁剪表)

技术方向 具体实施方案 核心收益与最终效果
构建时压缩

(Webpack)
在前端打包工具中配置 image-webpack-loader 等自动化压缩插件。 在执行 build 打包时,全自动剔除所有图片的冗余元信息并无感知瘦身,彻底释放前端开发者的双手。
云端动态裁剪

(CDN 赋能)
前端通过 JS 获取当前设备屏幕宽度,在请求 URL 上动态拼接参数(如 ?w=375),让云端 CDN 实时裁剪后下发。 确保移动端绝对不去加载 PC 端的大图,按需下发,极致节省用户带宽和手机内存,极大地提升渲染速度。
纯 CSS 替代

(降维打击)
凡是能用 CSS3(阴影、渐变色、圆角、甚至三角形)手写出来的修饰效果,坚决不切图。 真正的“零”请求,DOM 和 CSS 的渲染速度远快于图片的解码速度,且完美适配任何分辨率的响应式设计。

CDN图片

  • 图片懒加载(见上文)
  • 图片预加载(preload)
  • 响应式图加载(不同设备不同图片)
  • 渐进式图加载(在图完全加载完前先显示低画质版本)

其他文件优化

  1. 服务端开启文件压缩功能
  2. 执行 JS 代码过长会卡住渲染,对于需要很多时间计算的代码可以使用 Webworker

webWorker是运行在后台的JS,另开一个子线程,不会影响性能

CDN

内容分发网络,静态资源使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名

其他优化

  • 使用 Webpack 优化
  1. 对于 Webpack,打包项目使用 production 模式,会自动开启代码压缩
  2. ES6 模块开启 tree shaking,移除没有使用的代码
  3. 优化图片,对于小图使用 base64 的方式写入文件
  4. 按照路由拆分代码,实现按需加载
  5. 给打包出来的文件名添加哈希,所以部署后,用户依然可以复用浏览器缓存的部分文件
  • 监控
    • 采集 (Collect):前端代码里埋伏着“哨兵”。利用 window.onerror 抓取 JS 报错,利用 unhandledrejection 抓取 Promise 崩溃,利用 Performance API 记录首屏时间(LCP)和接口耗时。
    • 上传 (Upload):为了不抢占业务接口的网速,通常会在页面卸载时利用 navigator.sendBeacon(),或者极其轻量的 1x1 像素 GIF 图,把收集到的错误数据悄悄发给监控服务器。
    • 分析 (Analyze):线上的代码都是经过 Webpack 压缩加密的乱码(比如 a.b is not a function)。监控服务端需要利用打包时生成的 Source Map(源码映射表),把乱码还原成你写的真实的 Vue/React 源码的第几行第几列。
    • 报警 (Alert):当某个接口的报错率在 5 分钟内飙升超过 5%,或者首屏白屏率大增时,立刻通过企业微信、钉钉或邮件触发报警,把开发人员从被窝里拉起来排障。
  • 虚拟列表?

SPA首屏优化

浏览器从响应用户输入网址地址,到首屏内容渲染完成时间,整个网页不一定要完全渲染完成,但需要展示当前视窗内容

加载慢的原因

  1. 网络延时
  2. 资源文件体积过大
  3. 资源加载重复发送请求
  4. 加载脚本时,渲染内容阻塞

解决

  1. 减少入口文件体积
  2. 静态资源本地缓存
  3. UI框架按需加载
  4. 图片资源压缩
  5. 组件重复打包
  6. 开启GZip压缩
  7. 使用SSR
  8. 二次启动时先利用缓存渲染,后台进行异步数据更新
  9. Ajax采用缓存调用

网址后面加上“/”:对服务器而言,不加斜杠服务器会多一次判断的过程,加斜杠就会直接返回网站设置的存放在网站根目录下的默认页面。

页面白屏

网络请求,返回状态
组件样式布局,组价未显示

网页卡顿原因

  1. 网络请求是否过多,导致数据传输变慢,可通过缓存优化
  2. 资源bundle太大,考虑拆分
  3. 代码是否有太多循环在主线程上花费太长时间
  4. 浏览器某个帧 中 渲染太多东西
  5. 页面渲染时,大量回流和重绘
  6. 内存泄露

动画性能优化

  1. 合理布局
  2. transform代替left、top 减少重排
  3. 硬件加速
  4. 避免不必要的图形层
  5. requestAnimationFrame实现动画

动画每一帧都是re-render,显示器刷新频率 60 HZ,意味着每一帧任务耗时不超过 16ms

前端安全

XSS跨站脚本攻击

Cross Site Scripting

用户输入或使用其他方式向代码中注入其他JS,然后JS代码被执行。

  1. 可能是写一个死循环、获取cookie登录
  2. 监听用户行为
  3. 修改DOM伪造登录表单
  4. 页面生成浮窗广告

1. 反射型 (Reflected XSS)

  • 核心一句话:服务器是个“复读机”,把你丢给它的恶意代码原样念了出来,结果念到了你自己的浏览器里执行了。
  • 例子
    • 场景:网站有个报错页面,URL 是 site.com/error?msg=登录失败,页面上会显示“提示:登录失败”。
    • 攻击:黑客发给你一个链接 site.com/error?msg=<script>偷Cookie</script>
    • 结果:你一点开,服务器这个复读机直接返回 HTML:“提示:<script>偷Cookie</script>”。你的浏览器立刻执行,你中招了。

2. 存储型 (Stored XSS)

  • 核心一句话:恶意代码被黑客存进了网站的数据库里,谁来看这个页面,谁就踩雷爆炸。
  • 极简例子
    • 场景:淘宝商品底下的买家评论区。
    • 攻击:黑客买了个东西,留下一句评论:“东西不错<script>偷Cookie</script>”。这句话被永久保存在了淘宝的数据库里。
    • 结果:第二天,有 1000 个正常顾客点开了这个商品页。淘宝服务器把这条评论从数据库提出来发给这 1000 个人。这 1000 个人的浏览器同时弹窗中招。

3. DOM 型 (DOM-based XSS)

  • 核心一句话:服务器全程没参与(完全无辜),是纯前端 JS 代码自己写得烂,把 URL 里的毒药亲手喂给了页面。
  • 极简例子
    • 场景:前端写了一段代码,读取 URL 井号后面的字来切换中英文:div.innerHTML = window.location.hash
    • 攻击:黑客给你发链接 site.com/#<img src=x onerror=偷Cookie>
    • 结果:浏览器井号 # 后面的内容是不会发给服务器的。但是,你的前端 JS 脚本一运行,直接把这段毒药塞进了 div 里。浏览器渲染,你中招了。

4. 文档型 (Document XSS / 中间人)

  • 核心一句话:网站代码和服务器 100% 安全,但数据在半空中的网线/路由器里被黑客掉包了。
  • 极简例子
    • 场景:你在咖啡厅连了一个叫“免费星巴克 WiFi”的黑客路由器,访问了一个没有加密 (HTTP) 的小说网站。
    • 攻击:小说网站的服务器正常发回了干净的网页。但是数据经过黑客的 WiFi 路由器时,黑客在数据包里强行塞入了一句 <script>偷Cookie</script>
    • 结果:你的手机最终收到了被篡改过的网页并执行,你中招了。

防范:

1.对输入转码过滤

2.利用CSP

关键指令 (Directive) 管控的资源类型 核心作用
script-src JS 脚本(防 XSS 的重中之重) 规定浏览器只能从哪些域名下载 JS,以及是否允许执行内联代码。
default-src 全局默认规则 如果你没写 script-src 或 img-src,就全盘按这个默认的白名单来兜底。
img-src 图片资源 规定只能从哪里加载图片,防止黑客用 <img src="恶意网站"> 来偷数据。
connect-src AJAX / Fetch 请求 规定 fetch 或 axios 只能向哪些域名发请求,直接切断黑客偷完 Cookie 后往外发送的通道
例如:
1
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.aliyun.com; connect-src 'self'; object-src 'none';

3.HttpOnly

HttpOnly类型的cookie阻止JS对其的访问(标记或授权对话)

跨站伪造请求

Cross-Site Request Forgery

攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证(比如cookie) ,绕过后台的用户验证,因此可以冒充用户对被攻击的网站执行某项操作。

利用服务器的验证漏洞和用户之前的登录状态模拟用户操作。

点击链接后,可能发生3件事

  1. 自动发送GET请求。利用src发送请求
  2. 自动发送POST请求
  3. 诱导点击发送GET请求

防范

1.SameSite

SameSite可以设置为三个值,Strict、Lax和None。

  • a. 在Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。

  • b. 在Lax模式,宽松一点,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。

  • c. 在None模式下,也就是默认模式,请求会自动携带上 Cookie。

2.验证来源站点

请求头中的origin和referer

origin只包含域名信息,referer包含具体的URL路径。

3.CSRF Roken

利用token(后端生成的一个唯一登陆态,传给前端保存)每次前端请求都会带token,后端检验通过才同意请求。

  • 敏感操作需要确认
  • 敏感信息的cookie只能有较短的生命周期

4.安全框架

如Spring Security。

SQL注入攻击

Sql 注入攻击,将恶意的 Sql查询或添加语句插入到应用的输入参数中,再在后台 Sql服务器上解析执行。
例子:

  • 黑客输入:账号框里填入 admin' -- (注意这里有一个单引号和两个减号),密码随便填。

  • 最终执行的 SQLSELECT * FROM users WHERE username = 'admin' --' AND password = '随便填'

  • 结果:不用输入密码就完成登陆

  • 核心防范手段:尽量不字符串拼接,采用ORM框架参数化查询

其他种类的攻击:

DDoS全称Distributed Denial of Service:分布式拒绝服务攻击。是拒绝服务攻击的升级版。拒绝攻击服务顾名思义,让服务不可用。常用于攻击对外提供服务的服务器,像常见的:

  • Web服务
  • 邮件服务
  • DNS服务
  • 即时通讯服务

DNS劫持(劫持网络链路,对应错的ip,页面可能却一样?)
JSON劫持
暴力破解
HTTP报头追踪漏洞(TRACE请求)
信息泄露
目录遍历漏洞
命令执行漏洞
文件上传漏洞等等

HTTPS中间人攻击

客户端和服务器之间的桥梁、双向获取并且篡改信息

标准回答

攻击者通过与客户端和客户端的目标服务器同时建立连接,作为客户端和服务器的桥梁,处理双方的数据,整个会话期间的内容几乎完全被攻击者控制。攻击者可以拦截双方的会话并且插入新的数据内容

加分回答

中间人攻击的过程:

  1. 本地请求被劫持,所有请求均发送到中间人服务器
  2. 中间人服务器返回中间人自己的证书
  3. 客户端创建随机数,通过中间人证书中的公钥加密 传送给中间人,凭随机数构造对称加密对传输内容加密传输
  4. 中间人拥有客户端返回的随机数,可以对内容解密
  5. 中间人以客户端的请求内容向真的服务器发送请求
  6. 服务器通过建立的通道返回加密后的数据
  7. 中间人对加密算法内容解密
  8. 中间人对内容加密传输
  9. 客户端通过和中间人建立的对称加密算法对返回数据解密

缺少证书的验证,客户端完全不知道自己的网络被拦截,数据被中间人窃取

CDN

📦 为什么要建 CDN?

想象一下春运期间的 12306 网站:

  • 没有 CDN(单点发货):1 亿人同时请求一张验证码图片,全都要挤进“长途骨干网”,跨越千山万水去北京的机房拿。结果就是:骨干网大塞车(网络拥堵)、机房大爆炸(服务器宕机)、偏远地区白屏(物理距离带来的高延迟)
  • 有 CDN(前置仓直发):12306 提前把这张图片分发到了全国各地(比如武汉、广州、成都)的 CDN 节点。广州的用户要看图,直接从广州本地的机房发给他,完全不需要走国家长途骨干网。速度拉满,源站躺平。

🏭 CDN 的物理组成

CDN 并不是一台超级计算机,而是一套分布式的网络系统,主要由**“一中心,多边缘”**构成:

组成部分 官方术语 “物流网”比喻 核心职责
中心节点 网管中心 + GSLB (全局负载均衡) 北京全国物流总调度室 不存货,只管人。 负责统筹全网状况,判断哪个省的仓库还有余力,把用户的请求精准导航过去。
边缘节点 区域负载均衡 (RLB) 华南区/广州调度分中心 监控广州本地各个具体仓库的死活、负载情况和距离用户的远近。
边缘节点 Cache (高速缓存服务器) 天河区具体的街道前置仓 只存货,干苦力。 里面存着真正的图片和视频,直接面向用户发货。

🗺️ 极其硬核的“取货全流程”

当你(用户)在浏览器输入 https://cdn.site.com/image.jpg 时,真正的暗战开始了。请跟着下面的步骤走,这是面试遇到高级网络题时的满分回答逻辑

第一阶段:DNS 的“击鼓传花”

  1. 本地寻址:浏览器问本地 DNS:“cdn.site.com 的 IP 是多少?”
  2. CNAME 甩锅:本地 DNS 查了一下,发现这个域名被设置了 CNAME 记录(别名),指向了 CDN 厂商的专用 DNS 服务器(比如阿里云的 aliyundns.com)。
  3. 接管路权:域名解析权正式移交给 CDN 的专属 DNS 系统。CDN 的 DNS 掐指一算,把请求甩给了中心节点(GSLB 全局负载均衡设备)

第二阶段:层层筛选“最优解”(负载均衡大发神威) 
4.  全局调度 (GSLB):中心节点看着用户的 IP 地址(比如发现是广东联通),于是向广东区域的负载均衡设备 (RLB) 发出指令:“你那边挑个最好的机器接待他”。
5.  区域盘点 (RLB):区域调度器开始综合考量三个条件: * 算距离:哪个节点离这个广东联通的 IP 最近? * 看状态:那个最近的节点现在是不是快满载(炸)了? * 查库存:那个节点里到底有没有这张 image.jpg 图片?
6.  确定目标:区域调度器选出了一台状态完美、距离最近的 Cache 缓存服务器,并把它的 IP 地址层层上报,最终通过 GSLB 返回给了用户的浏览器。

第三阶段:一手交钱,一手交货 
7.  直奔目标:浏览器终于拿到了这台最完美的 CDN 缓存服务器的 IP,直接向它发起 HTTP 请求。
8.  成功返回:缓存服务器把图片吐给浏览器。如果这台机器上刚好没这张图(回源),它会自己跑去源站(网站真正的服务器)拉取一份存下来,再发给用户。

Web Worker

注意

  1. 同源限制
  2. DOM限制
  3. 无法读取主线程所在网页的DOM对象,但可读navigator/location对象
  4. worker线程和主线程不在同一个上下文环境,不能直接通信
  5. worker不能执行alert()/confirm(),但可以发出Ajax请求

微前端

  1. 将庞大应用拆分,每个部分可以单独部署 、维护,提升效率
  2. 整合系统,在基本不修改原来系统逻辑的同时 兼容新老老套系统并行运行
技术方案 通俗比喻与核心机制 优点 致命缺点
1. Nginx 路由转发 “换乘地铁”



根据 URL 上的 /app1 或 /app2,运维在 Nginx 层面直接把请求打给不同的静态资源服务器。
极其简单,根本不需要改前端代码。 体验极差。每次切换应用,浏览器都会白屏刷新,根本不是单页面体验。
2. iframe 嵌套 “画地为牢”



主应用留一个框,把子应用用 <iframe> 标签嵌进来。
完美的天然隔离。自带沙箱,JS/CSS 绝对不会互相污染。 显得 Low。弹窗无法覆盖全局、UI 难以统一、通信困难(只能靠 postMessage跨域喊话)。
3. Web Components “官方造积木”



使用浏览器原生的自定义元素 API 来封装微应用。
浏览器原生支持,隔离性极好。 现存的旧系统改造成本极高,生态还在发展中,踩坑多。
4. 组合式路由分发



(当下主流,如 qiankun)
“中央集权调度”



纯前端实现。主应用就是一个空壳基座,监听路由变化。当你点击“财务模块”时,主应用通过 JS 把财务子应用的 HTML/JS/CSS 动态拉取过来,渲染到指定的 div 里。
体验完美,无缝切换。完全保留了 SPA 的顺滑感。 技术最复杂!必须解决**“样式冲突”“全局变量污染”**的两大世纪难题。

既然大家都运行在同一个浏览器的同一个 window 环境下(主流的组合式路由分发方案),如果不做隔离,A 应用写了一句 window.name = 'appA',B 应用去读的时候就会天下大乱;A 应用写了一个 .btn { color: red },B 应用的按钮也跟着变红了。

为了解决这个问题,基座框架(比如阿里的 qiankun)在底层做了两手准备:

1. CSS 隔离(防止样式打架)

  • 工程化规避:在 Webpack 打包时,利用 CSS Modules 或者给每个子应用强制加上独特的前缀(比如 .app-vue-btn),从根源上错开名字。
  • 动态卸载机制:主应用在加载 A 子应用时,把 A 的 <style> 标签打个标记塞进页面。当用户切到 B 应用时,主应用会立刻把 A 的所有 <style> 标签连根拔起全部删掉,然后再插上 B 的样式。眼不见心不烦。

2. JS 隔离(防微杜渐的“沙箱 Sandbox”)

这是微前端最核心的黑科技。绝对不能让子应用直接触碰真正的浏览器 window 全局对象。

  • Proxy 代理沙箱(现代浏览器的主流做法): 主应用会用 Proxy 给 window 披上一件“隐形斗篷”(创建一个伪造的 window 对象)交个子应用去玩。
    • 子应用 A 以为自己在修改全局变量:window.admin = '小明'
    • 其实它只是修改了专属 A 的那个伪造对象。
    • 当切到子应用 B 时,主应用会换上专属 B 的伪造对象。这样,大家都在自己的沙箱里玩泥巴,再也不会互相覆盖了。

浏览器与网络
http://example.com/2026/03/13/浏览器与网络/
作者
Lingkai Shi
发布于
2026年3月13日
许可协议