Skip to content

第 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

维度XHRfetch
写法回调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,浏览器会自动加 boundary

2.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 1URL 2同源?
http://a.com/xhttp://a.com/y
http://a.comhttps://a.com❌ 协议不同
http://a.comhttp://b.com❌ 域名不同
http://a.com:80http://a.com:8080❌ 端口不同
http://a.comhttp://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)
postMessageiframe 跨域通信第三方嵌入
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: 86400

OPTIONS 通过后才发真正的请求。

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永久异步离线应用、大量数据
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);
};

💡 IndexedDB 原生 API 难用,推荐用 idbDexie.js 包装。

4.4 选哪个?

text
┌─────────────────────────────────────────────────┐
│ 数据 < 5MB ?                                    │
│   ├─ 是 → 需要随请求发送?                        │
│   │       ├─ 是 → Cookie                       │
│   │       └─ 否 → 关闭 tab 要保留?              │
│   │              ├─ 是 → LocalStorage          │
│   │              └─ 否 → SessionStorage        │
│   └─ 否 → IndexedDB                            │
└─────────────────────────────────────────────────┘

5. 安全:XSS、CSRF、Cookie 防护

5.1 XSS(跨站脚本)

  • 危害:恶意 JS 注入到页面,盗 cookie / 篡改内容
  • 防御
    • 输出转义(<&lt;
    • cookie 加 HttpOnly(JS 读不到)
    • CSP 头限制脚本源

5.2 CSRF(跨站请求伪造)

  • 危害:用户在 a.com 登录后,访问恶意 b.com 自动以你身份发请求到 a.com
  • 防御
    • cookie 加 SameSite=Lax/Strict
    • 关键操作要 token(CSRF Token)
    • 校验 Referer / Origin

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. 延伸阅读