客戶端儲存

現代 Web 瀏覽器支援多種方式,讓網站在使用者計算機上儲存資料 — 經過使用者許可 — 然後在需要時檢索資料。這使您能夠持久化資料以進行長期儲存、儲存網站或文件以供離線使用、保留使用者對您網站的特定設定等等。本文介紹了這些工作原理的基礎知識。

先決條件 JavaScript 基礎知識(參見 入門構建塊JavaScript 物件),客戶端 API 基礎知識
目標 學習如何使用客戶端儲存 API 儲存應用程式資料。

客戶端儲存?

在 MDN 學習區域的其他地方,我們討論了 靜態網站動態網站 之間的區別。大多數主要的現代網站都是動態的 - 它們使用某種資料庫(伺服器端儲存)在伺服器上儲存資料,然後執行 伺服器端 程式碼來檢索所需資料,將其插入靜態頁面模板,並將生成的 HTML 提供給客戶端以供使用者的瀏覽器顯示。

客戶端儲存的工作原理類似,但用途不同。它包含 JavaScript API,允許您將資料儲存在客戶端(即使用者機器上),然後在需要時檢索它。這有許多不同的用途,例如

  • 個性化網站偏好(例如,顯示使用者選擇的自定義小部件、配色方案或字型大小)。
  • 持久化以前的網站活動(例如,儲存來自先前會話的購物車內容,記住使用者是否以前登入過)。
  • 在本地儲存資料和資產,以便網站下載速度更快(並且可能成本更低),或者可以在沒有網路連線的情況下使用。
  • 在本地儲存 Web 應用程式生成的文件以供離線使用

通常客戶端儲存和伺服器端儲存一起使用。例如,您可以下載一批音樂檔案(可能用於 Web 遊戲或音樂播放器應用程式),將它們儲存在客戶端資料庫中,並在需要時播放它們。使用者只需下載一次音樂檔案 - 在後續訪問中,它們將從資料庫中檢索。

注意:使用客戶端儲存 API 可以儲存的資料量有限(可能是每個 API 以及累積的);確切限制因瀏覽器而異,並且可能基於使用者設定。有關更多資訊,請參見 瀏覽器儲存配額和驅逐標準

老方法:Cookie

客戶端儲存的概念已經存在很長時間了。自網路的早期階段以來,網站一直使用 Cookie 來儲存資訊以個性化使用者在網站上的體驗。它們是 Web 上最常使用的客戶端儲存的早期形式。

如今,有更簡單的機制可用於儲存客戶端資料,因此我們不會在本文中教您如何使用 Cookie。但是,這並不意味著 Cookie 在現代網路中完全沒有用 - 它們仍然經常用於儲存與使用者個性化和狀態相關的資料,例如會話 ID 和訪問令牌。有關 Cookie 的更多資訊,請參見我們的 使用 HTTP Cookie 文章。

新方法:Web 儲存和 IndexedDB

我們上面提到的“更容易”的功能如下

  • Web 儲存 API 提供了一種機制來儲存和檢索較小的資料項,這些資料項由名稱和相應的價值組成。這在您只需要儲存一些簡單資料時很有用,例如使用者名稱、是否登入、螢幕背景要使用什麼顏色等。
  • IndexedDB API 為瀏覽器提供了一個完整的資料庫系統,用於儲存複雜資料。這可以用於從完整的客戶記錄集到甚至音訊或影片檔案等複雜資料型別等各種事物。

您將在下面詳細瞭解這些 API。

快取 API

Cache API 旨在儲存對特定請求的 HTTP 響應,並且非常適合執行諸如在離線狀態下儲存網站資產之類的操作,以便該網站可以在沒有網路連線的情況下使用。快取通常與 Service Worker API 結合使用,儘管它不必如此。

快取和 Service Worker 的使用是一個高階主題,我們不會在本文中詳細介紹,但我們將在下面的 離線資產儲存 部分中展示一個示例。

儲存簡單資料 — Web 儲存

Web 儲存 API 非常易於使用 - 您儲存簡單的資料名稱/值對(僅限於字串、數字等),並在需要時檢索這些值。

基本語法

讓我們向您展示如何操作

  1. 首先,訪問我們位於 GitHub 上的 Web 儲存空白模板(在新標籤頁中開啟)。
  2. 開啟瀏覽器開發者工具的 JavaScript 控制檯。
  3. 您所有 Web 儲存資料都包含在瀏覽器內的兩個類似物件的結構中:sessionStoragelocalStorage。第一個在瀏覽器開啟時保留資料(關閉瀏覽器時資料會丟失),第二個即使在關閉瀏覽器並重新開啟後也會保留資料。我們將在本文中使用第二個,因為它通常更有用。 Storage.setItem() 方法允許您在儲存中儲存資料項 - 它接受兩個引數:專案的名稱和它的值。嘗試在您的 JavaScript 控制檯中鍵入以下內容(如果需要,將值更改為您自己的姓名!)
    js
    localStorage.setItem("name", "Chris");
    
  4. Storage.getItem() 方法接受一個引數 - 您要檢索的資料項的名稱 - 並返回該項的值。現在將以下行鍵入到您的 JavaScript 控制檯中
    js
    let myName = localStorage.getItem("name");
    myName;
    
    鍵入第二行後,您應該看到 myName 變數現在包含 name 資料項的值。
  5. Storage.removeItem() 方法接受一個引數 - 您要刪除的資料項的名稱 - 並從 Web 儲存中刪除該項。在您的 JavaScript 控制檯中鍵入以下行
    js
    localStorage.removeItem("name");
    myName = localStorage.getItem("name");
    myName;
    
    第三行現在應該返回 null - name 專案不再存在於 Web 儲存中。

資料持久化!

Web 儲存的一個關鍵特性是資料在頁面載入之間(甚至在瀏覽器關閉的情況下,在 localStorage 的情況下)持久化。讓我們來看看它的實際應用。

  1. 再次開啟我們的 Web 儲存空白模板,但這次在與您開啟本教程的瀏覽器不同的瀏覽器中開啟!這樣更容易處理。
  2. 在瀏覽器的 JavaScript 控制檯中鍵入以下行
    js
    localStorage.setItem("name", "Chris");
    let myName = localStorage.getItem("name");
    myName;
    
    您應該看到 name 專案返回。
  3. 現在關閉瀏覽器,然後再次開啟它。
  4. 再次輸入以下行
    js
    let myName = localStorage.getItem("name");
    myName;
    
    您應該看到該值仍然可用,即使瀏覽器已經關閉並重新開啟。

每個域的獨立儲存

每個域(瀏覽器中載入的每個單獨的 Web 地址)都有一個單獨的資料儲存。您會發現,如果您載入兩個網站(例如 google.com 和 amazon.com)並在其中一個網站上嘗試儲存專案,它將無法在另一個網站上使用。

這是有道理的 - 您可以想象,如果網站能夠看到彼此的資料,將會出現哪些安全問題!

更復雜的示例

讓我們透過編寫一個工作示例來應用這些新獲得的知識,讓您瞭解 Web 儲存的使用方式。我們的示例將允許您輸入名稱,之後頁面將更新以向您提供個性化的問候。這種狀態也將跨頁面/瀏覽器重新載入持久化,因為名稱儲存在 Web 儲存中。

您可以在 personal-greeting.html 中找到示例 HTML - 它包含一個帶有標題、內容和頁尾以及用於輸入您的名稱的表單的網站。

A Screenshot of a website that has a header, content and footer sections. The header has a welcome text to the left-hand side and a button labelled 'forget' to the right-hand side. The content has an heading followed by a two paragraphs of dummy text. The footer reads 'Copyright nobody. Use the code as you like'.

讓我們逐步構建示例,這樣您就可以理解它是如何工作的。

  1. 首先,在計算機上的新目錄中製作我們 personal-greeting.html 檔案的本地副本。
  2. 接下來,請注意我們的 HTML 如何引用一個名為 index.js 的 JavaScript 檔案,並使用類似 <script src="index.js" defer></script> 的行。我們需要建立它並將我們的 JavaScript 程式碼寫入其中。在與您的 HTML 檔案相同的目錄中建立一個 index.js 檔案。
  3. 我們將從建立對我們需要在此示例中操作的所有 HTML 特性進行引用開始 - 我們將它們全部建立為常量,因為這些引用在應用程式的生命週期中不需要更改。將以下行新增到您的 JavaScript 檔案中
    js
    // create needed constants
    const rememberDiv = document.querySelector(".remember");
    const forgetDiv = document.querySelector(".forget");
    const form = document.querySelector("form");
    const nameInput = document.querySelector("#entername");
    const submitBtn = document.querySelector("#submitname");
    const forgetBtn = document.querySelector("#forgetname");
    
    const h1 = document.querySelector("h1");
    const personalGreeting = document.querySelector(".personal-greeting");
    
  4. 接下來,我們需要包含一個小型的事件監聽器,以阻止表單在按下提交按鈕時實際提交自身,因為這不是我們想要的行為。在您之前的程式碼下方新增此程式碼段
    js
    // Stop the form from submitting when a button is pressed
    form.addEventListener("submit", (e) => e.preventDefault());
    
  5. 現在我們需要新增一個事件監聽器,它的處理程式函式將在單擊“說你好”按鈕時執行。註釋詳細說明了每個部分的作用,但實質上,我們在這裡將使用者輸入文字輸入框中的名稱儲存到 Web 儲存中,使用 setItem(),然後執行一個名為 nameDisplayCheck() 的函式,該函式將處理更新實際網站文字。將其新增到程式碼的底部
    js
    // run function when the 'Say hello' button is clicked
    submitBtn.addEventListener("click", () => {
      // store the entered name in web storage
      localStorage.setItem("name", nameInput.value);
      // run nameDisplayCheck() to sort out displaying the personalized greetings and updating the form display
      nameDisplayCheck();
    });
    
  6. 在這一點上,我們還需要一個事件處理程式,以便在單擊“忘記”按鈕時執行一個函式 - 這僅在單擊“說你好”按鈕後顯示(兩個表單狀態來回切換)。在此函式中,我們使用 removeItem() 從 Web 儲存中刪除 name 專案,然後再次執行 nameDisplayCheck() 來更新顯示。將其新增到底部
    js
    // run function when the 'Forget' button is clicked
    forgetBtn.addEventListener("click", () => {
      // Remove the stored name from web storage
      localStorage.removeItem("name");
      // run nameDisplayCheck() to sort out displaying the generic greeting again and updating the form display
      nameDisplayCheck();
    });
    
  7. 現在是時候定義 nameDisplayCheck() 函式本身了。在這裡,我們檢查 name 專案是否已儲存在 Web 儲存中,方法是將 localStorage.getItem('name') 用作條件測試。如果已儲存名稱,此呼叫將評估為 true;如果沒有,此呼叫將評估為 false。如果呼叫評估為 true,我們將顯示個性化問候,顯示錶單的“忘記”部分,並隱藏表單的“說你好”部分。如果呼叫評估為 false,我們將顯示通用問候,並執行相反的操作。同樣,將以下程式碼放在底部
    js
    // define the nameDisplayCheck() function
    function nameDisplayCheck() {
      // check whether the 'name' data item is stored in web Storage
      if (localStorage.getItem("name")) {
        // If it is, display personalized greeting
        const name = localStorage.getItem("name");
        h1.textContent = `Welcome, ${name}`;
        personalGreeting.textContent = `Welcome to our website, ${name}! We hope you have fun while you are here.`;
        // hide the 'remember' part of the form and show the 'forget' part
        forgetDiv.style.display = "block";
        rememberDiv.style.display = "none";
      } else {
        // if not, display generic greeting
        h1.textContent = "Welcome to our website ";
        personalGreeting.textContent =
          "Welcome to our website. We hope you have fun while you are here.";
        // hide the 'forget' part of the form and show the 'remember' part
        forgetDiv.style.display = "none";
        rememberDiv.style.display = "block";
      }
    }
    
  8. 最後但並非最不重要的一點是,我們需要在頁面載入時執行 nameDisplayCheck() 函式。如果我們不這樣做,那麼個性化問候將無法在頁面重新載入之間持久化。將以下內容新增到程式碼的底部
    js
    nameDisplayCheck();
    

您的示例已完成 - 做得好!現在剩下的就是儲存程式碼並在瀏覽器中測試您的 HTML 頁面。您可以在 此處檢視我們的已完成版本線上執行

注意:使用 Web 儲存 API 中還有一個稍微更復雜的示例可供探索。

注意:在我們已完成版本原始碼中的 <script src="index.js" defer></script> 行中,defer 屬性指定 <script> 元素的內容將不會在頁面載入完成後執行。

儲存複雜資料 — IndexedDB

IndexedDB API(有時縮寫為 IDB)是瀏覽器中可用的完整資料庫系統,您可以在其中儲存複雜的相關資料,其型別不受字串或數字等簡單值的限制。您可以在 IndexedDB 例項中儲存影片、影像,以及幾乎所有其他內容。

IndexedDB API 允許您建立一個數據庫,然後在該資料庫中建立物件儲存。物件儲存類似於關係資料庫中的表,每個物件儲存可以包含多個物件。要詳細瞭解 IndexedDB API,請參見 使用 IndexedDB

然而,這也有代價:IndexedDB 比 Web 儲存 API 複雜得多。在本節中,我們只會觸及它功能的表面,但我們會提供足夠的知識讓你入門。

使用筆記儲存示例進行操作

在這裡,我們將引導你完成一個示例,讓你能夠在瀏覽器中儲存筆記,並在需要時檢視和刪除它們。我們將引導你構建它,並隨著我們進行解釋 IDB 的最基本部分。

該應用程式看起來像這樣

IndexDB notes demo screenshot with 4 sections. The first section is the header. The second section lists all the notes that have been created. It has two notes, each with a delete button. A third section is a form with 2 input fields for 'Note title' and 'Note text' and a button labeled 'Create new note'. The bottom section footer reads 'Copyright nobody. Use the code as you like'.

每個筆記都有一個標題和一些正文文字,每個都可以單獨編輯。我們將在下面介紹的 JavaScript 程式碼包含詳細的註釋,以幫助你理解正在發生的事情。

入門

  1. 首先,將我們的 index.htmlstyle.cssindex-start.js 檔案的本地副本複製到本地機器上的新目錄中。
  2. 檢視這些檔案。你會看到 HTML 定義了一個網站,包含頁首和頁尾,以及一個主要內容區域,其中包含用於顯示筆記的位置,以及一個用於將新筆記輸入資料庫的表單。CSS 提供了一些樣式,使之更清楚地瞭解正在發生的事情。JavaScript 檔案包含五個已宣告的常量,包含指向 <ul> 元素的引用,筆記將在其中顯示,標題和正文 <input> 元素,<form> 本身,以及 <button>
  3. 將你的 JavaScript 檔案重新命名為 index.js。你現在可以開始向其中新增程式碼了。

資料庫初始設定

現在讓我們看看我們首先要做什麼,才能實際設定一個數據庫。

  1. 在常量宣告下方,新增以下行
    js
    // Create an instance of a db object for us to store the open database in
    let db;
    
    在這裡,我們聲明瞭一個名為 db 的變數——它將在稍後用於儲存代表我們資料庫的物件。我們將在幾個地方使用它,因此我們已在此處將其宣告為全域性變數,以簡化操作。
  2. 接下來,新增以下內容
    js
    // Open our database; it is created if it doesn't already exist
    // (see the upgradeneeded handler below)
    const openRequest = window.indexedDB.open("notes_db", 1);
    
    此行建立一個請求,以開啟名為 notes_db 的資料庫的版本 1。如果它還不存在,則隨後程式碼將為你建立它。你會看到這種請求模式在 IndexedDB 中經常使用。資料庫操作需要時間。你不希望在等待結果時掛起瀏覽器,因此資料庫操作是 非同步的,這意味著它們不會立即發生,而是在將來的某個時間點發生,並且在它們完成後你會收到通知。為了在 IndexedDB 中處理這個問題,你需要建立一個請求物件(它可以被稱為任何你喜歡的名稱——我們在這裡將其稱為 openRequest,因此很明顯它是用來做什麼的)。然後,你可以使用事件處理程式在請求完成、失敗等情況下執行程式碼,你將在下面看到它的使用。

    注意:版本號很重要。如果你想升級你的資料庫(例如,透過更改表結構),你需要再次執行程式碼,並使用更大的版本號、在 upgradeneeded 處理程式(見下文)中指定的不同模式等。在本教程中,我們不會介紹如何升級資料庫。

  3. 現在,在之前的新增內容下方新增以下事件處理程式
    js
    // error handler signifies that the database didn't open successfully
    openRequest.addEventListener("error", () =>
      console.error("Database failed to open"),
    );
    
    // success handler signifies that the database opened successfully
    openRequest.addEventListener("success", () => {
      console.log("Database opened successfully");
    
      // Store the opened database object in the db variable. This is used a lot below
      db = openRequest.result;
    
      // Run the displayData() function to display the notes already in the IDB
      displayData();
    });
    
    error 事件處理程式將在系統返回說請求失敗時執行。這使你可以對這個問題做出響應。在我們的示例中,我們只是將一條訊息列印到 JavaScript 控制檯。當請求成功返回時,success 事件處理程式將執行,這意味著資料庫已成功開啟。如果是這種情況,表示已開啟的資料庫的物件將出現在 openRequest.result 屬性中,使我們能夠操作資料庫。我們將它儲存在我們之前建立的 db 變數中,以便以後使用。我們還會執行一個名為 displayData() 的函式,它將在資料庫中的 <ul> 中顯示資料。我們現在執行它,以便在頁面載入時立即顯示資料庫中已有的筆記。你將在後面看到對 displayData() 的定義。
  4. 最後,在本節中,我們將新增可能最重要的事件處理程式,用於設定資料庫:upgradeneeded。如果資料庫尚未設定,或者如果資料庫以比現有儲存的資料庫更大的版本號(在執行升級時)開啟,則此處理程式會執行。在之前的處理程式下方新增以下程式碼
    js
    // Set up the database tables if this has not already been done
    openRequest.addEventListener("upgradeneeded", (e) => {
      // Grab a reference to the opened database
      db = e.target.result;
    
      // Create an objectStore in our database to store notes and an auto-incrementing key
      // An objectStore is similar to a 'table' in a relational database
      const objectStore = db.createObjectStore("notes_os", {
        keyPath: "id",
        autoIncrement: true,
      });
    
      // Define what data items the objectStore will contain
      objectStore.createIndex("title", "title", { unique: false });
      objectStore.createIndex("body", "body", { unique: false });
    
      console.log("Database setup complete");
    });
    
    這是我們定義資料庫模式(結構)的地方;也就是說,它包含的列(或欄位)集。在這裡,我們首先從事件目標(e.target.result)的 result 屬性中獲取對現有資料庫的引用,它就是 request 物件。這等效於 success 事件處理程式中的 db = openRequest.result; 一行,但我們需要在這裡單獨執行此操作,因為 upgradeneeded 事件處理程式(如果需要)將在 success 事件處理程式之前執行,這意味著如果我們沒有執行此操作,db 值將不可用。然後,我們使用 IDBDatabase.createObjectStore() 在我們開啟的資料庫中建立一個名為 notes_os 的新物件儲存。這等效於傳統資料庫系統中的單個表。我們給它命名為 notes,並指定了一個名為 idautoIncrement 鍵欄位——在每個新記錄中,它將自動分配一個遞增的值——開發人員無需顯式設定它。作為鍵,id 欄位將用於唯一標識記錄,例如在刪除或顯示記錄時。我們還使用 IDBObjectStore.createIndex() 方法建立了另外兩個索引(欄位):title(它將包含每個筆記的標題),以及 body(它將包含筆記的正文文字)。

因此,隨著此資料庫模式的設定,當我們開始向資料庫新增記錄時,每個記錄都將以類似於以下物件的表示形式出現

json
{
  "title": "Buy milk",
  "body": "Need both cows milk and soy.",
  "id": 8
}

向資料庫新增資料

現在讓我們看看如何向資料庫新增記錄。這將透過我們頁面上的表單來完成。

在之前的事件處理程式下方,新增以下行,它設定了一個 submit 事件處理程式,當表單提交時(當提交 <button> 被按下,導致表單成功提交)執行一個名為 addData() 的函式

js
// Create a submit event handler so that when the form is submitted the addData() function is run
form.addEventListener("submit", addData);

現在讓我們定義 addData() 函式。在之前的那行下方新增以下內容

js
// Define the addData() function
function addData(e) {
  // prevent default - we don't want the form to submit in the conventional way
  e.preventDefault();

  // grab the values entered into the form fields and store them in an object ready for being inserted into the DB
  const newItem = { title: titleInput.value, body: bodyInput.value };

  // open a read/write db transaction, ready for adding the data
  const transaction = db.transaction(["notes_os"], "readwrite");

  // call an object store that's already been added to the database
  const objectStore = transaction.objectStore("notes_os");

  // Make a request to add our newItem object to the object store
  const addRequest = objectStore.add(newItem);

  addRequest.addEventListener("success", () => {
    // Clear the form, ready for adding the next entry
    titleInput.value = "";
    bodyInput.value = "";
  });

  // Report on the success of the transaction completing, when everything is done
  transaction.addEventListener("complete", () => {
    console.log("Transaction completed: database modification finished.");

    // update the display of data to show the newly added item, by running displayData() again.
    displayData();
  });

  transaction.addEventListener("error", () =>
    console.log("Transaction not opened due to error"),
  );
}

這很複雜;讓我們分解一下,我們

  • 在事件物件上執行 Event.preventDefault(),以阻止表單實際以傳統方式提交(這會導致頁面重新整理,並破壞體驗)。
  • 建立一個物件,表示要輸入資料庫的記錄,並使用來自表單輸入的值填充它。請注意,我們無需顯式包含 id 值——正如我們之前所解釋的,這是自動填充的。
  • 使用 IDBDatabase.transaction() 方法,對 notes_os 物件儲存執行 readwrite 事務。此事務物件使我們能夠訪問物件儲存,以便我們可以對其執行一些操作,例如新增新記錄。
  • 使用 IDBTransaction.objectStore() 方法訪問物件儲存,並將結果儲存到 objectStore 變數中。
  • 使用 IDBObjectStore.add() 將新記錄新增到資料庫。這將建立一個請求物件,與我們之前看到的相同。
  • requesttransaction 物件新增一堆事件處理程式,以在生命週期的關鍵點執行程式碼。請求成功後,我們將清除表單輸入,以便輸入下一條筆記。事務完成後,我們將再次執行 displayData() 函式,以更新頁面上筆記的顯示。

顯示資料

我們已經在程式碼中兩次引用了 displayData(),因此我們最好定義它。在之前函式定義下方新增以下內容

js
// Define the displayData() function
function displayData() {
  // Here we empty the contents of the list element each time the display is updated
  // If you didn't do this, you'd get duplicates listed each time a new note is added
  while (list.firstChild) {
    list.removeChild(list.firstChild);
  }

  // Open our object store and then get a cursor - which iterates through all the
  // different data items in the store
  const objectStore = db.transaction("notes_os").objectStore("notes_os");
  objectStore.openCursor().addEventListener("success", (e) => {
    // Get a reference to the cursor
    const cursor = e.target.result;

    // If there is still another data item to iterate through, keep running this code
    if (cursor) {
      // Create a list item, h3, and p to put each data item inside when displaying it
      // structure the HTML fragment, and append it inside the list
      const listItem = document.createElement("li");
      const h3 = document.createElement("h3");
      const para = document.createElement("p");

      listItem.appendChild(h3);
      listItem.appendChild(para);
      list.appendChild(listItem);

      // Put the data from the cursor inside the h3 and para
      h3.textContent = cursor.value.title;
      para.textContent = cursor.value.body;

      // Store the ID of the data item inside an attribute on the listItem, so we know
      // which item it corresponds to. This will be useful later when we want to delete items
      listItem.setAttribute("data-note-id", cursor.value.id);

      // Create a button and place it inside each listItem
      const deleteBtn = document.createElement("button");
      listItem.appendChild(deleteBtn);
      deleteBtn.textContent = "Delete";

      // Set an event handler so that when the button is clicked, the deleteItem()
      // function is run
      deleteBtn.addEventListener("click", deleteItem);

      // Iterate to the next item in the cursor
      cursor.continue();
    } else {
      // Again, if list item is empty, display a 'No notes stored' message
      if (!list.firstChild) {
        const listItem = document.createElement("li");
        listItem.textContent = "No notes stored.";
        list.appendChild(listItem);
      }
      // if there are no more cursor items to iterate through, say so
      console.log("Notes all displayed");
    }
  });
}

再次,讓我們分解一下

  • 首先,我們清空 <ul> 元素的內容,然後再用更新後的內容填充它。如果你沒有執行此操作,最終將會有大量的重複內容新增到每次更新中。
  • 接下來,我們使用 IDBDatabase.transaction()IDBTransaction.objectStore() 獲取對 notes_os 物件儲存的引用,就像我們在 addData() 中所做的那樣,只是我們在這裡將它們連結在一起,寫成一行程式碼。
  • 下一步是使用 IDBObjectStore.openCursor() 方法開啟遊標請求——這是一個可以用來遍歷物件儲存中記錄的構造。我們將 success 事件處理程式連結到此行的末尾,以使程式碼更簡潔——當遊標成功返回時,將執行處理程式。
  • 我們使用 const cursor = e.target.result 獲取對遊標本身(一個 IDBCursor 物件)的引用。
  • 接下來,我們檢查遊標是否包含來自資料儲存的記錄(if (cursor){ })——如果有,我們建立一個 DOM 片段,用來自記錄的資料填充它,並將其插入頁面(在 <ul> 元素內部)。我們還包括一個刪除按鈕,當單擊它時,將透過執行 deleteItem() 函式刪除該筆記,我們將在下一節中介紹它。
  • if 塊的末尾,我們使用 IDBCursor.continue() 方法將遊標推進到資料儲存中的下一條記錄,並再次執行 if 塊的內容。如果有另一條記錄要迭代,這將導致它被插入到頁面中,然後再次執行 continue(),依此類推。
  • 當沒有更多記錄可以迭代時,cursor 將返回 undefined,因此將執行 else 塊,而不是 if 塊。此塊檢查是否已將任何筆記插入到 <ul> 中——如果沒有,它將插入一條訊息,說明沒有儲存任何筆記。

刪除筆記

如上所述,當按下筆記的刪除按鈕時,該筆記將被刪除。這是透過 deleteItem() 函式實現的,該函式如下所示

js
// Define the deleteItem() function
function deleteItem(e) {
  // retrieve the name of the task we want to delete. We need
  // to convert it to a number before trying to use it with IDB; IDB key
  // values are type-sensitive.
  const noteId = Number(e.target.parentNode.getAttribute("data-note-id"));

  // open a database transaction and delete the task, finding it using the id we retrieved above
  const transaction = db.transaction(["notes_os"], "readwrite");
  const objectStore = transaction.objectStore("notes_os");
  const deleteRequest = objectStore.delete(noteId);

  // report that the data item has been deleted
  transaction.addEventListener("complete", () => {
    // delete the parent of the button
    // which is the list item, so it is no longer displayed
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
    console.log(`Note ${noteId} deleted.`);

    // Again, if list item is empty, display a 'No notes stored' message
    if (!list.firstChild) {
      const listItem = document.createElement("li");
      listItem.textContent = "No notes stored.";
      list.appendChild(listItem);
    }
  });
}
  • 此函式的第一部分需要一些解釋——我們使用 Number(e.target.parentNode.getAttribute('data-note-id')) 檢索要刪除的記錄的 ID——請記住,記錄的 ID 在 <li>data-note-id 屬性中儲存,當時它第一次顯示。但是,我們需要將該屬性透過全域性內建的 Number() 物件傳遞,因為它屬於字串資料型別,因此資料庫無法識別它,資料庫需要一個數字。
  • 然後,我們使用之前看到過的相同模式獲取對物件儲存的引用,並使用 IDBObjectStore.delete() 方法從資料庫中刪除該記錄,並將 ID 傳遞給它。
  • 當資料庫事務完成時,我們將從 DOM 中刪除筆記的 <li>,並再次進行檢查,以檢視 <ul> 是否現在為空,並在必要時插入一條筆記。

就是這樣!你的示例現在應該可以正常工作了。

如果你遇到問題,請隨時檢視我們的即時示例(也可以檢視原始碼)。

透過 IndexedDB 儲存複雜資料

正如我們上面提到的,IndexedDB 不僅可以用來儲存文字字串,還可以儲存幾乎任何你想要的東西,包括像影片或圖片 blob 這樣的複雜物件。實現起來也不比其他型別的資料複雜多少。

為了演示如何實現,我們編寫了另一個名為IndexedDB 影片儲存的示例(在這裡即時執行)。當你第一次執行這個示例時,它會從網路下載所有影片,將它們儲存在 IndexedDB 資料庫中,然後在 UI 中的<video> 元素內顯示這些影片。當你第二次執行它時,它會在資料庫中找到影片,然後從資料庫中獲取這些影片,並在顯示它們之前進行處理——這使得後續的載入速度更快,並且對頻寬的消耗更少。

讓我們看一下這個例子中最有趣的部分。我們不會看所有的程式碼——其中很多程式碼與前面的例子相似,並且程式碼都有很好的註釋。

  1. 在這個例子中,我們將要獲取的影片名稱儲存在一個物件陣列中。
    js
    const videos = [
      { name: "crystal" },
      { name: "elf" },
      { name: "frog" },
      { name: "monster" },
      { name: "pig" },
      { name: "rabbit" },
    ];
    
  2. 首先,在資料庫成功開啟後,我們執行一個init()函式。這個函式會遍歷不同的影片名稱,嘗試從videos資料庫中載入一個由每個名稱標識的記錄。如果在資料庫中找到了每個影片(透過檢視request.result是否為true來判斷——如果記錄不存在,它將是undefined),它的影片檔案(作為 blob 儲存)和影片名稱將直接傳遞給displayVideo()函式以將其放置在 UI 中。如果沒有找到,影片名稱將傳遞給fetchVideoFromNetwork()函式,以從網路中獲取影片。
    js
    function init() {
      // Loop through the video names one by one
      for (const video of videos) {
        // Open transaction, get object store, and get() each video by name
        const objectStore = db.transaction("videos_os").objectStore("videos_os");
        const request = objectStore.get(video.name);
        request.addEventListener("success", () => {
          // If the result exists in the database (is not undefined)
          if (request.result) {
            // Grab the videos from IDB and display them using displayVideo()
            console.log("taking videos from IDB");
            displayVideo(
              request.result.mp4,
              request.result.webm,
              request.result.name,
            );
          } else {
            // Fetch the videos from the network
            fetchVideoFromNetwork(video);
          }
        });
      }
    }
    
  3. 下面的程式碼片段摘自fetchVideoFromNetwork()內部——在這裡我們使用兩個單獨的fetch()請求來獲取影片的 MP4 和 WebM 版本。然後我們使用Response.blob()方法將每個響應的主體提取為一個 blob,從而得到可以稍後儲存和顯示的影片物件的表示。但是這裡我們有一個問題——這兩個請求都是非同步的,但我們只希望在兩個 promise 都完成之後才嘗試顯示或儲存影片。幸運的是,有一個內建方法可以處理這樣的問題——Promise.all()。它接受一個引數——將要檢查是否完成的所有單個 promise 的引用,並將它們放在一個數組中——並返回一個 promise,當所有單個 promise 都完成時,這個 promise 將完成。在這個 promise 的then()處理程式內部,我們像之前那樣呼叫displayVideo()函式來在 UI 中顯示影片,然後我們也呼叫storeVideo()函式將這些影片儲存到資料庫中。
    js
    // Fetch the MP4 and WebM versions of the video using the fetch() function,
    // then expose their response bodies as blobs
    const mp4Blob = fetch(`videos/${video.name}.mp4`).then((response) =>
      response.blob(),
    );
    const webmBlob = fetch(`videos/${video.name}.webm`).then((response) =>
      response.blob(),
    );
    
    // Only run the next code when both promises have fulfilled
    Promise.all([mp4Blob, webmBlob]).then((values) => {
      // display the video fetched from the network with displayVideo()
      displayVideo(values[0], values[1], video.name);
      // store it in the IDB using storeVideo()
      storeVideo(values[0], values[1], video.name);
    });
    
  4. 我們先看一下storeVideo()。這與你在前面示例中看到的將資料新增到資料庫中的模式非常相似——我們開啟一個readwrite事務,獲取對videos_os物件儲存的引用,建立一個表示要新增到資料庫的記錄的物件,然後使用IDBObjectStore.add()將其新增。
    js
    // Define the storeVideo() function
    function storeVideo(mp4, webm, name) {
      // Open transaction, get object store; make it a readwrite so we can write to the IDB
      const objectStore = db
        .transaction(["videos_os"], "readwrite")
        .objectStore("videos_os");
    
      // Add the record to the IDB using add()
      const request = objectStore.add({ mp4, webm, name });
    
      request.addEventListener("success", () =>
        console.log("Record addition attempt finished"),
      );
      request.addEventListener("error", () => console.error(request.error));
    }
    
  5. 最後,我們有displayVideo(),它建立了在 UI 中插入影片所需的 DOM 元素,然後將它們附加到頁面上。其中最有趣的部分是在下面顯示的——為了在<video>元素中實際顯示我們的影片 blob,我們需要使用URL.createObjectURL()方法建立物件 URL(指向儲存在記憶體中的影片 blob 的內部 URL)。完成之後,我們可以將物件 URL 設定為我們的<source>元素的src屬性的值,它就可以正常工作了。
    js
    // Define the displayVideo() function
    function displayVideo(mp4Blob, webmBlob, title) {
      // Create object URLs out of the blobs
      const mp4URL = URL.createObjectURL(mp4Blob);
      const webmURL = URL.createObjectURL(webmBlob);
    
      // Create DOM elements to embed video in the page
      const article = document.createElement("article");
      const h2 = document.createElement("h2");
      h2.textContent = title;
      const video = document.createElement("video");
      video.controls = true;
      const source1 = document.createElement("source");
      source1.src = mp4URL;
      source1.type = "video/mp4";
      const source2 = document.createElement("source");
      source2.src = webmURL;
      source2.type = "video/webm";
    
      // Embed DOM elements into page
      section.appendChild(article);
      article.appendChild(h2);
      article.appendChild(video);
      video.appendChild(source1);
      video.appendChild(source2);
    }
    

離線資產儲存

上面的例子已經展示瞭如何建立一個應用程式,它可以將大型資產儲存在 IndexedDB 資料庫中,從而避免重複下載它們。這已經是對使用者體驗的很大改善,但是仍然缺少一個東西——主要的 HTML、CSS 和 JavaScript 檔案仍然需要在每次訪問網站時下載,這意味著在沒有網路連線的情況下它將無法工作。

Firefox offline screen with an illustration of a cartoon character to the left-hand side holding a two-pin plug in its right hand and a two-pin socket in its left hand. On the right-hand side there is an Offline Mode message and a button labeled 'Try again'.

這就是服務工作者和密切相關的快取 API的用武之地。

服務工作者是一個 JavaScript 檔案,它在瀏覽器訪問某個特定來源(網站或特定域名的網站的一部分)時,會針對該來源進行註冊。註冊後,它可以控制該來源的可用頁面。它是透過坐在載入的頁面和網路之間,攔截針對該來源的網路請求來實現的。

當它攔截一個請求時,它可以對請求做任何你希望它做的事情(參見用例想法),但最典型的例子是將網路響應儲存到離線狀態,然後在響應請求時提供這些響應,而不是提供來自網路的響應。實際上,它允許你使網站完全離線工作。

快取 API 是另一種客戶端儲存機制,但也有一些不同——它旨在儲存 HTTP 響應,因此與服務工作者配合得很好。

服務工作者示例

讓我們看一個例子,讓你對它可能是什麼樣子有更深入的瞭解。我們建立了前面部分中看到的影片儲存示例的另一個版本——它在功能上是相同的,只是它還使用服務工作者將 HTML、CSS 和 JavaScript 儲存到快取 API 中,從而使示例可以在離線狀態下執行!

檢視帶有服務工作者的 IndexedDB 影片儲存即時執行,以及檢視原始碼

註冊服務工作者

首先需要注意的是,在主 JavaScript 檔案中添加了一些額外的程式碼(檢視index.js)。首先,我們進行一項功能檢測測試,以檢視serviceWorker成員是否在Navigator物件中可用。如果返回 true,那麼我們就知道至少服務工作者的基本功能是受支援的。在這裡,我們使用ServiceWorkerContainer.register()方法來註冊位於sw.js檔案中的服務工作者,使其可以控制與它位於同一目錄或子目錄中的頁面。當它的 promise 完成時,服務工作者被認為已註冊。

js
// Register service worker to control making site work offline
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register(
      "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js",
    )
    .then(() => console.log("Service Worker Registered"));
}

注意:給出的sw.js檔案的路徑是相對於站點來源的,而不是包含程式碼的 JavaScript 檔案。服務工作者位於https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js。來源是https://mdn.github.io,因此給出的路徑必須是/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js。如果你想將這個例子放在自己的伺服器上,你需要相應地改變它。這有點令人困惑,但出於安全原因,它必須這樣工作。

安裝服務工作者

下次訪問服務工作者控制下的任何頁面(例如,重新載入示例時),服務工作者就會針對該頁面進行安裝,這意味著它將開始控制該頁面。當這種情況發生時,一個install事件會針對服務工作者觸發;你可以在服務工作者本身內部編寫程式碼來響應安裝。

讓我們看一個例子,在sw.js檔案(服務工作者)中。你會看到,安裝監聽器是在self上註冊的。這個self關鍵字是用來從服務工作者檔案內部引用服務工作者的全域性範圍的。

install處理程式內部,我們使用ExtendableEvent.waitUntil()方法,該方法在事件物件上可用,用來表示瀏覽器在內部 promise 成功完成之前不應完成服務工作者的安裝。

這裡我們看到了快取 API 的實際應用。我們使用CacheStorage.open()方法開啟一個新的快取物件,可以在其中儲存響應(類似於 IndexedDB 物件儲存)。這個 promise 會用一個表示video-store快取的Cache物件完成。然後我們使用Cache.addAll()方法來獲取一系列資產,並將它們的響應新增到快取中。

js
self.addEventListener("install", (e) => {
  e.waitUntil(
    caches
      .open("video-store")
      .then((cache) =>
        cache.addAll([
          "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/",
          "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html",
          "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js",
          "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css",
        ]),
      ),
  );
});

現在就這些了,安裝完成。

響應後續請求

在服務工作者針對我們的 HTML 頁面註冊和安裝之後,以及所有相關的資產都新增到我們的快取中後,我們幾乎準備好了。還有一件事要做:編寫一些程式碼來響應後續的網路請求。

這就是sw.js中的第二部分程式碼所做的。我們在服務工作者的全域性範圍中添加了另一個監聽器,當fetch事件觸發時,它會執行處理程式函式。每當瀏覽器請求服務工作者註冊目錄中的資產時,就會發生這種情況。

在處理程式內部,我們首先記錄請求的資產的 URL。然後我們使用FetchEvent.respondWith()方法為請求提供自定義響應。

在這個程式碼塊內部,我們使用CacheStorage.match()檢查任何快取中是否可以找到匹配的請求(即與 URL 匹配)。如果找到匹配項,這個 promise 將用匹配的響應完成,否則將用undefined完成。

如果找到匹配項,我們將將其作為自定義響應返回。如果沒有找到,我們將從網路獲取響應,並將其返回。

js
self.addEventListener("fetch", (e) => {
  console.log(e.request.url);
  e.respondWith(
    caches.match(e.request).then((response) => response || fetch(e.request)),
  );
});

這就是我們的服務工作者的全部內容。你還可以用它做很多其他的事情——更多細節請檢視服務工作者手冊。非常感謝 Paul Kinlan 的文章將服務工作者和離線功能新增到你的 Web 應用程式,它啟發了這個例子。

離線測試示例

要測試我們的服務工作者示例,你需要載入它幾次,以確保它已安裝。安裝完成後,你可以:

  • 嘗試拔掉網路/關閉你的 Wi-Fi。
  • 如果你使用的是 Firefox,請選擇檔案 > 離線工作
  • 如果你使用的是 Chrome,請轉到開發者工具,然後選擇應用程式 > 服務工作者,然後勾選離線複選框。

如果你再次重新整理示例頁面,你應該仍然看到它正常載入。所有內容都儲存在離線狀態——頁面資產儲存在快取中,影片儲存在 IndexedDB 資料庫中。

總結

現在就這些了。我們希望你發現我們對客戶端儲存技術的概述很有用。

另請參見