主题
第 08 章 · 网络与存储(Fetch / 存储 / 跨域)
一句话开篇:本章告诉你"前端怎么和后端打交道(Fetch / XHR)、怎么存数据(Cookie / Storage / IndexedDB)、为什么会跨域以及怎么解决"——是写"动起来的网页"必备技能。
0. 生活类比(先建立直觉)
| 概念 | 类比 |
|---|---|
| HTTP 请求 | 你点了一份外卖(前端 → 后端) |
| 响应 | 外卖送到你家(后端 → 前端) |
| Cookie | 商家记着你的会员卡号,每次自动带上 |
| LocalStorage | 你家里的"长期橱柜",关电脑也不丢 |
| SessionStorage | 你家"临时桌子",关窗口就清空 |
| IndexedDB | 你家"地下大仓库",能放几 G 的结构化数据 |
| 同源策略 | 不同小区不能互相串门(浏览器安全护栏) |
| CORS | 物业开了"白名单",允许某些小区进入 |
| AbortController | 你点完外卖反悔了,按"取消订单"键 |
1. AJAX 简史
AJAX(Asynchronous JavaScript And XML):让网页不刷新就能向后端发请求。 今天主流是 fetch,老项目还会见到 XHR。
1.1 XHR(XMLHttpRequest,老 API)
js
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status === 200) console.log(xhr.response);
};
xhr.onerror = () => console.error('网络错误');
xhr.send();痛点:
- 回调写法,嵌套混乱
- 需要手动 setRequestHeader、监听 readystatechange
- 没有 Promise 化
1.2 Fetch(现代 API)
js
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));或 async:
js
async function loadUsers() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error(res.statusText);
return res.json();
}1.3 fetch vs XHR
| 维度 | XHR | fetch |
|---|---|---|
| 写法 | 回调 | Promise |
| 兼容性 | IE6+ | 主流现代浏览器 |
| 默认带 cookie | ✅ | ❌(要 credentials: 'include') |
| 进度监听 | ✅(onprogress) | ❌(要用 ReadableStream) |
| 取消 | xhr.abort() | AbortController |
| HTTP 错误自动 reject | ❌(4xx/5xx 也走 onload) | ❌(也要自己判断 res.ok) |
⚠️ fetch 不会自动 reject HTTP 错误!只有网络错误(断网、CORS)才 reject,404/500 是 resolve 的,要手动判断
res.ok。
2. Fetch 详解
2.1 完整签名
js
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Tom' }),
mode: 'cors', // cors / no-cors / same-origin
credentials: 'include', // omit / same-origin / include
cache: 'default', // 缓存策略
redirect: 'follow', // 重定向策略
signal: ctrl.signal // 取消
});2.2 响应 Body 的不同读法
js
const res = await fetch(url);
await res.json(); // JSON
await res.text(); // 文本
await res.blob(); // 二进制(图片、文件)
await res.arrayBuffer();// 字节
await res.formData(); // 表单数据⚠️ Body 只能读一次! 读过一次就不能再读了,需要先
res.clone()。
2.3 取消请求 — AbortController
js
const ctrl = new AbortController();
fetch('/api', { signal: ctrl.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') console.log('已取消');
});
setTimeout(() => ctrl.abort(), 3000); // 3s 后取消2.4 上传文件
js
const fd = new FormData();
fd.append('avatar', file);
fd.append('name', 'Tom');
await fetch('/upload', { method: 'POST', body: fd });
// 不要手动 set Content-Type,浏览器会自动加 boundary2.5 错误处理最佳实践
js
async function safeFetch(url, opts) {
try {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null;
console.error('请求失败:', err);
throw err;
}
}3. 同源策略与跨域
3.1 什么是同源
同源 = 协议 + 域名 + 端口 完全相同。
| URL 1 | URL 2 | 同源? |
|---|---|---|
http://a.com/x | http://a.com/y | ✅ |
http://a.com | https://a.com | ❌ 协议不同 |
http://a.com | http://b.com | ❌ 域名不同 |
http://a.com:80 | http://a.com:8080 | ❌ 端口不同 |
http://a.com | http://sub.a.com | ❌ 子域不同 |
3.2 同源策略限制了什么
- ❌ AJAX 请求不同源
- ❌ 读取不同源 iframe 的 DOM
- ❌ 读取不同源 Cookie / LocalStorage
- ✅ 不限制
<script>、<img>、<link>加载(但 JS 不能读到内容)
3.3 跨域解决方案
| 方案 | 原理 | 应用 |
|---|---|---|
| CORS | 服务端在响应头加 Access-Control-Allow-Origin | 主流方案 |
| JSONP | 利用 <script> 不受限,后端返回 JS 调用 | 老项目,仅 GET |
| 代理服务器 | 同源前端请求自己的 Node 后端,由后端转发 | 开发期常用(vite proxy) |
| postMessage | iframe 跨域通信 | 第三方嵌入 |
| Nginx 反向代理 | 配置同域,转发到不同后端 | 生产部署 |
3.4 CORS 详解
简单请求
满足以下条件 → 简单请求:
- 方法:GET / POST / HEAD
- Content-Type:
text/plain/multipart/form-data/application/x-www-form-urlencoded - 无自定义头部
浏览器直接发请求,服务端响应头有 Access-Control-Allow-Origin: 允许的源 就放行。
预检请求(Preflight)
不满足简单请求 → 浏览器先发一次 OPTIONS 请求询问:
http
OPTIONS /api HTTP/1.1
Origin: https://a.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Token服务端响应:
http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, X-Token
Access-Control-Max-Age: 86400OPTIONS 通过后才发真正的请求。
携带 Cookie
js
// 前端
fetch(url, { credentials: 'include' });
// 后端
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://a.com // 不能是 *4. 客户端存储
四大方式:
| 方式 | 容量 | 生命周期 | 是否随请求发送 | 是否同步 | 适用场景 |
|---|---|---|---|---|---|
| Cookie | ~4KB | 自定义/会话 | ✅ 自动 | 同步 | 登录态、SSO |
| LocalStorage | ~5MB | 永久 | ❌ | 同步 | 用户偏好、缓存数据 |
| SessionStorage | ~5MB | 当前 tab 关闭即清 | ❌ | 同步 | 表单临时草稿 |
| IndexedDB | 几百 MB ~ GB | 永久 | ❌ | 异步 | 离线应用、大量数据 |
4.1 Cookie
js
// 设置(带各种属性)
document.cookie = 'token=abc; path=/; max-age=3600; secure; samesite=strict';
// 读取(所有 cookie 一锅端,自己解析)
document.cookie; // 'token=abc; user=tom'关键属性:
Expires / Max-Age:过期时间Domain / Path:作用域Secure:仅 HTTPS 发送HttpOnly:JS 不能读(防 XSS 偷 cookie)SameSite:跨站发送策略(防 CSRF)Strict:完全禁止跨站Lax:导航时允许(默认)None:允许(必须配合 Secure)
4.2 LocalStorage / SessionStorage
js
// 写
localStorage.setItem('user', JSON.stringify({ name: 'Tom' }));
// 读
const user = JSON.parse(localStorage.getItem('user') || '{}');
// 删
localStorage.removeItem('user');
localStorage.clear();
// 监听其他标签页修改(同源跨标签页通信)
window.addEventListener('storage', e => {
console.log('key:', e.key, 'old:', e.oldValue, 'new:', e.newValue);
});4.3 IndexedDB
浏览器内置的异步、事务型 NoSQL 数据库。
js
// 1. 打开数据库
const req = indexedDB.open('myDB', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
store.createIndex('byTitle', 'title', { unique: false });
};
req.onsuccess = (e) => {
const db = e.target.result;
// 2. 增删改查通过事务
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
store.add({ title: 'hello', content: 'world' });
store.getAll().onsuccess = (ev) => console.log(ev.target.result);
};4.4 选哪个?
text
┌─────────────────────────────────────────────────┐
│ 数据 < 5MB ? │
│ ├─ 是 → 需要随请求发送? │
│ │ ├─ 是 → Cookie │
│ │ └─ 否 → 关闭 tab 要保留? │
│ │ ├─ 是 → LocalStorage │
│ │ └─ 否 → SessionStorage │
│ └─ 否 → IndexedDB │
└─────────────────────────────────────────────────┘5. 安全:XSS、CSRF、Cookie 防护
5.1 XSS(跨站脚本)
- 危害:恶意 JS 注入到页面,盗 cookie / 篡改内容
- 防御:
- 输出转义(
<→<) - cookie 加
HttpOnly(JS 读不到) - CSP 头限制脚本源
- 输出转义(
5.2 CSRF(跨站请求伪造)
- 危害:用户在 a.com 登录后,访问恶意 b.com 自动以你身份发请求到 a.com
- 防御:
- cookie 加
SameSite=Lax/Strict - 关键操作要 token(CSRF Token)
- 校验 Referer / Origin
- cookie 加
6. ⚠️ 易踩的坑
- 坑 1:fetch 4xx/5xx 不 reject,要
if (!res.ok) throw - 坑 2:fetch 默认不带 cookie,跨域要
credentials: 'include' - 坑 3:response body 只能读一次,重复读要
res.clone() - 坑 4:跨域请求带 cookie 时,后端不能用
*,必须明确 Origin - 坑 5:localStorage 只能存字符串,对象要 JSON.stringify
- 坑 6:localStorage 满了报 QuotaExceededError,要 try/catch
- 坑 7:cookie 太大会让每个请求都变重,敏感数据放 localStorage
- 坑 8:privatebrowsing 模式 storage 可能不可用
- 坑 9:HTTPS 网站不能请求 HTTP 接口(Mixed Content)
- 坑 10:IndexedDB 每次操作都要事务,初学者容易嫌烦,用包装库
7. 一句话总结
前端跟后端聊天用 fetch(注意 4xx 不 reject、默认不带 cookie);存东西分四档——Cookie 自动带、LocalStorage 长期、SessionStorage 临时、IndexedDB 大仓库;跨域的根因是同源策略,主流靠 CORS 解决。
8. 延伸阅读
- MDN:Fetch API
- MDN:CORS
- MDN:IndexedDB
- 阮一峰:跨域资源共享 CORS 详解