当涉及到 Web 应用程序中的客户端存储时,localStorage API 作为一种简单且广泛支持的解决方案脱颖而出。它允许开发者直接在用户的浏览器中存储键值对。在这篇文章中,我们将探讨 localStorage API 的各个方面,包括其优势、局限性以及现代应用程序可用的替代存储选项。
什么是 localStorage API?
localStorage API 是 Web 浏览器的内置功能,使 Web 开发者能够在用户的设备上持久地存储少量数据。它基于简单的键值对操作,允许开发者保存字符串、数字和其他简单的数据类型。这些数据即使在用户关闭浏览器或离开页面后也仍然可用。API 提供了一种方便的方式来保持状态和存储用户偏好设置,而无需依赖于服务器端存储。
探索本地存储方法:实践示例
让我们通过一些动手编码示例来更好地理解如何利用 localStorage 的强大功能。API 提供了几种交互方法,包括 setItem、getItem、removeItem 和 clear。考虑以下代码片段:
// 使用setItem存储数据
localStorage.setItem('username', 'john_doe')
// 使用getItem检索数据
const storedUsername = localStorage.getItem('username')
// 使用removeItem删除数据
localStorage.removeItem('username')
// 清除所有数据
localStorage.clear()
在 JavaScript 中使用 JSON 序列化存储复杂数据
虽然 localStorage 擅长处理简单的键值对,但它还支持通过 JSON 序列化存储更复杂的数据。通过使用 JSON.stringify 和 JSON.parse,你可以存储和检索像对象和数组这样的结构化数据。以下是存储一个文档的示例:
const user = { name: 'Alice', age: 30, email: 'alice@example.com' }
// 存储一个用户对象
localStorage.setItem('user', JSON.stringify(user))
// 检索并解析用户对象
const storedUser = JSON.parse(localStorage.getItem('user'))
理解本地存储的局限性
尽管 localStorage 非常方便,但它确实带有开发者应该注意的一系列局限性:
非异步阻塞 API:一个重大缺点是 localStorage 作为一个非异步阻塞 API 运行。这意味着对 localStorage 执行的任何操作都可能阻塞主线程,导致应用程序性能下降和用户体验不响应。
有限的数据结构:与更先进的数据库不同,localStorage 仅限于简单的键值存储。这种限制使其不适合存储复杂的数据结构或管理数据元素之间的关系。
序列化开销:在 localStorage 中存储 JSON 数据需要在存储前将数据序列化,并在检索时解析它。这个过程引入了性能开销,可能会使操作速度降低多达 10 倍。
缺乏索引:localStorage 缺乏索引功能,使得基于特定标准执行高效搜索或迭代数据变得困难。这种局限性可能会阻碍依赖复杂数据检索的应用程序。
标签阻塞:在多标签环境中,一个标签的 localStorage 操作可能会通过独占 CPU 资源影响其他标签的性能。你可以通过在两个浏览器窗口中打开这个测试文件[1]并在其中一个中触发 localstorage 插入来观察这种情况。你会观察到两个窗口中的指示器都会卡住。
存储限制:浏览器通常对每个 origin 的 localStorage 规定大约 5 MiB[2] 的存储限制。
仍然使用 localStorage 的原因
localStorage 慢吗?
与关于性能的担忧相反,JavaScript 中的 localStorage API 在与 IndexedDB 或 OPFS 等替代存储解决方案相比时,实际上是相当快的。它在处理小型键值分配方面表现出色。由于其简单性和与浏览器的直接集成,访问和修改 localStorage 数据的开销很小。对于需要快速简单数据存储的场景,localStorage 仍然是一个可行的选择。例如,RxDB 在 localStorage 元优化器[3]中使用 localStorage 来管理简单的键值对,同时将“正常”文档存储在另一个存储中,如 IndexedDB。
何时不使用 localStorage
虽然 localStorage 提供了便利,但它可能不适用于每个用例。考虑以下情况,替代方案可能更合适:
数据必须可查询:如果你的应用程序严重依赖于基于特定标准的查询数据,localStorage 可能无法提供必要的查询功能。复杂的数据检索可能导致代码效率低下和性能缓慢。
大型 JSON 文档:在 localStorage 中存储大型 JSON 文档可能会消耗大量内存并降低性能。评估你打算存储的数据大小,并考虑更强大的解决方案来处理大量数据集至关重要。
大量读写操作:对 localStorage 的过度读写操作可能导致性能瓶颈。其他存储解决方案可能为需要频繁数据操作的应用程序提供更好的性能和可扩展性。
不需要持久性:如果你的应用程序可以在没有跨会话持久数据的情况下运行,考虑使用像 new Map() 或 new Set() 这样的内存数据结构。这些选项为瞬态数据提供了速度和效率。
在 JavaScript 中使用 localStorage API 的替代品
localStorage 与 IndexedDB
虽然 localStorage 作为简单数据需求的可靠存储解决方案,但在处理更复杂需求时,探索像 IndexedDB 这样的替代品是必要的。IndexedDB 旨在存储的不仅是键值对,还有 JSON 文档。与通常每个域有大约 5-10MB 存储限制的 localStorage 不同,IndexedDB 可以处理更大的数据集。IndexDB 通过支持索引来促进高效查询,使范围查询成为可能。然而,值得注意的是 IndexedDB 缺乏 localStorage 通过storage事件提供的独特可观察性。此外,复杂查询可能对 IndexedDB 构成挑战,虽然其性能可以接受,但 IndexedDB 对于某些用例来说可能太慢了[4]。
// localStorage可以通过存储事件观察变化。
// 这个功能在IndexedDB中是缺失的。
addEventListener('storage', (event) => {})
对于那些希望利用 IndexedDB 的全部功能并增加能力的开发者,建议使用像 RxDB[5] 或 Dexie.js[6] 这样的包装库。这些库通过复杂查询和可观察性等功能增强了 IndexedDB,提高了其在现代应用程序中的可用性。
文件系统 API (OPFS)
另一个不错的选择是 OPFS(文件系统 API)。这个 API 提供了直接访问基于 origin 的、沙盒化的文件系统,它的性能经过优化,并提供对其内容的原地写入访问。OPFS 有着令人赞叹的性能优势。然而,使用 OPFS API 可能会很复杂,并且它只能在 WebWorker 内部访问。为了简化其使用并扩展其功能,考虑使用像 RxDB 的 OPFS RxStorage[7] 这样的包装库,它在 OPFS API 之上构建了一个全面的数据库。这种抽象允许你利用 OPFS API 的强大功能,而避免直接使用的复杂性。
localStorage 与 Cookies
曾经是客户端数据存储的主要方法的 Cookies,由于其局限性,在现代 Web 开发中已经失宠。虽然它们可以存储数据,但与 localStorage API 相比,它们大约慢 100 倍。此外,cookies 包含在 HTTP 头中,这可能会影响网络性能。因此,不推荐在现代 Web 应用程序中使用 cookies 进行数据存储。
localStorage 与 WebSQL
尽管 WebSQL 提供了基于 SQL 的客户端数据存储接口,但它是一种已弃用的技术,应该避免使用。它的 API 已经从现代浏览器中逐步淘汰,缺乏像 IndexedDB 这样的替代品的健壮性。此外,WebSQL 通常比 IndexedDB 慢大约 10 倍,这使得它成为需要高效数据操作和检索的应用程序的一个不理想的选择。
localStorage 与 sessionStorage
在不需要跨会话持久性的情况下,开发者通常会转向 sessionStorage。这种存储机制仅在标签或浏览器会话期间保留数据。它在页面重新加载和恢复时存活,为临时数据需求提供了方便的解决方案。然而,重要的是要注意,sessionStorage 的范围有限,可能不适用于所有用例。
React Native 的 AsyncStorage
对于 React Native 开发者来说,AsyncStorage API 是首选解决方案,它模仿了 localStorage 的行为,但具有异步支持。由于不是所有的 JavaScript 运行时都支持 localStorage,AsyncStorage 为 React Native 应用程序中的数据持久性提供了无缝的替代方案。
Node.js 的 node-localstorage
由于原生的 localStorage 在 Node.js JavaScript 运行时中不存在,你将在 Node.js 或基于 Node 的运行时(如 Next.js)中得到 ReferenceError: localStorage is not defined 的错误。node-localstorage npm 包弥补了这一差距。这个包在 Node.js 环境中复制了浏览器的 localStorage API,确保了一致和兼容的数据存储能力。
浏览器扩展中的 localStorage
虽然 Chrome 和 Firefox 的浏览器扩展支持 localStorage API,但在该上下文中存储与扩展相关的数据并不推荐。在许多情况下,如用户清除浏览历史时,浏览器会清除数据。
相反,应该使用扩展存储 API。与 localStorage 相比,存储 API 是异步的,所有操作都返回一个 Promise。此外,它提供自动同步,以在用户登录的所有浏览器实例之间复制数据。存储 API 甚至能够存储 JSON 可序列化对象,而不是简单的字符串。
// 在chrome中使用存储API
await chrome.storage.local.set({ foobar: { nr: 1 } })
const result = await chrome.storage.local.get('foobar')
console.log(result.foobar) // {nr: 1}
Deno 和 Bun 中的 localStorage
Deno JavaScript 运行时具有一个工作的 localStorage API,所以运行 localStorage.setItem() 和其他方法,将会工作,并且本地存储的数据将在多次运行中持续存在。
Bun 不支持 localStorage JavaScript API。尝试使用 localStorage 会得到 ReferenceError: Can't find variable: localStorage 的错误。要在 Bun 中本地存储数据,你可以使用 bun:sqlite 模块,或者直接使用 RxDB 等支持 Bun 的 JavaScript 数据库。
结论:选择正确的存储解决方案
在现代 Web 开发的世界上,localStorage 作为轻量级数据存储的有价值工具,它的简单性和速度使其成为小型键值对存储的极佳选择。然而,随着应用程序复杂性的增长,开发者必须仔细评估他们的存储需求。对于需要高级查询、复杂数据结构或高容量操作的场景,像 IndexedDB、带有额外功能(如 RxDB)的包装库或平台特定的 API 提供了更强大的解决方案。通过了解各种存储选项的优势和局限性,开发者可以做出明智的决策,为高效和可扩展的应用程序铺平道路。