现代Web存储技术(二):存储容量规划与传统方案对比
除了三大主力存储技术,浏览器还有一些传统存储方式。虽然它们有各自的局限性,但在特定场景下仍然有用武之地。本文将详细介绍这些传统存储方式,以及如何管理浏览器存储容量。
1 传统存储方式:能用但有坑
除了Cache API、IndexedDB和OPFS这三大主力,浏览器还有一些老牌存储方式。它们不是不能用,但都有各自的问题。
1.1 LocalStorage:简单但性能差
什么时候还在用?
• 存个主题设置(深色/浅色模式)
• 记住用户的语言偏好
• 保存简单的表单数据
问题在哪?
• 会卡页面:读写数据时整个页面都得等着
• 容量太小:只有5MB,存不了什么大东西
• 只能存文本:图片、文件什么的都存不了
实际体验
你有没有遇到过网页突然卡住几秒?很可能就是某个网站在用LocalStorage存大量数据。
使用示例
// 主题设置管理
class ThemeManager {
constructor() {
this.THEME_KEY = 'app-theme';
this.LANGUAGE_KEY = 'app-language';
}
// 设置主题
setTheme(theme) {
try {
localStorage.setItem(this.THEME_KEY, theme);
document.body.className = `theme-${theme}`;
console.log(`主题已切换为: ${theme}`);
} catch (error) {
console.error('设置主题失败:', error);
// 可能是存储空间不足或隐私模式
this.handleStorageError(error);
}
}
// 获取主题
getTheme() {
try {
return localStorage.getItem(this.THEME_KEY) || 'light';
} catch (error) {
console.error('获取主题失败:', error);
return 'light'; // 默认主题
}
}
// 设置语言
setLanguage(language) {
try {
localStorage.setItem(this.LANGUAGE_KEY, language);
// 触发语言切换事件
window.dispatchEvent(new CustomEvent('languageChange', {
detail: { language }
}));
} catch (error) {
console.error('设置语言失败:', error);
}
}
// 获取语言
getLanguage() {
try {
return localStorage.getItem(this.LANGUAGE_KEY) || 'zh-CN';
} catch (error) {
console.error('获取语言失败:', error);
return 'zh-CN';
}
}
// 处理存储错误
handleStorageError(error) {
if (error.name === 'QuotaExceededError') {
alert('存储空间不足,请清理浏览器数据');
} else if (error.name === 'SecurityError') {
console.warn('隐私模式下无法使用LocalStorage');
}
}
// 清理所有设置
clearSettings() {
try {
localStorage.removeItem(this.THEME_KEY);
localStorage.removeItem(this.LANGUAGE_KEY);
console.log('设置已清理');
} catch (error) {
console.error('清理设置失败:', error);
}
}
}
// 表单数据自动保存
class FormAutoSave {
constructor(formId, saveKey) {
this.form = document.getElementById(formId);
this.saveKey = saveKey;
this.debounceTimer = null;
this.init();
}
init() {
if (!this.form) return;
// 页面加载时恢复数据
this.restoreFormData();
// 监听表单变化
this.form.addEventListener('input', (e) => {
this.debounceAutoSave();
});
// 表单提交时清理保存的数据
this.form.addEventListener('submit', () => {
this.clearSavedData();
});
}
// 防抖自动保存
debounceAutoSave() {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.saveFormData();
}, 1000); // 1秒后保存
}
// 保存表单数据
saveFormData() {
try {
const formData = new FormData(this.form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
localStorage.setItem(this.saveKey, JSON.stringify(data));
console.log('表单数据已自动保存');
} catch (error) {
console.error('保存表单数据失败:', error);
}
}
// 恢复表单数据
restoreFormData() {
try {
const savedData = localStorage.getItem(this.saveKey);
if (!savedData) return;
const data = JSON.parse(savedData);
for (let [key, value] of Object.entries(data)) {
const input = this.form.querySelector(`[name="${key}"]`);
if (input) {
input.value = value;
}
}
console.log('表单数据已恢复');
} catch (error) {
console.error('恢复表单数据失败:', error);
}
}
// 清理保存的数据
clearSavedData() {
try {
localStorage.removeItem(this.saveKey);
console.log('已清理保存的表单数据');
} catch (error) {
console.error('清理数据失败:', error);
}
}
}
// 使用示例
const themeManager = new ThemeManager();
const formAutoSave = new FormAutoSave('contact-form', 'contact-form-data');
// 初始化主题
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = themeManager.getTheme();
themeManager.setTheme(savedTheme);
});
1.2 SessionStorage:用完就扔
适合存什么?
• 表单填到一半的内容(防止误关页面)
• 当前页面的临时状态
• 购物车里的商品(关闭页面就清空)
特点
• 关闭标签页就没了,很适合临时数据
• 同样会卡页面,同样只有5MB
使用示例
// 页面状态管理
class PageStateManager {
constructor() {
this.STATE_KEY = 'page-state';
this.init();
}
init() {
// 页面加载时恢复状态
window.addEventListener('load', () => {
this.restorePageState();
});
// 页面卸载时保存状态
window.addEventListener('beforeunload', () => {
this.savePageState();
});
}
// 保存页面状态
savePageState() {
try {
const state = {
scrollPosition: window.scrollY,
timestamp: Date.now(),
activeTab: document.querySelector('.tab.active')?.dataset.tab,
searchQuery: document.querySelector('#search-input')?.value,
filters: this.getActiveFilters()
};
sessionStorage.setItem(this.STATE_KEY, JSON.stringify(state));
console.log('页面状态已保存');
} catch (error) {
console.error('保存页面状态失败:', error);
}
}
// 恢复页面状态
restorePageState() {
try {
const savedState = sessionStorage.getItem(this.STATE_KEY);
if (!savedState) return;
const state = JSON.parse(savedState);
// 恢复滚动位置
if (state.scrollPosition) {
window.scrollTo(0, state.scrollPosition);
}
// 恢复活动标签
if (state.activeTab) {
const tab = document.querySelector(`[data-tab="${state.activeTab}"]`);
if (tab) {
tab.click();
}
}
// 恢复搜索查询
if (state.searchQuery) {
const searchInput = document.querySelector('#search-input');
if (searchInput) {
searchInput.value = state.searchQuery;
}
}
// 恢复筛选器
if (state.filters) {
this.restoreFilters(state.filters);
}
console.log('页面状态已恢复');
} catch (error) {
console.error('恢复页面状态失败:', error);
}
}
// 获取当前激活的筛选器
getActiveFilters() {
const filters = {};
document.querySelectorAll('.filter-checkbox:checked').forEach(checkbox => {
filters[checkbox.name] = checkbox.value;
});
return filters;
}
// 恢复筛选器状态
restoreFilters(filters) {
for (let [name, value] of Object.entries(filters)) {
const checkbox = document.querySelector(`input[name="${name}"][value="${value}"]`);
if (checkbox) {
checkbox.checked = true;
}
}
}
}
// 临时购物车管理
class TempShoppingCart {
constructor() {
this.CART_KEY = 'temp-shopping-cart';
}
// 添加商品到购物车
addItem(product) {
try {
const cart = this.getCart();
const existingItem = cart.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
cart.push({ ...product, quantity: 1 });
}
sessionStorage.setItem(this.CART_KEY, JSON.stringify(cart));
this.updateCartUI();
console.log('商品已添加到购物车');
} catch (error) {
console.error('添加商品失败:', error);
}
}
// 获取购物车内容
getCart() {
try {
const cart = sessionStorage.getItem(this.CART_KEY);
return cart ? JSON.parse(cart) : [];
} catch (error) {
console.error('获取购物车失败:', error);
return [];
}
}
// 移除商品
removeItem(productId) {
try {
const cart = this.getCart();
const updatedCart = cart.filter(item => item.id !== productId);
sessionStorage.setItem(this.CART_KEY, JSON.stringify(updatedCart));
this.updateCartUI();
console.log('商品已移除');
} catch (error) {
console.error('移除商品失败:', error);
}
}
// 清空购物车
clearCart() {
try {
sessionStorage.removeItem(this.CART_KEY);
this.updateCartUI();
console.log('购物车已清空');
} catch (error) {
console.error('清空购物车失败:', error);
}
}
// 更新购物车UI
updateCartUI() {
const cart = this.getCart();
const cartCount = cart.reduce((total, item) => total + item.quantity, 0);
const cartBadge = document.querySelector('.cart-badge');
if (cartBadge) {
cartBadge.textContent = cartCount;
cartBadge.style.display = cartCount > 0 ? 'block' : 'none';
}
}
}
// 使用示例
const pageStateManager = new PageStateManager();
const tempCart = new TempShoppingCart();
1.3 Cookies:古老但必需
现在主要用来干嘛?
• 存登录状态(Session ID)
• 记住"下次自动登录"
• 广告追踪(虽然大家都讨厌)
为什么不用它存其他东西?
• 太小了:每个Cookie最多4KB
• 拖慢网速:每次请求都会把所有Cookie发给服务器
• 不安全:容易被脚本读取(除非设置HttpOnly)
真实案例
某些网站Cookie太多,光是发送Cookie就要几KB,拖慢了整个网站的加载速度。
使用示例
// Cookie管理工具类
class CookieManager {
// 设置Cookie
static setCookie(name, value, options = {}) {
const {
expires = null,
maxAge = null,
path = '/',
domain = null,
secure = false,
sameSite = 'Lax'
} = options;
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (expires) {
cookieString += `; expires=${expires.toUTCString()}`;
}
if (maxAge) {
cookieString += `; max-age=${maxAge}`;
}
cookieString += `; path=${path}`;
if (domain) {
cookieString += `; domain=${domain}`;
}
if (secure) {
cookieString += '; secure';
}
cookieString += `; samesite=${sameSite}`;
document.cookie = cookieString;
console.log(`Cookie已设置: ${name}`);
}
// 获取Cookie
static getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (decodeURIComponent(cookieName) === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}
// 删除Cookie
static deleteCookie(name, path = '/', domain = null) {
let cookieString = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
if (domain) {
cookieString += `; domain=${domain}`;
}
document.cookie = cookieString;
console.log(`Cookie已删除: ${name}`);
}
// 获取所有Cookie
static getAllCookies() {
const cookies = {};
const cookieArray = document.cookie.split(';');
for (let cookie of cookieArray) {
const [name, value] = cookie.trim().split('=');
if (name && value) {
cookies[decodeURIComponent(name)] = decodeURIComponent(value);
}
}
return cookies;
}
}
// 用户认证管理
class AuthManager {
constructor() {
this.TOKEN_KEY = 'auth_token';
this.REMEMBER_KEY = 'remember_login';
}
// 登录
login(token, rememberMe = false) {
if (rememberMe) {
// 记住登录状态30天
const expires = new Date();
expires.setDate(expires.getDate() + 30);
CookieManager.setCookie(this.TOKEN_KEY, token, {
expires,
secure: true,
sameSite: 'Strict'
});
CookieManager.setCookie(this.REMEMBER_KEY, 'true', {
expires,
secure: true,
sameSite: 'Strict'
});
} else {
// 会话Cookie,关闭浏览器就失效
CookieManager.setCookie(this.TOKEN_KEY, token, {
secure: true,
sameSite: 'Strict'
});
}
console.log('用户已登录');
}
// 登出
logout() {
CookieManager.deleteCookie(this.TOKEN_KEY);
CookieManager.deleteCookie(this.REMEMBER_KEY);
console.log('用户已登出');
}
// 检查登录状态
isLoggedIn() {
return CookieManager.getCookie(this.TOKEN_KEY) !== null;
}
// 获取认证令牌
getToken() {
return CookieManager.getCookie(this.TOKEN_KEY);
}
// 检查是否记住登录
isRememberLogin() {
return CookieManager.getCookie(this.REMEMBER_KEY) === 'true';
}
}
// 使用示例
const authManager = new AuthManager();
// 登录时
function handleLogin(username, password, rememberMe) {
// 假设这里调用登录API
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.token) {
authManager.login(data.token, rememberMe);
window.location.href = '/dashboard';
}
})
.catch(error => {
console.error('登录失败:', error);
});
}
1.4 File System Access API:直接操作本地文件
这个比较特殊,用来干嘛?
- VS Code网页版:直接编辑你电脑上的代码文件
- 网页版视频编辑器:导入本地视频进行编辑
- 在线图片编辑器:直接保存到你指定的文件夹
使用条件:
- 用户必须主动选择文件或文件夹
- 浏览器会弹出权限确认
- 主要用于专业工具类网站
使用示例:
// 文件系统访问管理器
class FileSystemAccessManager {
constructor() {
this.supportedTypes = {
text: {
description: '文本文件',
accept: {
'text/plain': ['.txt'],
'text/javascript': ['.js'],
'text/html': ['.html'],
'text/css': ['.css']
}
},
image: {
description: '图片文件',
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp']
}
},
video: {
description: '视频文件',
accept: {
'video/*': ['.mp4', '.webm', '.ogg']
}
}
};
}
// 检查浏览器支持
isSupported() {
return 'showOpenFilePicker' in window;
}
// 选择并读取文件
async openFile(type = 'text') {
if (!this.isSupported()) {
throw new Error('浏览器不支持File System Access API');
}
try {
const [fileHandle] = await window.showOpenFilePicker({
types: [this.supportedTypes[type]],
multiple: false
});
const file = await fileHandle.getFile();
const content = await file.text();
return {
handle: fileHandle,
file,
content,
name: file.name,
size: file.size,
lastModified: file.lastModified
};
} catch (error) {
if (error.name === 'AbortError') {
console.log('用户取消了文件选择');
return null;
}
throw error;
}
}
// 选择多个文件
async openMultipleFiles(type = 'image') {
if (!this.isSupported()) {
throw new Error('浏览器不支持File System Access API');
}
try {
const fileHandles = await window.showOpenFilePicker({
types: [this.supportedTypes[type]],
multiple: true
});
const files = [];
for (const handle of fileHandles) {
const file = await handle.getFile();
files.push({
handle,
file,
name: file.name,
size: file.size,
lastModified: file.lastModified
});
}
return files;
} catch (error) {
if (error.name === 'AbortError') {
console.log('用户取消了文件选择');
return [];
}
throw error;
}
}
// 保存文件
async saveFile(content, suggestedName = 'untitled.txt', type = 'text') {
if (!this.isSupported()) {
// 降级到下载
this.downloadFile(content, suggestedName);
return;
}
try {
const fileHandle = await window.showSaveFilePicker({
types: [this.supportedTypes[type]],
suggestedName
});
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
console.log('文件已保存');
return fileHandle;
} catch (error) {
if (error.name === 'AbortError') {
console.log('用户取消了文件保存');
return null;
}
throw error;
}
}
// 降级方案:下载文件
downloadFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('文件已下载');
}
// 选择目录
async openDirectory() {
if (!('showDirectoryPicker' in window)) {
throw new Error('浏览器不支持目录选择');
}
try {
const dirHandle = await window.showDirectoryPicker();
return dirHandle;
} catch (error) {
if (error.name === 'AbortError') {
console.log('用户取消了目录选择');
return null;
}
throw error;
}
}
}
// 在线代码编辑器示例
class OnlineCodeEditor {
constructor() {
this.fsManager = new FileSystemAccessManager();
this.currentFileHandle = null;
this.editor = document.getElementById('code-editor');
}
// 打开文件
async openFile() {
try {
const result = await this.fsManager.openFile('text');
if (result) {
this.currentFileHandle = result.handle;
this.editor.value = result.content;
document.title = `编辑器 - ${result.name}`;
console.log(`已打开文件: ${result.name}`);
}
} catch (error) {
console.error('打开文件失败:', error);
alert('打开文件失败: ' + error.message);
}
}
// 保存文件
async saveFile() {
try {
const content = this.editor.value;
if (this.currentFileHandle) {
// 保存到当前文件
const writable = await this.currentFileHandle.createWritable();
await writable.write(content);
await writable.close();
console.log('文件已保存');
} else {
// 另存为新文件
const handle = await this.fsManager.saveFile(content, 'code.js', 'text');
if (handle) {
this.currentFileHandle = handle;
}
}
} catch (error) {
console.error('保存文件失败:', error);
alert('保存文件失败: ' + error.message);
}
}
// 另存为
async saveAsFile() {
try {
const content = this.editor.value;
const handle = await this.fsManager.saveFile(content, 'code.js', 'text');
if (handle) {
this.currentFileHandle = handle;
}
} catch (error) {
console.error('另存为失败:', error);
alert('另存为失败: ' + error.message);
}
}
}
// 使用示例
const codeEditor = new OnlineCodeEditor();
// 绑定按钮事件
document.getElementById('open-btn')?.addEventListener('click', () => {
codeEditor.openFile();
});
document.getElementById('save-btn')?.addEventListener('click', () => {
codeEditor.saveFile();
});
document.getElementById('save-as-btn')?.addEventListener('click', () => {
codeEditor.saveAsFile();
});
2 存储容量:比你想象的大得多
2.1 到底能存多少东西?
先说结论:现在的浏览器存储空间大得惊人,基本不用担心不够用。
Chrome浏览器:最大方
- 如果你硬盘有500GB,Chrome最多能用400GB来存网页数据
- 单个网站最多能用300GB
- 隐身模式比较抠门,只给25GB
Firefox:也很慷慨
- 能用一半的可用空间
- 同一个网站(包括子域名)最多2GB
Safari:相对保守
- 默认给1GB
- 用完了会问你要不要再给200MB
- 如果是添加到桌面的网页应用,空间会更大
2.2 这些数字意味着什么?
我们用具体例子来感受一下:
一个音乐网站能存多少歌?
- 一首3分钟的歌(128kbps):约3MB
- 1GB能存300多首歌
- Chrome给的空间能存10万首歌!
一个新闻应用能存多少文章?
- 一篇图文并茂的新闻:约50KB
- 1GB能存2万篇文章
- 够你看一辈子了
一个在线文档应用能存多少文档?
- 一个10页的Word文档:约100KB
- 1GB能存1万个文档
- 比大多数人一辈子写的都多
实际项目中的存储使用:
拿一个典型的新闻应用举例:
- 应用本身(HTML、CSS、JS、图标):10MB
- 缓存100篇新闻文章:5MB
- 用户设置、阅读历史:1MB
- 总共才16MB,连浏览器限制的零头都不到
所以,容量基本不是问题,关键是怎么合理使用。
3 存储容量检测与管理
3.1 使用StorageManager API检测容量
现代浏览器提供了StorageManager API来查询存储使用情况:
// 存储容量监控器
class StorageMonitor {
constructor() {
this.updateInterval = null;
}
// 检查存储使用情况
async checkStorageUsage() {
if (!navigator.storage?.estimate) {
console.warn('浏览器不支持StorageManager API');
return null;
}
try {
const estimate = await navigator.storage.estimate();
const usage = {
used: estimate.usage || 0,
quota: estimate.quota || 0,
usedMB: ((estimate.usage || 0) / 1024 / 1024).toFixed(2),
quotaMB: ((estimate.quota || 0) / 1024 / 1024).toFixed(2),
percentage: estimate.quota ? ((estimate.usage || 0) / estimate.quota * 100).toFixed(2) : 0,
remaining: (estimate.quota || 0) - (estimate.usage || 0),
remainingMB: (((estimate.quota || 0) - (estimate.usage || 0)) / 1024 / 1024).toFixed(2)
};
return usage;
} catch (error) {
console.error('检查存储使用情况失败:', error);
return null;
}
}
// 显示存储使用情况
async displayStorageUsage() {
const usage = await this.checkStorageUsage();
if (!usage) return;
console.log('=== 存储使用情况 ===');
console.log(`已使用: ${usage.usedMB} MB`);
console.log(`总配额: ${usage.quotaMB} MB`);
console.log(`使用率: ${usage.percentage}%`);
console.log(`剩余空间: ${usage.remainingMB} MB`);
// 更新UI
this.updateStorageUI(usage);
return usage;
}
// 更新存储UI
updateStorageUI(usage) {
const progressBar = document.querySelector('.storage-progress');
const usageText = document.querySelector('.storage-usage-text');
if (progressBar) {
progressBar.style.width = `${usage.percentage}%`;
progressBar.className = `storage-progress ${
usage.percentage > 90 ? 'danger' :
usage.percentage > 70 ? 'warning' : 'normal'
}`;
}
if (usageText) {
usageText.textContent = `已使用 ${usage.usedMB} MB / ${usage.quotaMB} MB (${usage.percentage}%)`;
}
}
// 开始监控
startMonitoring(intervalMs = 30000) {
this.stopMonitoring();
this.updateInterval = setInterval(() => {
this.displayStorageUsage();
}, intervalMs);
// 立即执行一次
this.displayStorageUsage();
}
// 停止监控
stopMonitoring() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
// 检查是否需要清理
async needsCleanup(threshold = 80) {
const usage = await this.checkStorageUsage();
return usage && usage.percentage > threshold;
}
// 获取详细的存储分解
async getStorageBreakdown() {
const breakdown = {
indexedDB: 0,
cache: 0,
localStorage: 0,
sessionStorage: 0
};
try {
// 估算LocalStorage大小
let localStorageSize = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
localStorageSize += localStorage[key].length + key.length;
}
}
breakdown.localStorage = localStorageSize;
// 估算SessionStorage大小
let sessionStorageSize = 0;
for (let key in sessionStorage) {
if (sessionStorage.hasOwnProperty(key)) {
sessionStorageSize += sessionStorage[key].length + key.length;
}
}
breakdown.sessionStorage = sessionStorageSize;
// Cache API大小需要遍历所有缓存
if ('caches' in window) {
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
// 这里只能估算,实际大小需要获取每个响应
breakdown.cache += requests.length * 1024; // 粗略估算
}
}
return breakdown;
} catch (error) {
console.error('获取存储分解失败:', error);
return breakdown;
}
}
}
// 使用示例
const storageMonitor = new StorageMonitor();
// 开始监控存储使用情况
storageMonitor.startMonitoring(60000); // 每分钟检查一次
// 手动检查
document.getElementById('check-storage-btn')?.addEventListener('click', async () => {
const usage = await storageMonitor.displayStorageUsage();
if (await storageMonitor.needsCleanup()) {
if (confirm('存储空间使用率较高,是否需要清理?')) {
// 触发清理逻辑
console.log('开始清理存储空间...');
}
}
});
3.2 开发者工具调试
在开发过程中,你可以使用浏览器开发者工具来:
- 查看存储使用情况:Application → Storage
- 清除存储数据:方便测试不同场景
- 模拟存储限制:Chrome 88+支持自定义存储配额模拟
Chrome存储配额模拟步骤:
- 打开开发者工具
- 进入Application → Storage
- 勾选"Simulate custom storage quota"
- 输入想要模拟的存储限制
总结
传统存储方式虽然有各自的局限性,但在特定场景下仍然有用:
- LocalStorage:适合存储简单的用户偏好设置
- SessionStorage:适合临时状态和会话数据
- Cookies:主要用于身份认证和服务器通信
- File System Access API:专业工具的本地文件操作
关键是要了解每种技术的特点和限制,在合适的场景使用合适的技术。同时,现代浏览器提供了充足的存储空间,重点应该放在如何合理管理和使用这些空间上。
- 点赞
- 收藏
- 关注作者
评论(0)