现代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);}}// 更新购物车UIupdateCartUI() {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 {// 设置Cookiestatic 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}`);}// 获取Cookiestatic 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;}// 删除Cookiestatic 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}`);}// 获取所有Cookiestatic 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) {// 假设这里调用登录APIfetch('/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`);// 更新UIthis.updateStorageUI(usage);return usage;}// 更新存储UIupdateStorageUI(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:专业工具的本地文件操作
关键是要了解每种技术的特点和限制,在合适的场景使用合适的技术。同时,现代浏览器提供了充足的存储空间,重点应该放在如何合理管理和使用这些空间上。