瞭解瀏覽器中的不同儲存型別

2020-11-24 21:01:36

在後端開發中,儲存是工作的常見部分。應用程式資料儲存在資料庫中,檔案儲存在物件儲存中,瞬態資料儲存在快取記憶體中……似乎存在無限種儲存任何型別資料的可能性。但是,資料儲存不僅限於後端,前端(瀏覽器)還具有許多儲存資料的選項。我們可以通過利用這種儲存方式來提升我們的應用效能,儲存使用者的偏好,在多個對談,甚至不同的計算機上保持應用狀態。

在本文中,我們將通過不同的可能性在瀏覽器中儲存資料。我們將涵蓋每種方法的三個用例,以掌握其利弊。最後,你將能夠決定什麼儲存是最適合你的用例。

讓我們開始吧!

localStorage API

localStorage 是瀏覽器中最受歡迎的儲存選項之一,也是許多開發人員的首選。資料跨對談儲存,從不與伺服器共用,並且可用於同一協定和域下的所有頁面。儲存空間限制為〜5MB。

令人驚訝的是,谷歌Chrome團隊並不建議使用這個選項,因為它遮蔽了主執行緒,而且web workers和service workers無法存取。他們推出了一個實驗:KV Storage,作為一個更好的版本,但這只是一個試驗,似乎還沒有任何進展。

localStorage API 可作為 window.localStorage 使用,並且只能儲存UTF-16字串。在將資料儲存到 localStorage 之前,我們必須確保將其轉換為字串。主要的三個功能是:

  • setItem('key', 'value')
  • getItem('key')
  • removeItem('key')

它們都是同步的,因此使用起來很簡單,但是它們會阻塞主執行緒。

值得一提的是,localStorage 有一個稱為 sessionStorage 的雙胞胎。唯一的區別是,儲存在 sessionStorage 中的資料將僅持續當前對談,但API相同。

這個太簡單了,相信大家都用過。

IndexedDB API

IndexedDB是瀏覽器中的現代儲存解決方案。它可以儲存大量的結構化資料,甚至檔案和Blob。和每一個資料庫一樣,IndexedDB對資料進行索引,以便高效地執行查詢。使用IndexedDB比較複雜,我們必須建立一個資料庫,表,並使用事務。

localStorage 相比,IndexedDB需要更多程式碼。在例子中,我使用了原生API與Promise包裝器,但我強烈建議使用第三方庫來幫助你。我推薦的是localForage,因為它使用了同樣的 localStorage API,但實現方式是逐步增強的,也就是說,如果你的瀏覽器支援IndexedDB,就會使用它;如果不支援,就會退回到 localStorage

讓我們來編寫程式碼,前往我們的使用者偏好範例吧!

<input type="checkbox" id="darkTheme" name="darkTheme" onclick='onChange(this);'>
<label for="darkTheme">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;
  });

效果

1.gif

idb 是我們使用的Promise包裝器,而不是使用基於事件的低階API。首先要注意的是,對資料庫的每次存取都是非同步的,這意味著我們不會阻塞主執行緒,與 localStorage 相比,這是一個主要優勢。

我們需要開啟與資料庫的連線,以便在整個應用程式中都可以使用它進行讀寫。我們給資料庫起一個名字 my-db,一個模式版本 1,以及一個更新函數,以在版本之間應用更改,這與資料庫遷移非常相似。我們的資料庫架構很簡單:只有一個object store preferences。object store 等效於SQL表,要寫入或讀取資料庫,必須使用事務,這是使用IndexedDB的乏味部分。看一下演示中新的 saveload 功能。

毫無疑問,IndexedDB具有更多的開銷,並且與 localStorage 相比,學習曲線更陡峭。對於鍵值的情況,使用 localStorage 或第三方庫可能更有意義,它們將幫助我們提高效率。

<p id="loading">loading...</p>
<ul id="list">
</ul>
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!');
  });

效果

2.png

你可以在此資料庫中儲存數百兆甚至更多。您可以將所有Pokémon儲存在IndexedDB中,並使其離線甚至建立索引!這絕對是用於儲存應用程式資料的一種選擇。

我跳過了第三個範例的實現,因為與 localStorage 相比,IndexedDB在這種情況下沒有任何區別。即使使用 IndexedDB,使用者仍然不會與他人分享所選頁面,也不會將其作為書籤供將來使用。它們都不適合這個用例。

Cookies

使用cookies是一種獨特的儲存方式,這是唯一的與伺服器共用的儲存方式。Cookies作為每次請求的一部分被傳送。它可以是當使用者瀏覽我們的應用程式中的頁面或當使用者傳送Ajax請求時。這樣我們就可以在使用者端和伺服器之間建立一個共用狀態,也可以在不同子域的多個應用程式之間共用狀態。本文中介紹的其他儲存選項無法實現。需要注意的是:每個請求都會傳送 cookie,這意味著我們必須保持 cookie 較小,以保持適當的請求大小。

Cookies的最常見用途是身份驗證,這不在本文的討論範圍之內。就像 localStorage 一樣,cookie只能儲存字串。這些cookie被連線成一個以分號分隔的字串,並在請求的cookie頭中傳送。你可以為每個cookie設定很多屬性,比如過期、允許的域名、允許的頁面等等。

在例子中,我展示瞭如何通過使用者端來操作cookie,但也可以在你的伺服器端應用程式中改變它們。

<input type="checkbox" id="darkTheme" name="darkTheme" onclick='onChange(this);'>
<label for="darkTheme">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;

效果還是跟前面一樣

3.gif

將使用者的喜好儲存在cookie中,如果伺服器能夠以某種方式利用它,就可以很好地滿足使用者的需求。例如,在主題用例中,伺服器可以交付相關的CSS檔案,並減少潛在的捆綁大小(在我們進行伺服器端渲染的情況下)。另一個用例可能是在沒有資料庫的情況下,在多個子域應用之間共用這些偏好。

用JavaScript讀寫cookie並不像您想象的那麼簡單。要儲存新的cookie,您需要設定 document.cookie ——在上面的範例中檢視 save 函數。我設定了 dark_theme cookie,並給它新增了一個 max-age 屬性,以確保它在關閉標籤時不會過期。另外,我新增 SameSiteSecure 屬性。這些都是必要的,因為CodePen使用iframe來執行這些例子,但在大多數情況下你並不需要它們。讀取一個cookie需要解析cookie字串。

Cookie字串如下所示:

key1=value1;key2=value2;key3=value3

因此,首先,我們必須用分號分隔字串。現在,我們有一個形式為 key1=value1 的Cookie陣列,所以我們需要在陣列中找到正確的元素。最後,我們將等號分開並獲得新陣列中的最後一個元素。有點繁瑣,但一旦你實現了 getCookie 函數(或從我的例子中複製它:P),你就可以忘記它。

將應用程式資料儲存在cookie中可能是個壞主意!它將大大增加請求的大小,並降低應用程式效能。此外,伺服器無法從這些資訊中獲益,因為它是資料庫中已有資訊的陳舊版本。如果你使用cookies,請確保它們很小。

分頁範例也不適合cookie,就像 localStorageIndexedDB 一樣。當前頁面是我們想要與他人共用的臨時狀態,這些方法都無法實現它。

URL storage

URL本身並不是儲存裝置,但它是建立可共用狀態的好方法。實際上,這意味著將查詢引數新增到當前URL中,這些引數可用於重新建立當前狀態。最好的例子是搜尋查詢和過濾器。如果我們在CSS-Tricks上搜尋術語flexbox,則URL將更新為https://css-tricks.com/?s=fle...。看看我們使用URL後,分享搜尋查詢有多簡單?另一個好處是,你只需點選重新整理按鈕,就可以獲得更新的查詢結果,甚至可以將其收藏。

我們只能在URL中儲存字串,它的最大長度是有限的,所以我們沒有那麼多的空間。我們將不得不保持我們的狀態小,沒有人喜歡又長又嚇人的網址。

同樣,CodePen使用iframe執行範例,因此您看不到URL實際更改。不用擔心,因為所有的碎片都在那裡,所以你可以在任何你想要的地方使用它。

<input type="checkbox" id="darkTheme" name="darkTheme" onclick='onChange(this);'>
<label for="darkTheme">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;

效果還是一樣

4.gif

我們可以通過 window.location.search 存取查詢字串,幸運的是,可以使用 URLSearchParams 類對其進行解析,無需再應用任何複雜的字串解析。當我們想讀取當前值時,可以使用 get 函數,當我們想寫時,可以使用 set。僅設定值是不夠的,我們還需要更新URL。這可以使用 history.pushStatehistory.replaceState 來完成,取決於我們想要完成的行為。

我不建議將使用者的偏好儲存在URL中,因為我們必須將這個狀態新增到使用者存取的每一個URL中,而且我們無法保證;例如,如果使用者點選了谷歌搜尋的連結。

就像Cookie一樣,由於空間太小,我們無法在URL中儲存應用程式資料。而且即使我們真的設法儲存它,網址也會很長,而且不吸引人點選。可能看起來像是釣魚攻擊的一種。

<p>Select page:</p>
<p id="pages">
  <button onclick="updatePage(0)">0</button>
  <button onclick="updatePage(1)">1</button>
  <button onclick="updatePage(3)">3</button>
  <button onclick="updatePage(4)">4</button>
  <button onclick="updatePage(5)">5</button>
</p>
<ul id="list">
</ul>
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);

效果

5.gif

就像我們的分頁例子一樣,臨時應用狀態是最適合URL查詢字串的。同樣,你無法看到URL的變化,但每次點選一個頁面時,URL都會以 ?page=x 查詢引數更新。當網頁載入時,它會查詢這個查詢引數,並相應地獲取正確的頁面。現在,我們可以把這個網址分享給我們的朋友,讓他們可以享受我們最喜歡的神奇寶貝。

Cache API

Cache API是網路級的儲存,它用於快取網路請求及其響應。Cache API非常適合service worker,service worker可以攔截每一個網路請求,使用 Cache API 它可以輕鬆地快取這兩個請求。service worker也可以將現有的快取項作為網路響應返回,而不是從伺服器上獲取。這樣,您可以減少網路負載時間,並使你的應用程式即使處於離線狀態也能正常工作。最初,它是為service worker建立的,但在現代瀏覽器中,Cache API也可以在視窗、iframe和worker上下文中使用。這是一個非常強大的API,可以極大地改善應用的使用者體驗。

就像IndexedDB一樣,Cache API的儲存不受限制,您可以儲存數百兆位元組,如果需要甚至可以儲存更多。API是非同步的,所以它不會阻塞你的主執行緒,而且它可以通過全域性屬性 caches 來存取。

Browser extension

如果你建立一個瀏覽器擴充套件,你有另一個選擇來儲存你的資料,我在進行擴充套件程式daily.dev時發現了它。如果你使用Mozilla的polyfill,它可以通過 chrome.storagebrowser.storage 獲得。確保在你的清單中申請一個儲存許可權以獲得存取權。

有兩種型別的儲存選項:local和sync。local儲存是不言而喻的,它的意思是不共用,儲存在本地。sync儲存是作為谷歌賬戶的一部分同步的,你在任何地方用同一個賬戶安裝擴充套件,這個儲存都會被同步。兩者都有相同的API,所以如果需要的話,來回切換超級容易。它是非同步儲存,因此不會像 localStorage 這樣阻塞主執行緒。不幸的是,我不能為這個儲存選項建立一個演示,因為它需要一個瀏覽器擴充套件,但它的使用非常簡單,幾乎和 localStorage 一樣。有關確切實現的更多資訊,請參閱Chrome檔案。

結束

瀏覽器有許多選項可用於儲存資料。根據Chrome團隊的建議,我們的首選儲存應該是IndexedDB,它是非同步儲存,有足夠的空間來儲存我們想要的任何東西。不鼓勵使用 localStorage,但它比 IndexedDB 更易於使用。Cookies是與伺服器共用使用者端狀態的一種好方法,但通常用於身份驗證。

如果你想建立具有可共用狀態的頁面,如搜尋頁面,請使用URL的查詢字串來儲存這些資訊。最後,如果你建立一個擴充套件,一定要閱讀關於 chrome.storage

更多程式設計相關知識,請存取:!!

以上就是了解瀏覽器中的不同儲存型別的詳細內容,更多請關注TW511.COM其它相關文章!