Storage API 详解
localStorage 和 sessionStorage 都实现了统一的 Storage 接口。本节将深入讲解 Storage 接口的所有方法和属性,包括 StorageEvent 跨标签页通信、key() 遍历、JSON 序列化最佳实践以及存储操作的异常处理。
前置知识
阅读本节前,建议先了解:sessionStorage
基础概念
Storage 接口定义
Storage 接口定义了 Web Storage 的标准 API,localStorage 和 sessionStorage 都是 Storage 接口的实例。
| 属性/方法 | 返回值 | 说明 |
|---|---|---|
length | number | 存储的键值对数量 |
key(index) | `string | null` |
getItem(key) | `string | null` |
setItem(key, value) | void | 存储键值对 |
removeItem(key) | void | 删除指定键 |
clear() | void | 清空所有数据 |
语法与 API
length 属性
javascript
// 获取存储的键值对数量
const count = localStorage.length;
console.log(`存储了 ${count} 条数据`);
// 监测存储变化
function checkStorageEmpty(storage) {
if (storage.length === 0) {
console.log('存储为空');
return true;
}
return false;
}key(index) 方法
javascript
// key() 返回指定索引位置的键名
const firstKey = localStorage.key(0); // 第一个键
const secondKey = localStorage.key(1); // 第二个键
const lastKey = localStorage.key(localStorage.length - 1); // 最后一个键
// 索引超出范围返回 null
const invalid = localStorage.key(999); // null
// 注意:键的顺序由浏览器决定,不保证插入顺序完整 CRUD 操作
javascript
// ========== Create - 创建数据 ==========
localStorage.setItem('config:theme', 'dark');
localStorage.setItem('config:lang', 'zh-CN');
localStorage.setItem('config:fontSize', '16');
// ========== Read - 读取数据 ==========
const theme = localStorage.getItem('config:theme'); // 'dark'
const allLang = localStorage.getItem('config:lang'); // 'zh-CN'
const notExist = localStorage.getItem('config:color'); // null
// ========== Update - 更新数据 ==========
// setItem 对已存在的键会覆盖
localStorage.setItem('config:theme', 'light');
localStorage.getItem('config:theme'); // 'light'(已更新)
// ========== Delete - 删除数据 ==========
localStorage.removeItem('config:fontSize');
localStorage.getItem('config:fontSize'); // null(已删除)
// 清空所有
localStorage.clear();详细说明
StorageEvent - 跨标签页通信
StorageEvent 是 Web Storage 最重要的特性之一,当同源的另一个标签页(或 iframe)修改了 localStorage 时,会触发该事件。这提供了一种轻量级的跨标签页通信方式。
javascript
// 监听 storage 事件
window.addEventListener('storage', (e) => {
console.log('存储变化事件:', {
key: e.key, // 被修改的键名
oldValue: e.oldValue, // 修改前的值
newValue: e.newValue, // 修改后的值
url: e.url, // 触发事件的页面 URL
storageArea: e.storageArea // localStorage 或 sessionStorage
});
// 根据键名执行不同操作
switch (e.key) {
case 'user:logout':
handleLogout();
break;
case 'theme:changed':
document.body.className = e.newValue;
break;
case 'notification:new':
showNotification(e.newValue);
break;
}
});StorageEvent 属性
| 属性 | 类型 | 说明 |
|---|---|---|
key | `string | null` |
oldValue | `string | null` |
newValue | `string | null` |
url | string | 触发变化的页面 URL |
storageArea | Storage | 被修改的 Storage 对象 |
StorageEvent 的重要特性
javascript
// 1. 不会在当前页面触发(只触发在其他标签页)
// 在标签页 A 中
localStorage.setItem('test', '1');
// 标签页 A 的 storage 事件监听器不会触发
// 在标签页 B 中(同源)
// storage 事件监听器会触发
// 2. sessionStorage 不触发 storage 事件
// sessionStorage 的变化不会触发 storage 事件
// 只有 localStorage 会
// 3. 只在同源页面间触发
// 不同域名、不同协议、不同端口不会触发使用 StorageEvent 实现标签页同步
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>StorageEvent 标签页同步</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
.panel {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.event-log {
padding: 16px;
background: #1a1a2e;
color: #0f0;
font-family: monospace;
font-size: 13px;
border-radius: 8px;
max-height: 300px;
overflow-y: auto;
}
.controls { display: flex; gap: 12px; margin-top: 16px; }
.controls input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; flex: 1; }
.controls button {
padding: 8px 20px;
border: none;
border-radius: 6px;
background: #1a73e8;
color: white;
cursor: pointer;
}
.badge {
display: inline-block;
padding: 2px 10px;
background: #e3f2fd;
color: #1565c0;
border-radius: 12px;
font-size: 13px;
}
</style>
</head>
<body>
<h2>StorageEvent 跨标签页通信</h2>
<p><span class="badge">请打开多个标签页查看效果</span></p>
<div class="panel">
<h3>操作面板</h3>
<div class="controls">
<input type="text" id="inputKey" placeholder="键名" value="message">
<input type="text" id="inputValue" placeholder="值">
<button onclick="sendData()">发送</button>
</div>
<div class="controls" style="margin-top:8px;">
<button onclick="broadcastNotification()" style="flex:1;background:#e91e63;">发送通知到所有标签页</button>
</div>
</div>
<div class="panel">
<h3>事件日志</h3>
<div class="event-log" id="eventLog">
等待来自其他标签页的 StorageEvent...
</div>
</div>
<script>
// 发送数据
function sendData() {
const key = document.getElementById('inputKey').value;
const value = document.getElementById('inputValue').value;
if (key && value) {
localStorage.setItem(key, value);
addLog('本地发送', `${key} = ${value}`);
}
}
// 广播通知
function broadcastNotification() {
const message = `通知来自标签页 ${Date.now()}`;
localStorage.setItem('broadcast:notification', message);
addLog('广播', message);
}
// 监听 storage 事件
window.addEventListener('storage', (e) => {
addLog('StorageEvent', `
key: ${e.key}
oldValue: ${e.oldValue}
newValue: ${e.newValue}
url: ${e.url}
`);
});
// 添加日志
function addLog(type, content) {
const log = document.getElementById('eventLog');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<p><span style="color:#ff0">[${time}]</span> <strong>${type}:</strong> ${content}</p>`;
log.scrollTop = log.scrollHeight;
}
</script>
</body>
</html>遍历 Storage 数据
javascript
// 方式一:使用 key() 和 length 遍历
function iterateStorage(storage) {
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
console.log(`${key}: ${value}`);
}
}
// 方式二:使用 Object.keys()(不推荐,包含原型链属性)
// Object.keys(localStorage).forEach(key => { ... });
// 方式三:推荐使用 for...of + entries()
function iterateStorageModern(storage) {
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
yield { key, value };
}
}
// 使用示例
for (const item of iterateStorageModern(localStorage)) {
console.log(item.key, '=', item.value);
}
// 方式四:按前缀筛选
function getByPrefix(storage, prefix) {
const result = {};
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key.startsWith(prefix)) {
result[key] = storage.getItem(key);
}
}
return result;
}
// 获取所有以 'config:' 开头的配置
const configs = getByPrefix(localStorage, 'config:');
console.log(configs);JSON 序列化与反序列化
javascript
// 安全的 JSON 存储工具
const StorageHelper = {
// 存储 JSON
setJSON(storage, key, data) {
try {
storage.setItem(key, JSON.stringify(data));
return true;
} catch (e) {
console.error('存储 JSON 失败:', e);
return false;
}
},
// 读取 JSON
getJSON(storage, key, defaultValue = null) {
try {
const raw = storage.getItem(key);
return raw !== null ? JSON.parse(raw) : defaultValue;
} catch (e) {
console.error('解析 JSON 失败:', e);
return defaultValue;
}
},
// 删除并返回 JSON
popJSON(storage, key, defaultValue = null) {
const data = this.getJSON(storage, key, defaultValue);
storage.removeItem(key);
return data;
},
// 更新嵌套对象的某个属性
updateNested(storage, key, path, value) {
const data = this.getJSON(storage, key, {});
const keys = path.split('.');
let target = data;
for (let i = 0; i < keys.length - 1; i++) {
if (!target[keys[i]]) target[keys[i]] = {};
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
return this.setJSON(storage, key, data);
}
};
// 使用示例
StorageHelper.setJSON(localStorage, 'user', { name: '张三', settings: { theme: 'dark' } });
StorageHelper.updateNested(localStorage, 'user', 'settings.theme', 'light');
const user = StorageHelper.getJSON(localStorage, 'user');
console.log(user.settings.theme); // 'light'实战示例
Storage 封装工具类
javascript
/**
* Web Storage 封装工具类
* 支持命名空间、过期时间、类型转换
*/
class WebStorage {
constructor(storage, namespace = '') {
this.storage = storage;
this.namespace = namespace;
}
// 生成带命名空间的键
_key(key) {
return this.namespace ? `${this.namespace}:${key}` : key;
}
// 存储数据
set(key, value, ttl) {
const data = {
value: value,
type: typeof value
};
// 设置过期时间
if (ttl) {
data.expiry = Date.now() + ttl * 1000;
}
try {
this.storage.setItem(this._key(key), JSON.stringify(data));
return true;
} catch (e) {
console.error('存储失败:', e);
return false;
}
}
// 读取数据
get(key, defaultValue = null) {
try {
const raw = this.storage.getItem(this._key(key));
if (raw === null) return defaultValue;
const data = JSON.parse(raw);
// 检查过期
if (data.expiry && Date.now() > data.expiry) {
this.remove(key);
return defaultValue;
}
return data.value;
} catch (e) {
console.error('读取失败:', e);
return defaultValue;
}
}
// 删除数据
remove(key) {
this.storage.removeItem(this._key(key));
}
// 清空命名空间下的数据
clear() {
const prefix = this._key('');
const keysToRemove = [];
for (let i = 0; i < this.storage.length; i++) {
if (this.storage.key(i).startsWith(prefix)) {
keysToRemove.push(this.storage.key(i));
}
}
keysToRemove.forEach(key => this.storage.removeItem(key));
}
// 判断键是否存在
has(key) {
return this.storage.getItem(this._key(key)) !== null;
}
// 获取命名空间下的所有数据
getAll() {
const result = {};
const prefix = this._key('');
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i);
if (key.startsWith(prefix)) {
const shortKey = key.slice(prefix.length);
result[shortKey] = this.get(shortKey);
}
}
return result;
}
}
// 使用示例
const appStorage = new WebStorage(localStorage, 'myapp');
// 存储数据(带 1 小时过期时间)
appStorage.set('user', { name: '张三' }, 3600);
appStorage.set('token', 'abc123', 7200);
// 读取数据
const user = appStorage.get('user');
const token = appStorage.get('token', 'default-token');
// 获取命名空间下所有数据
const allData = appStorage.getAll();
// 清空命名空间
appStorage.clear();注意事项
异常处理
javascript
// 1. 存储空间满
try {
localStorage.setItem('big-data', largeData);
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('存储空间已满');
// 清理策略:删除最旧的数据
}
}
// 2. 浏览器禁用存储
function isStorageAvailable(storage) {
try {
const test = '__test__';
storage.setItem(test, '1');
storage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
// 3. JSON 解析失败
function safeParse(raw) {
try {
return JSON.parse(raw);
} catch {
return null;
}
}最佳实践
- 使用命名空间避免键名冲突
- 为缓存数据添加过期时间
- 封装统一的 Storage 工具类
- 利用
StorageEvent实现标签页间同步 - 对所有操作进行异常捕获
下一节
继续学习:IndexedDB 入门