一、Web Storage(Web 存储)
1. 概念
Web Storage(Web 存储)是 HTML5 提供的一种在浏览器本地存储数据的机制。你可以把它理解为浏览器提供的一个"本地小仓库"。
它最大的特点是:数据只保存在用户的浏览器里,不会像 Cookie 那样在每次请求时都发给服务器,所以它更安全,也不会浪费网络带宽。
2. 特征
Web Storage 主要分为 localStorage 和 sessionStorage,它们有以下几个共同和不同的特征:
- 同源性限制 :只有在同一个域名下才能访问。比如
a.com存的数据,b.com是看不到的。 - 只能存字符串 :这是新手最容易踩坑的地方!如果你存一个对象(Object)或数组,它会自动变成字符串
"[object Object]"。必须用JSON.stringify()转成字符串再存,取出来时用JSON.parse()转回对象。 - 容量较大:通常每个域名有 5MB 左右的存储空间(比 Cookie 的 4KB 大多了)。
- 生命周期不同 :
- localStorage:永久有效,除非手动删除。
- sessionStorage:关闭当前标签页即失效。
3. 使用场景
| 存储类型 | 适用场景 | 典型示例 |
|---|---|---|
| localStorage | 用户个人的、不需要频繁更新的、希望下次打开还在的数据 | 深色模式 / 字体大小设置、记住用户名、缓存不常变动的列表数据 |
| sessionStorage | 临时的、只在当前操作流中有效的数据 | 多步表单的临时数据、搜索结果页面的过滤条件、防止表单重复提交的临时标识 |
4. 使用方法
它们提供的方法非常简单,只有这几个:setItem(存)、getItem(取)、removeItem(删)、clear(清空)。
场景一:使用 localStorage 记住用户的"夜间模式"偏好
假设我们做了一个博客,用户点击按钮切换到夜间模式,我们希望他下次打开还是夜间模式。
javascript
// 1. 存储数据(存)
// 假设用户点击了切换按钮
const theme = 'dark';
localStorage.setItem('userTheme', theme);
// 2. 读取数据(取)
// 在网页刚加载时,我们读取这个设置
const savedTheme = localStorage.getItem('userTheme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode'); // 给网页加上夜间模式样式
}
// 3. 删除数据(删)
// 如果用户想恢复默认设置
localStorage.removeItem('userTheme');
场景二:使用 sessionStorage 暂存多步表单
假设你在做一个"发布文章"的功能,第一步填标题,第二步填正文。为了防止用户在第二步不小心刷新页面导致第一步填的标题没了:
javascript
// 第一步:用户填完标题,点击"下一步"时暂存
const title = document.getElementById('titleInput').value;
sessionStorage.setItem('draftTitle', title);
// 第二步:在正文页面的初始化代码中,检查有没有草稿
const draftTitle = sessionStorage.getItem('draftTitle');
if (draftTitle) {
document.getElementById('titleInput').value = draftTitle; // 自动填回标题
}
5. 避坑指南
如何存储对象?
在实际开发中,我们很少只存简单的字符串,经常要存对象或数组。记住这个万能公式:
javascript
const userInfo = { name: '张三', age: 18 };
// ❌ 错误写法:直接存对象
localStorage.setItem('user', userInfo); // 存进去会变成 "[object Object]"
// ✅ 正确写法:存之前转成 JSON 字符串
localStorage.setItem('user', JSON.stringify(userInfo));
// ✅ 正确写法:取出来时解析回对象
const userStr = localStorage.getItem('user');
const realUser = JSON.parse(userStr);
console.log(realUser.name); // 正常输出 '张三'
其他常见坑点:
- 容量限制:虽然单个域名有 5MB 左右,但超出后会报错,建议在存储大量数据时做异常处理。
- 隐私模式:部分浏览器在隐私模式下可能无法写入 localStorage,或者容量被限制,使用前最好做一下可用性检测。
- 同步操作 :
setItem/getItem是同步的,大量数据读写可能会阻塞主线程,复杂场景建议配合 IndexedDB 使用。
二、Cookies(饼干)
1. 概念
Cookie(饼干)是服务器发送给浏览器并保存在本地的一小块数据(通常不超过 4KB)。你可以把它理解为"服务器发给你的一张专属身份凭证(或记事本)"。
它和 Web Storage 最大的区别在于:只要是你向该服务器发送网络请求,浏览器都会自动把 Cookie 带上,服务器一看到就知道你是谁、你之前做过什么。
2. 特征
- 自动携带:每次发起 HTTP 请求时,浏览器会自动在请求头(Request Header)中带上相关的 Cookie,无需手动添加。
- 容量极小:单个 Cookie 的大小通常限制在 4KB 左右,不适合存大量数据。
- 生命周期可控 :
- 如果不设置过期时间,关闭浏览器后它就会自动消失(会话级 Cookie)。
- 如果设置了过期时间,它就会一直存在直到过期(持久化 Cookie)。
- 安全性配置 :支持
HttpOnly(防止前端 JS 读取,防 XSS 攻击)、Secure(只允许 HTTPS 传输)、SameSite(防止跨站请求伪造 CSRF)等安全属性。
3. 使用场景
| 使用场景 | 说明 | 典型示例 |
|---|---|---|
| 用户登录状态(会话管理) | 最核心的用途。用户登录后,服务器发一个 Token 存在 Cookie 里,后续请求自动携带,服务器便能识别用户身份。 | 登录后访问个人中心、订单页等需要鉴权的页面 |
| 购物车记录 | 未登录状态下往购物车加商品,服务器用 Cookie 记住这些商品,登录后再合并到账号下。 | 电商网站的未登录购物车 |
| 个性化设置 | 记住用户的语言、主题等偏好,服务器在返回页面时就按偏好渲染。 | 中英文切换、网站主题颜色 |
| 行为追踪 | 广告商或分析工具记录用户的浏览轨迹,用于推送精准广告或统计分析。 | 百度统计、Google Analytics |
4. 使用方法
💡 场景一:原生 JavaScript 操作 Cookie
原生操作就是通过 document.cookie 这个属性。
javascript
// 1. 创建/修改 Cookie
// 格式:"名字=值; expires=过期时间; path=生效路径"
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + 7); // 7天后过期
document.cookie = "username=张三; expires=" + expireDate.toUTCString() + "; path=/";
// 2. 读取 Cookie
// 注意:document.cookie 会把当前域名下所有的 Cookie 拼成一个长字符串返回
console.log(document.cookie); // 输出: "username=张三"
// 3. 删除 Cookie
// 原理:把过期时间设置为过去的时间,浏览器就会自动删掉它
document.cookie = "username=张三; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
注意 :原生读取时需要自己写正则或
split去截取想要的值,非常麻烦,所以实际开发中极少这么干。
💡 场景二:实际开发中的标准姿势(使用 js-cookie 库)
在实际企业级项目中,我们通常会安装一个轻量级库 js-cookie,它把复杂的原生 API 封装得极其简单:
bash
npm install js-cookie
javascript
import Cookies from 'js-cookie';
// 1. 设置 Cookie(超级简单,支持直接传对象,支持设置天数)
Cookies.set('userToken', 'abc123456', { expires: 7, secure: true });
// 2. 读取 Cookie
const token = Cookies.get('userToken'); // 直接拿到 'abc123456'
// 3. 删除 Cookie
Cookies.remove('userToken');
5. Cookies 配置
核心基础配置(决定门票的归属)
- Name(名称)和 Value(值) :最基础的键值对,比如
token=abc123。 - Domain(域名) :规定这张门票在哪个游乐园生效。比如设置为
.example.com,那么www.example.com和api.example.com都能使用这张门票(支持子域名共享)。如果不设置,默认就是当前访问的完整域名。 - Path(路径) :规定门票在游乐园的哪些区域有效。比如设置为
/admin,那么只有访问/admin/xxx时才会带上这个 Cookie;如果是/,则全站有效。 - Expires / Max-Age(过期时间) :门票的有效期。
- 如果设置了具体的过期时间(比如 7 天后),浏览器会把它保存在硬盘上,即使关闭浏览器再打开,门票依然有效(持久化 Cookie)。
- 如果不设置这个属性,浏览器一关闭,门票就自动销毁(会话 Cookie)。
安全配置(防止门票被偷或伪造)
在实际开发中,这部分极其重要!
- HttpOnly :设置为
true后,这张门票就禁止前端 JavaScript 读取(即document.cookie拿不到它)。这能极大防止黑客通过 XSS 攻击偷走用户的登录凭证。它只允许浏览器在发请求时默默带上。 - Secure :设置为
true后,这张门票只允许在 HTTPS 加密通道下传输。如果用户不小心访问了 HTTP 的链接,浏览器绝不会把这个 Cookie 发出去,防止在公共 WiFi 下被窃听。 - SameSite(同站限制):用来防止 CSRF(跨站请求伪造)攻击。它有三个值:
| 取值 | 说明 |
|---|---|
| Strict | 最严格,完全不允许跨站携带。 |
| Lax | 默认推荐值,允许部分安全的跨站请求(比如点击链接跳转)携带。 |
| None | 允许跨站携带,但前提是必须同时设置 Secure: true。 |
现代进阶配置(应对复杂的追踪场景)
- PartitionKey(分区键):这是比较新的特性。为了防止第三方 Cookie 跨站追踪用户隐私,浏览器引入了分区存储。带有这个配置的 Cookie,只有在特定的顶级网站下才会生效,换个顶级网站就失效了,从而保护用户隐私。
6. 使用指南
- 别把 Cookie 当大仓库 :超过 4KB 的数据,或者不需要发给服务器的数据(如主题颜色、草稿),请果断使用 localStorage。
- 安全第一 :千万不要在 Cookie 里明文存储用户的密码!如果是存登录凭证(Token),一定要让后端加上 HttpOnly 和 Secure 属性,防止黑客通过脚本偷走你的 Cookie
三、Cookies、localStorage、sessionStorage 大横比
为了让你更直观地理解三者的差异,我将它们的关键特性整理成了一张对比表:
| 特性维度 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 存储容量 | 极小(约 4KB) | 较大(约 5MB) | 较大(约 5MB) |
| 生命周期 | 可设置过期时间,未设置则浏览器关闭失效 | 永久存储,除非手动清除 | 仅在当前标签页有效,关闭即销毁 |
| 是否随请求自动发送 | ✅ 是(每次请求都会携带) | ❌ 否(仅存本地) | ❌ 否(仅存本地) |
| 作用域(访问范围) | 同源窗口共享,可通过 Domain/Path 限制 | 同源的所有标签页共享 | 仅限当前标签页(同源也不共享) |
| 安全性 | 中高(可设置 HttpOnly 防 XSS,SameSite 防 CSRF) | 低(极易受 XSS 攻击窃取) | 低(极易受 XSS 攻击窃取) |
| API 易用性 | 较差(原生需自行拼接字符串) | 极佳(简单的键值对 API) | 极佳(同 localStorage) |
| 核心定位 | 与服务器通信的载体 | 本地持久化存储仓库 | 单次会话的临时草稿纸 |
简单总结:Cookie 是为了"沟通"而生,localStorage 是为了"记住"而生,sessionStorage 是为了"临时办事"而生。
四、Token 到底应该存在哪里?
在实际项目中,登录成功后拿到的 Token(如 JWT)该放在哪里,直接关系到应用的安全性和开发体验。下面详细分析三种主流方案。
1. 存在 Cookie 中(推荐:HttpOnly + Secure + SameSite)
这是安全性最高的方案。
- 优点 :通过设置
HttpOnly,前端 JavaScript 无法读取 Token,这从根本上杜绝了 XSS(跨站脚本)攻击窃取 Token 的风险。同时,浏览器会自动携带,无需前端手动处理。 - 缺点 :由于浏览器会自动带上 Cookie,它容易遭受 CSRF(跨站请求伪造)攻击,需要后端配合
SameSite属性或 CSRF Token 进行防御。此外,前后端分离的跨域场景配置相对复杂。 - 适用场景:对安全性要求极高的系统(如金融、银行网站),以及传统的同域前后端应用。
核心原则 :前端只负责"触发",后端负责"安全交付与验证"。这种方案不能由前端直接用 JavaScript 设置(因为 JS 无法设置 HttpOnly 属性),而是需要前后端紧密配合。以下是完整的落地实现方案:
第一步:后端安全交付 Token(核心)
当用户登录成功或 OAuth 授权回调完成后,后端绝不能把 Token 放在 JSON 响应体里返回给前端,而是应该通过 HTTP 响应头(Set-Cookie)将 Token 写入 Cookie,并加上"安全三连"配置。
Node.js (Express) 示例:
javascript
// 登录成功后,后端签发 Token 并写入 Cookie
res.cookie('auth_token', token, {
httpOnly: true, // 核心:禁止前端 JS 读取,防 XSS
secure: true, // 核心:强制仅在 HTTPS 下传输
sameSite: 'Strict', // 核心:限制跨站请求,防 CSRF
maxAge: 3600000, // 设置过期时间(如 1 小时)
path: '/' // 全站有效
});
// 返回成功状态,但不包含 Token 明文
res.json({ code: 200, message: '登录成功' });
第二步:前端发起请求(无需手动带 Token)
因为 Token 存在 HttpOnly Cookie 中,浏览器在发起同域请求时会自动携带这个 Cookie,前端完全不需要(也无法)手动把它加到请求头里。
前端 Axios 请求示例:
javascript
// 发起请求时,只需确保携带凭证
fetch('/api/user/info', {
credentials: 'include' // 关键:允许跨域时携带 Cookie
});
第三步:防御 CSRF 攻击(双重保险)
虽然 SameSite=Strict 已经能防住绝大多数 CSRF 攻击,但在高安全场景下,通常还会叠加 CSRF Token 机制:
- 后端下发 :在登录时,后端额外生成一个 CSRF Token,放在普通 Cookie(
httpOnly: false)中返回,让前端 JS 可以读取。 - 前端携带 :前端在发起修改数据的请求(POST/PUT/DELETE)时,从普通 Cookie 中读出 CSRF Token,并放入请求头(如
X-CSRF-Token)中。 - 后端校验 :后端同时校验 Cookie 中的
auth_token和请求头中的X-CSRF-Token是否匹配。由于恶意网站无法读取你域名下的普通 Cookie,也就无法伪造这个请求头。
💡 跨域场景的特殊处理
如果你的前端(如 www.example.com)和后端(如 api.example.com)不在同一个域名下:
- 后端设置 Cookie 时,必须将
sameSite设置为'None',并且必须配合secure: true。 - 后端必须配置 CORS 响应头,允许前端域名访问,并设置
Access-Control-Allow-Credentials: true。 - 前端请求时,必须加上
credentials: 'include'。
2. 存在 localStorage 中(目前最主流)
这是目前前后端分离架构下最常见的方案。
- 优点:跨域非常灵活,不受 Cookie 的 Domain 限制,天然免疫 CSRF 攻击。前端管理 Token(如刷新、清除)非常方便。
- 缺点 :安全性较弱。只要网站存在 XSS 漏洞,恶意脚本就可以直接通过
localStorage.getItem('token')把 Token 偷走。 - 适用场景 :绝大多数前后端分离项目、中后台管理系统、ToC 产品等。前提是必须做好严格的 XSS 防护(如配置 CSP 策略、对用户输入进行转义)。
3. 存在 sessionStorage 中
- 特点:生命周期极短,关闭标签页就消失。
- 适用场景:仅适用于"阅后即焚"或临时会话场景。例如,用户在一个新标签页中执行一次敏感操作(如修改密码),验证通过后生成一个临时 Token,操作完成或关闭页面后立即失效,防止 Token 被长期留存带来的风险。
五、IndexedDB(索引数据库)
1. 概念
IndexedDB 是一种嵌入在浏览器环境中的客户端 NoSQL 数据库。它允许网页在用户的本地存储和操作大量结构化数据。你可以把它理解为浏览器里的一个"本地微型数据库",支持事务、索引查询,并且操作是异步的,绝对不会让你的网页卡顿。
与 Web Storage 不同,IndexedDB 不是简单的键值对存储,而是一个完整的、具备事务机制的数据库,它能够处理复杂的查询和海量的数据,是构建现代 Web 应用的重要基石。
2. 特征
- 大容量:它的存储空间通常不少于 250MB,甚至没有明确上限,远超 Cookie 和 LocalStorage。这使得它非常适合存储离线应用所需的大量数据。
- 异步操作:所有的增删改查都是异步的,不会阻塞主线程,非常适合处理海量数据。即使你在读写几万条数据,页面依然可以流畅滚动和交互。
- 支持事务:它支持只读、读写等事务模式。如果一组操作中有一个失败了,所有的操作都会回滚,保证数据的一致性。就像关系型数据库一样,事务是 IndexedDB 保证数据完整性的核心机制。
- 支持索引:这是它叫"Indexed"的原因。你可以为数据中的某个属性建立索引,从而实现快速查询(就像数据库里的索引一样)。比如,你可以为"创建时间"建索引,快速按时间排序查询。
- 存储类型丰富:不仅能存字符串,还能直接存储复杂的 JavaScript 对象,甚至是二进制数据(如图片、文件 Blob)。这意味着你可以把图片、视频、文档等直接存到本地数据库中。
3. 使用场景
IndexedDB 并不是用来替代 LocalStorage 的,它专治"大容量、复杂结构"的痛点:
| 使用场景 | 说明 | 典型示例 |
|---|---|---|
| 离线应用(PWA) | 在断网时,网页可以从 IndexedDB 中读取之前缓存的数据,让你依然能查看和编辑。 | 在线文档、离线地图、邮件客户端 |
| 海量数据缓存 | 把电商网站的超长商品列表、后台管理系统的字典数据存在本地,能极大减少网络请求,提升页面秒开速度。 | 商品列表缓存、字典数据本地化 |
| 本地业务数据 | 前端表单的复杂草稿、本地编辑的视频/图片、不需要实时同步到后端的购物车数据。 | 富文本编辑器草稿、图片编辑历史 |
4. 怎么用(举例)
⚠️ 新手高能预警 :IndexedDB 的原生 API 非常繁琐,充满了各种事件回调(onsuccess、onupgradeneeded 等),直接写原生代码极其痛苦。
强烈建议在实际开发中使用成熟的第三方封装库。推荐使用目前最流行、最优雅的 Promise 封装库 idb。
第一步:安装库
bash
npm install idb
第二步:创建/打开数据库并定义表结构
javascript
import { openDB } from 'idb';
async function initDB() {
const db = await openDB('MyNotesDB', 1, {
upgrade(db) {
// 如果 'notes' 这个表(对象存储)不存在,就创建它
if (!db.objectStoreNames.contains('notes')) {
// 创建一个表,主键 id 会自动递增
db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
}
},
});
return db;
}
第三步:增删改查(CRUD)
javascript
async function manageNotes() {
const db = await initDB();
// 1. 增加数据(读写事务)
await db.put('notes', { title: '学习 IndexedDB', content: '今天搞懂了本地数据库!' });
// 2. 查询所有数据(只读事务)
const allNotes = await db.getAll('notes');
console.log('所有的备忘录:', allNotes);
// 3. 按主键查询单条数据
const note = await db.get('notes', 1); // 获取 id 为 1 的数据
console.log('第一条备忘录:', note);
// 4. 删除数据
await db.delete('notes', 1); // 删除 id 为 1 的数据
}
manageNotes();
提示 :
idb库将 IndexedDB 原生的回调式 API 封装成了 Promise 风格,配合async/await使用,代码简洁明了,强烈推荐用它替代原生 API。