四、浏览器存储

一、Web Storage(Web 存储)

1. 概念

Web Storage(Web 存储)是 HTML5 提供的一种在浏览器本地存储数据的机制。你可以把它理解为浏览器提供的一个"本地小仓库"。

它最大的特点是:数据只保存在用户的浏览器里,不会像 Cookie 那样在每次请求时都发给服务器,所以它更安全,也不会浪费网络带宽。

2. 特征

Web Storage 主要分为 localStoragesessionStorage,它们有以下几个共同和不同的特征:

  • 同源性限制 :只有在同一个域名下才能访问。比如 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.comapi.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),一定要让后端加上 HttpOnlySecure 属性,防止黑客通过脚本偷走你的 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)该放在哪里,直接关系到应用的安全性和开发体验。下面详细分析三种主流方案。

这是安全性最高的方案。

  • 优点 :通过设置 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 非常繁琐,充满了各种事件回调(onsuccessonupgradeneeded 等),直接写原生代码极其痛苦。

强烈建议在实际开发中使用成熟的第三方封装库。推荐使用目前最流行、最优雅的 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。

相关推荐
breeze jiang1 小时前
CSS 三栏布局怎么写:Flex、Grid 完整方案与 BFC 区别
前端·css
郭邯1 小时前
手写一个Cron表达式解析器:从需求分析到AI辅助实现
前端
阿黎梨梨1 小时前
Docker 容器化实战:从零搭建 Web 服务与反向代理
前端·后端·docker
Liora_Yvonne1 小时前
前端项目为什么需要一个配置单一真相源?
前端
禁止摆烂_才浅1 小时前
Vite 面试题
前端·面试·vite
GISer_Jing1 小时前
全栈AI实战:基于 TypeScript + LangChain + MCP 的企业级智能研发助手
前端·后端·ai·langchain·前端框架
禁止摆烂_才浅1 小时前
微信小程序高频面试题
前端·面试·微信小程序
禁止摆烂_才浅1 小时前
Vue2 高频面试题
前端·vue.js·面试
禁止摆烂_才浅1 小时前
前端性能优化面试题
前端·面试·性能优化