In backend development, storage is a common part of the job. Application data is stored in databases, files are stored in object storage, transient data is stored in caches... there are seemingly endless possibilities for storing any type of data. However, data storage is not limited to the backend , the frontend (browser) also has many options for storing data. We can improve the performance of our applications by taking advantage of this storage method, save user preferences, and maintain application state across multiple sessions and even different computers.
In this article we will go through the different possibilities for storing data in the browser. We'll cover three use cases for each approach to get a handle on its pros and cons. In the end, you'll be able to decide what storage is best for your use case.
let's start!
localStorage
is one of the most popular storage options in browsers and the first choice for many developers. Data is stored across sessions, never shared with the server, and available to all pages under the same protocol and domain. Storage limit is ~5MB.
Surprisingly, the Google Chrome team does not recommend using this option because it blocks the main thread and makes web workers and service workers inaccessible. They launched an experiment: KV Storage, as a better version, but it was just an experiment and there doesn't seem to be any progress yet.
localStorage API
is available as window.localStorage
and can only save UTF-16 strings. Before saving the data to localStorage
we have to make sure it is converted to a string. The main three functions are:
setItem('key',
'value')
getItem(' key')
removeItem('key')
They are all synchronous, so they are simple to use, but they will block the main thread.
It is worth mentioning that localStorage
has a twin called sessionStorage
. The only difference is that the data stored in sessionStorage
will only persist for the current session, but the API is the same.
This is too simple, I believe everyone has used it.
IndexedDB is a modern storage solution in the browser. It can store large amounts of structured data, even files and blobs. Like every database, IndexedDB indexes data to run queries efficiently. Using IndexedDB is more complicated, we have to create a database, tables, and use transactions.
Compared with localStorage
, IndexedDB requires more code. In the example, I used the native API with a Promise wrapper, but I highly recommend using a third-party library to help you. What I recommend is localForage, because it uses the same localStorage
API, but the implementation is gradually enhanced, that is, if your browser supports IndexedDB, it will be used; if not, It will fall back to localStorage
.
Let’s write some code and head to our user preference example!
<input> <label>Dark theme</label><br>
let db; function toggle(on) { if (on) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } async function save(on) { const tx = db.transaction('preferences', 'readwrite'); const store = tx.objectStore('preferences'); store.put({key: 'darkTheme', value: on}); return tx.complete; } async function load() { const tx = db.transaction('preferences', 'readonly'); const store = tx.objectStore('preferences'); const data = await store.get('darkTheme'); return data && data.value; } async function onChange(checkbox) { const value = checkbox.checked; toggle(value); await save(value); } function openDatabase() { return idb.openDB('my-db', 1, { upgrade(db) { db.createObjectStore('preferences', {keyPath: 'key'}); }, }); } openDatabase() .then((_db) => { db = _db; return load(); }) .then((initialValue) => { toggle(initialValue); document.querySelector('#darkTheme').checked = initialValue; });
Effect
idb
is a Promise wrapper that we use instead of using the low-level event-based API. The first thing to note is that every access to the database is asynchronous, which means we don't block the main thread, which is a major advantage compared to localStorage
.
We need to open a connection to the database so that it can be used for reading and writing throughout the application. We give the database a name my-db
, a schema version 1
, and an update function to apply changes between versions, much like database migrations. Our database architecture is very simple: only one object store preferences
. The object store is equivalent to a SQL table, to write to or read from the database you must use transactions, which is the tedious part of using IndexedDB. Take a look at the new save
and load
functionality in the demo.
There is no doubt that IndexedDB has more overhead and a steeper learning curve compared to localStorage
. For the key-value case, it might make more sense to use localStorage
or a third-party library, which will help us be more efficient.
<p>loading...</p>
let db; async function loadPokemons() { const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=10'); const data = await res.json(); return data.results; } function removeLoading() { const elem = document.querySelector('#loading'); if (elem) { elem.parentNode.removeChild(elem); } } function appendPokemon(pokemon) { const node = document.createElement('li'); const textnode = document.createTextNode(pokemon.name); node.appendChild(textnode); document.querySelector('#list').appendChild(node); } function clearList() { const list = document.querySelector('#list'); while (list.firstChild) { list.removeChild(list.lastChild); } } function saveToCache(pokemons) { const tx = db.transaction('pokemons', 'readwrite'); const store = tx.objectStore('pokemons'); pokemons.forEach(pokemon => store.put(pokemon)); return tx.complete; } function loadFromCache() { const tx = db.transaction('pokemons', 'readonly'); const store = tx.objectStore('pokemons'); return store.getAll(); } function openDatabase() { return idb.openDB('my-db2', 1, { upgrade(db) { db.createObjectStore('pokemons', {keyPath: 'name'}); }, }); } openDatabase() .then((_db) => { db = _db; return loadFromCache(); }) .then((cachedPokemons) => { if (cachedPokemons) { removeLoading(); cachedPokemons.forEach(appendPokemon); console.log('loaded from cache!'); } return loadPokemons(); }) .then((pokemons) => { removeLoading(); saveToCache(pokemons); clearList(); pokemons.forEach(appendPokemon); console.log('loaded from network!'); });
Effect
You can store hundreds of megabytes or more in this database. You can store all your Pokémon in IndexedDB, take them offline and even index them! This is definitely an option for storing application data.
I skipped the implementation of the third example because IndexedDB makes no difference in this case compared to localStorage
. Even with IndexedDB, users still won't share the selected page with others or bookmark it for future use. None of them are suitable for this use case.
使用cookies是一种独特的存储方式,这是唯一的与服务器共享的存储方式。Cookies作为每次请求的一部分被发送。它可以是当用户浏览我们的应用程序中的页面或当用户发送Ajax请求时。这样我们就可以在客户端和服务器之间建立一个共享状态,也可以在不同子域的多个应用程序之间共享状态。本文中介绍的其他存储选项无法实现。需要注意的是:每个请求都会发送 cookie
,这意味着我们必须保持 cookie 较小,以保持适当的请求大小。
Cookies的最常见用途是身份验证,这不在本文的讨论范围之内。就像 localStorage
一样,cookie
只能存储字符串。这些cookie被连接成一个以分号分隔的字符串,并在请求的cookie头中发送。你可以为每个cookie设置很多属性,比如过期、允许的域名、允许的页面等等。
在例子中,我展示了如何通过客户端来操作cookie,但也可以在你的服务器端应用程序中改变它们。
<input> <label>Dark theme</label>
function getCookie(cname) { const name = cname + '='; const decoded = decodeURIComponent(document.cookie); const split = decoded.split(';'); const relevantCookie = split.find((cookie) => cookie.indexOf(`${cname}=`) === 0); if (relevantCookie) { return relevantCookie.split('=')[1]; } return null; } function toggle(on) { if (on) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } function save(on) { document.cookie = `dark_theme=${on.toString()}; max-age=31536000; SameSite=None; Secure`; } function load() { return getCookie('dark_theme') === 'true'; } function onChange(checkbox) { const value = checkbox.checked; toggle(value); save(value); } const initialValue = load(); toggle(initialValue); document.querySelector('#darkTheme').checked = initialValue;
效果还是跟前面一样
将用户的喜好保存在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中可能是个坏主意!它将大大增加请求的大小,并降低应用程序性能。此外,服务器无法从这些信息中获益,因为它是数据库中已有信息的陈旧版本。如果你使用cookies,请确保它们很小。
分页示例也不适合cookie,就像 localStorage
和 IndexedDB
一样。当前页面是我们想要与他人共享的临时状态,这些方法都无法实现它。
URL本身并不是存储设备,但它是创建可共享状态的好方法。实际上,这意味着将查询参数添加到当前URL中,这些参数可用于重新创建当前状态。最好的例子是搜索查询和过滤器。如果我们在CSS-Tricks上搜索术语flexbox,则URL将更新为https://css-tricks.com/?s=fle...。看看我们使用URL后,分享搜索查询有多简单?另一个好处是,你只需点击刷新按钮,就可以获得更新的查询结果,甚至可以将其收藏。
我们只能在URL中保存字符串,它的最大长度是有限的,所以我们没有那么多的空间。我们将不得不保持我们的状态小,没有人喜欢又长又吓人的网址。
同样,CodePen使用iframe运行示例,因此您看不到URL实际更改。不用担心,因为所有的碎片都在那里,所以你可以在任何你想要的地方使用它。
<input> <label>Dark theme</label>
function toggle(on) { if (on) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } function save(on) { const params = new URLSearchParams(window.location.search); params.set('dark_theme', on.toString()); history.pushState(null, null, `?${params.toString()}`); } function load() { const params = new URLSearchParams(window.location.search); return params.get('dark_theme') === 'true'; } function onChange(checkbox) { const value = checkbox.checked; toggle(value); save(value); } const initialValue = load(); toggle(initialValue); document.querySelector('#darkTheme').checked = initialValue;
效果还是一样
我们可以通过 window.location.search
访问查询字符串,幸运的是,可以使用 URLSearchParams
类对其进行解析,无需再应用任何复杂的字符串解析。当我们想读取当前值时,可以使用 get
函数,当我们想写时,可以使用 set
。仅设置值是不够的,我们还需要更新URL。这可以使用 history.pushState
或 history.replaceState
来完成,取决于我们想要完成的行为。
我不建议将用户的偏好保存在URL中,因为我们必须将这个状态添加到用户访问的每一个URL中,而且我们无法保证;例如,如果用户点击了谷歌搜索的链接。
就像Cookie一样,由于空间太小,我们无法在URL中保存应用程序数据。而且即使我们真的设法存储它,网址也会很长,而且不吸引人点击。可能看起来像是钓鱼攻击的一种。
<p>Select page:</p> <p> <button>0</button> <button>1</button> <button>3</button> <button>4</button> <button>5</button> </p>
async function loadPokemons(page) { const res = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=10&offset=${page * 10}`); const data = await res.json(); return data.results; } function appendPokemon(pokemon) { const node = document.createElement('li'); const textnode = document.createTextNode(pokemon.name); node.appendChild(textnode); document.querySelector('#list').appendChild(node); } function clearList() { const list = document.querySelector('#list'); while (list.firstChild) { list.removeChild(list.lastChild); } } function savePage(page) { const params = new URLSearchParams(window.location.search); params.set('page', page.toString()); history.pushState(null, null, `?${params.toString()}`); } function loadPage() { const params = new URLSearchParams(window.location.search); if (params.has('page')) { return parseInt(params.get('page')); } return 0; } async function updatePage(page) { clearList(); savePage(page); const pokemons = await loadPokemons(page); pokemons.forEach(appendPokemon); } const page = loadPage(); updatePage(page);
效果
就像我们的分页例子一样,临时应用状态是最适合URL查询字符串的。同样,你无法看到URL的变化,但每次点击一个页面时,URL都会以 ?page=x
查询参数更新。当网页加载时,它会查找这个查询参数,并相应地获取正确的页面。现在,我们可以把这个网址分享给我们的朋友,让他们可以享受我们最喜欢的神奇宝贝。
Cache API是网络级的存储,它用于缓存网络请求及其响应。Cache API非常适合service worker,service worker可以拦截每一个网络请求,使用 Cache API 它可以轻松地缓存这两个请求。service worker也可以将现有的缓存项作为网络响应返回,而不是从服务器上获取。这样,您可以减少网络负载时间,并使你的应用程序即使处于脱机状态也能正常工作。最初,它是为service worker创建的,但在现代浏览器中,Cache API也可以在窗口、iframe和worker上下文中使用。这是一个非常强大的API,可以极大地改善应用的用户体验。
就像IndexedDB一样,Cache API的存储不受限制,您可以存储数百兆字节,如果需要甚至可以存储更多。API是异步的,所以它不会阻塞你的主线程,而且它可以通过全局属性 caches
来访问。
如果你建立一个浏览器扩展,你有另一个选择来存储你的数据,我在进行扩展程序daily.dev时发现了它。如果你使用Mozilla的polyfill,它可以通过 chrome.storage
或 browser.storage
获得。确保在你的清单中申请一个存储权限以获得访问权。
有两种类型的存储选项:local和sync。local存储是不言而喻的,它的意思是不共享,保存在本地。sync存储是作为谷歌账户的一部分同步的,你在任何地方用同一个账户安装扩展,这个存储都会被同步。两者都有相同的API,所以如果需要的话,来回切换超级容易。它是异步存储,因此不会像 localStorage
这样阻塞主线程。不幸的是,我不能为这个存储选项创建一个演示,因为它需要一个浏览器扩展,但它的使用非常简单,几乎和 localStorage
一样。有关确切实现的更多信息,请参阅Chrome文档。
浏览器有许多选项可用于存储数据。根据Chrome团队的建议,我们的首选存储应该是IndexedDB,它是异步存储,有足够的空间来存储我们想要的任何东西。不鼓励使用 localStorage
,但它比 IndexedDB
更易于使用。Cookies是与服务器共享客户端状态的一种好方法,但通常用于身份验证。
如果你想创建具有可共享状态的页面,如搜索页面,请使用URL的查询字符串来存储这些信息。最后,如果你建立一个扩展,一定要阅读关于 chrome.storage
。
更多编程相关知识,请访问:编程视频!!
The above is the detailed content of Learn about the different storage types in browsers. For more information, please follow other related articles on the PHP Chinese website!