在后端开发中,存储是工作中常见的一部分。应用程序数据存储在数据库中,文件存储在对象存储中,瞬态数据存储在缓存中……似乎有无限的可能性来存储任何类型的数据。但数据存储不仅限于后端。前端(浏览器)也配备了许多存储数据的选项。我们可以通过利用此存储来提高应用程序性能,保存用户偏好,在多个会话甚至不同的计算机之间保持应用程序状态。
在本文中,我们将介绍在浏览器中存储数据的不同可能性。我们将介绍每种方法的三个用例,以掌握其优缺点。最后,您将能够确定哪种存储最适合您的用例。所以让我们开始吧!
localStorage
API
localStorage
是浏览器中最受欢迎的存储选项之一,也是许多开发人员的首选。数据跨会话存储,从未与服务器共享,并且可用于同一协议和域下的所有页面。存储限制为约 5MB。
令人惊讶的是,Google Chrome 团队不建议使用此选项,因为它会阻塞主线程,并且无法访问 Web 工作线程和服务工作线程。他们启动了一个实验,键值存储,作为更好的版本,但它只是一个试验,似乎还没有取得任何进展。
localStorage
API 可作为 window.localStorage
使用,并且只能保存 UTF-16 字符串。我们必须确保在将数据保存到 localStorage
之前将其转换为字符串。主要三个函数是
setItem('key',
'value')
getItem('key')
removeItem('key')
它们都是同步的,这使得它们易于使用,但它们会阻塞主线程。
值得一提的是,localStorage
还有一个名为 sessionStorage
的孪生兄弟。唯一的区别是存储在 sessionStorage
中的数据仅在当前会话中有效,但 API 是相同的。
让我们看看它的实际应用。第一个示例演示了如何使用 localStorage
存储用户的偏好设置。在我们的例子中,它是一个布尔属性,用于打开或关闭我们网站的暗黑主题。
您可以选中复选框并刷新页面以查看状态是否跨会话保存。查看 save
和 load
函数以了解我如何将值转换为字符串以及如何解析它。重要的是要记住,我们只能存储字符串。
第二个示例从 PokéAPI 加载神奇宝贝名称。
我们使用 fetch
发送 GET 请求并在 ul
元素中列出所有名称。在收到响应后,我们将其缓存到 localStorage
中,以便我们下次访问可以更快甚至离线工作。我们必须使用 JSON.stringify
将数据转换为字符串,并使用 JSON.parse
从缓存中读取它。
在最后一个示例中,我演示了一个用例,用户可以在其中浏览不同的神奇宝贝页面,并且当前页面会为下次访问保存。
在这种情况下,localStorage
的问题在于状态是本地保存的。这种行为不允许我们与朋友共享所需的页面。稍后,我们将了解如何克服此问题。
我们还将在接下来的存储选项中使用这三个示例。我分叉了 Pens 并只更改了相关函数。所有方法的整体框架都是相同的。
IndexedDB API
IndexedDB 是浏览器中的一种现代存储解决方案。它可以存储大量结构化数据,甚至包括文件和 Blob。像每个数据库一样,IndexedDB 对数据进行索引以有效地运行查询。使用 IndexedDB 更加复杂。我们必须创建数据库、表并使用事务。
与 localStorage
相比,IndexedDB 需要更多代码。在示例中,我使用原生 API 以及 Promise 包装器,但我强烈建议使用第三方库来帮助您。我的推荐是 localForage,因为它使用相同的 localStorage
API,但以渐进增强的方式实现它,这意味着如果您的浏览器支持 IndexedDB,它将使用它;如果不支持,它将回退到 localStorage
。
让我们编写代码,并转到我们的用户偏好设置示例!
idb
是我们用来代替使用低级基于事件的 API 的 Promise 包装器。它们几乎相同,所以不用担心。首先要注意的是,每次访问数据库都是异步的,这意味着我们不会阻塞主线程。与 localStorage
相比,这是一个主要优势。
我们需要打开数据库连接,以便它可以在整个应用程序中用于读取和写入。我们为数据库命名为 my-db
,架构版本为 1,以及一个更新函数以应用版本之间的更改。这与数据库迁移非常相似。我们的数据库架构很简单:只有一个对象存储 preferences
。对象存储相当于 SQL 表。要写入或读取数据库,我们必须使用事务。这是使用 IndexedDB 的繁琐部分。请查看演示中的新 save
和 load
函数。
毫无疑问,与 localStorage
相比,IndexedDB 的开销要大得多,学习曲线也更陡峭。对于键值用例,使用 localStorage
或帮助我们提高生产力的第三方库可能更有意义。
应用程序数据(例如我们神奇宝贝示例中的数据)是 IndexedDB 的强项。您可以在此数据库中存储数百兆字节甚至更多的数据。您可以将所有神奇宝贝存储在 IndexedDB 中,并在脱机状态下使用它们,甚至对其进行索引!这绝对是存储应用程序数据的最佳选择。
我跳过了第三个示例的实现,因为与 localStorage
相比,IndexedDB 在此案例中没有任何区别。即使使用 IndexedDB,用户仍然无法与他人共享所选页面或将其添加为书签以供将来使用。它们都不适合此用例。
Cookie
使用 Cookie 是一种独特的存储选项。它是唯一也与服务器共享的存储。Cookie 作为每个请求的一部分发送。当用户浏览我们应用程序中的页面或发送 Ajax 请求时,它可能会发生。这使我们能够在客户端和服务器之间创建共享状态,以及在不同子域中的多个应用程序之间共享状态。本文中描述的其他存储选项无法实现此功能。一个警告:Cookie 会随每个请求一起发送,这意味着我们必须保持 Cookie 的大小以维持合理的请求大小。
Cookie 最常见的用途是身份验证,这超出了本文的范围。与 localStorage
一样,Cookie 也只能存储字符串。Cookie 连接到一个分号分隔的字符串中,并以请求的 Cookie 标头发送。您可以为每个 Cookie 设置许多属性,例如过期时间、允许的域、允许的页面等等。
在示例中,我展示了如何通过客户端操作 Cookie,但也可以在您的服务器端应用程序中更改它们。
如果服务器可以以某种方式利用 Cookie,则将用户的首选项保存在 Cookie 中可能是一个不错的选择。例如,在主题用例中,服务器可以传递相关的 CSS 文件并减少潜在的包大小(如果我们正在执行服务器端渲染)。另一个用例可能是跨多个子域应用程序共享这些首选项而无需数据库。
使用 JavaScript 读取和写入 Cookie 并不像您想象的那样简单。要保存新的 Cookie,您需要设置 document.cookie
- 查看上面示例中的 save
函数。我设置了 dark_theme
Cookie 并向其添加了 max-age
属性以确保它在选项卡关闭时不会过期。此外,我还添加了 SameSite
和 Secure
属性。这些是必要的,因为 CodePen 使用 iframe 来运行示例,但在大多数情况下您不需要它们。读取 Cookie 需要解析 Cookie 字符串。
Cookie 字符串如下所示
key1=value1;key2=value2;key3=value3
因此,首先,我们必须用分号分割字符串。现在,我们有一个以 key1=value1
形式表示的 Cookie 数组,因此我们需要在数组中找到正确的元素。最后,我们用等号分割并获取新数组中的最后一个元素。有点繁琐,但是一旦您实现了 getCookie
函数(或从我的示例中复制它 :P),您就可以忘记它了。
在 Cookie 中保存应用程序数据可能是一个坏主意!它会大大增加请求大小并降低应用程序性能。此外,服务器无法从这些信息中获益,因为它是其数据库中已有的信息的陈旧版本。如果使用 Cookie,请确保将其保持较小。
分页示例也不适合 Cookie,就像 localStorage
和 IndexedDB 一样。当前页面是我们希望与他人共享的临时状态,而这些方法都无法实现它。
URL 存储
URL 本身并不是存储,但它是一种创建可共享状态的好方法。在实践中,这意味着向当前 URL 添加查询参数,这些参数可用于重新创建当前状态。最好的例子是搜索查询和过滤器。如果我们在 CSS-Tricks 上搜索术语 flexbox
,则 URL 将更新为 https://css-tricks.cn/?s=flexbox
。看看使用 URL 共享搜索查询有多容易?另一个优点是,您只需点击刷新按钮即可获取查询的最新结果,甚至可以将其添加为书签。
我们只能在 URL 中保存字符串,并且其最大长度是有限制的,因此我们没有太多空间。我们必须保持我们的状态很小。没有人喜欢又长又吓人的 URL。
同样,CodePen 使用 iframe 来运行示例,因此您无法看到 URL 实际上发生了变化。不用担心,因为所有必要的部分都在那里,所以您可以在任何地方使用它。
我们可以通过 window.location.search
访问查询字符串,幸运的是,它可以使用 URLSearchParams
类进行解析。不再需要应用任何复杂的字符串解析了。当我们想要读取当前值时,可以使用 get
函数。当我们想要写入时,可以使用 set
。仅仅设置值是不够的;我们还需要更新 URL。这可以通过 history.pushState
或 history.replaceState
完成,具体取决于我们想要实现的行为。
我不建议将用户的偏好保存在 URL 中,因为我们必须将此状态添加到用户访问的每个 URL 中,而我们无法保证这一点;例如,如果用户点击 Google 搜索中的链接。
就像 cookie 一样,我们无法在 URL 中保存应用程序数据,因为我们的空间非常有限。即使我们设法存储它,URL 也会很长,而且不会让人想点击。看起来像是某种网络钓鱼攻击。
就像我们的分页示例一样,临时应用程序状态最适合 URL 查询字符串。同样,您无法看到 URL 更改,但每次点击页面时,URL 都会使用 ?page=x
查询参数进行更新。当网页加载时,它会查找此查询参数并相应地获取正确的页面。现在我们可以与朋友分享此 URL,以便他们可以欣赏我们最喜欢的宝可梦。
缓存 API
缓存 API 是网络级别的存储。它用于缓存网络请求及其响应。缓存 API 与 Service Worker 完美契合。Service Worker 可以拦截每个网络请求,并使用缓存 API 轻松缓存这两个请求。Service Worker 还可以返回现有的缓存项作为网络响应,而不是从服务器获取它。通过这样做,您可以减少网络加载时间,并使您的应用程序即使在离线状态下也能工作。最初,它是为 Service Worker 创建的,但在现代浏览器中,缓存 API 也在 window、iframe 和 worker 上下文中可用。这是一个非常强大的 API,可以极大地改善应用程序的用户体验。
就像 IndexedDB 一样,缓存 API 存储不受限制,您可以存储数百兆字节,如果需要,甚至可以存储更多。API 是异步的,因此它不会阻塞您的主线程。并且可以通过全局属性 caches
访问。
要了解有关缓存 API 的更多信息,Google Chrome 团队制作了一个很棒的教程。
Chris 创建了一个很棒的 Pen,其中包含一个结合了 Service Worker 和缓存 API 的实用示例。
额外内容:浏览器扩展
如果您构建浏览器扩展,则可以使用另一种存储数据的方式。我在开发我的扩展程序 daily.dev 时发现了它。它可以通过 chrome.storage
或 browser.storage
使用,如果您使用 Mozilla 的 polyfill。确保在清单文件中请求存储权限才能获得访问权限。
有两种类型的存储选项,本地存储和同步存储。本地存储不言而喻;这意味着它不会共享并保存在本地。同步存储作为 Google 帐户的一部分同步,您在使用同一帐户安装扩展程序的任何地方,此存储都将同步。如果您问我,这是一个很酷的功能。两者都具有相同的 API,因此在需要时可以非常轻松地在两者之间切换。它是异步存储,因此不会像 localStorage
那样阻塞主线程。不幸的是,我无法为此存储选项创建演示,因为它需要浏览器扩展,但它非常易于使用,并且几乎与 localStorage
相同。有关确切实现的更多信息,请参考 Chrome 文档。
结论
浏览器有很多选项可以用来存储我们的数据。遵循 Chrome 团队的建议,我们的首选存储应该是 IndexedDB。它是一个异步存储,有足够的空间来存储我们想要的任何东西。不鼓励使用 localStorage
,但它比 IndexedDB 更易于使用。Cookie 是一种与服务器共享客户端状态的好方法,但主要用于身份验证。
如果您想创建具有可共享状态的页面(例如搜索页面),请使用 URL 的查询字符串来存储此信息。最后,如果您构建扩展程序,请务必阅读有关 chrome.storage
的内容。
我们可以获得一些新的 CookieStore API 的关注吗?https://wicg.github.io/cookie-store/explainer.html
呵呵,这篇文章是在最近发布 CookieStore 之前写的。
作为存储,还有 window.name。这不是它的真正用途,但可以存储高达 2MB 的数据。
这是一个很棒的、全面的和经过深思熟虑的概述!
谢谢!
引用:“就像 IndexedDB 一样,缓存 API 存储不受限制,您可以存储数百兆字节,如果需要,甚至可以存储更多。”
嗯,据我所知,缓存的大小存在特定限制,并且它们因浏览器而异。
此外,Safari 似乎会在 7 天(我认为)内未访问或页面未添加到主屏幕(在 iOS 中)时删除任何类型的缓存。
所以我的问题是:有没有人看到过关于最大存储大小和到期日期的基于浏览器的最新概述?