这篇翻译不完整。请帮忙从英语翻译这篇文章

 

现代web浏览器提供了很多在用户电脑web客户端存放数据的方法 — 只要用户的允许 — 可以在它需要的时候被重新获得。这样能让你存留的数据长时间保存, 保存站点和文档在离线情况下使用, 保留你对其站点的个性化配置等等。本篇文章只解释它们工作的一些很基础的部分。

Prerequisites: JavaScript 基础 (查看 first steps, building blocks, JavaScript objects), the basics of Client-side APIs
Objective: 学习如何使用客户端存储 API 来存储应用数据。

客户端存储?

在其他的MDN学习中我们已经讨论过 静态网站(static sites) 和动态网站( dynamic sites)的区别。 大多数现代的web站点是动态的— 它们在服务端使用各种类型的数据库来存储数据(服务端存储), 之后通过运行服务端( server-side) 代码来重新获取需要的数据,把其数据插入到静态页面的模板中,并且生成出HTML渲染到用户浏览上。

客户端存储以相同的原理工作,但是在使用上有一些不同。它是由 JavaScript APIs 组成的因此允许你在客户端存储数据 (比如在用户的机器上),而且可以在需要的时候重新取得需要的数据。这有很多明显的用处,比如:

  • 个性化网站偏好(比如显示一个用户选择的窗口小部件,颜色主题,或者字体)。
  • 保存之前的站点行为 (比如从先前的session中获取购物车中的内容, 记住用户是否之前已经登陆过)。
  • 本地化保存数据和静态资源可以使一个站点更快(至少让资源变少)的下载, 甚至可以在网络失去链接的时候变得暂时可用。
  • 保存web已经生产的文档可以在离线状态下访问。

经常客户端和服务端存储是结合在一起使用的。例如,你可以从数据库中下载一个由网络游戏或音乐播放器应用程序使用的音乐文件,将它们存储在客户端数据库中,并按需要播放它们。用户只需下载音乐文件一次——在随后的访问中,它们将从数据库中检索。

 

注意:使用客户端存储 API 可以存储的数据量是有限的(可能是每个API单独的和累积的总量);具体的数量限制取决于浏览器,也可能基于用户设置。有关更多信息,获取更多信息,请参考浏览器存储限制和清理标准

传统方法:cookies

客户端存储的概念已经存在很长一段时间了。从早期的网络时代开始,网站就使用 cookies 来存储信息,以在网站上提供个性化的用户体验。它们是网络上最早最常用的客户端存储形式。
因为在那个年代,有许多问题——无论是从技术上的还是用户体验的角度——都是困扰着 cookies 的问题。这些问题非常重要,以至于当第一次访问一个网站时,欧洲居民会收到消息,告诉他们是否会使用 cookies 来存储关于他们的数据,而这是由一项被称为欧盟 Cookie 条例的欧盟法律导致的。

由于这些原因,我们不会在本文中教你如何使用cookie。毕竟它过时、存在各种安全问题,而且无法存储复杂数据,而且有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。
cookie的唯一优势是它们得到了非常旧的浏览器的支持,所以如果您的项目需要支持已经过时的浏览器(比如 Internet Explorer 8 或更早的浏览器),cookie可能仍然有用,但是对于大多数项目来说,您不需要再使用它们了。

为什么仍然有新创建的站点使用 cookies?这主要是因为开发人员的习惯,使用了仍然使用cookies的旧库,以及存在许多web站点,提供了过时的参考和培训材料来学习如何存储数据。

新流派:Web Storage 和 IndexedDB

现代浏览器有比使用 cookies 更简单、更有效的存储客户端数据的 API。

  • Web Storage API 提供了一种非常简单的语法,用于存储和检索较小的、由名称和相应值组成的数据项。当您只需要存储一些简单的数据时,比如用户的名字,用户是否登录,屏幕背景使用了什么颜色等等,这是非常有用的。
  • IndexedDB API 为浏览器提供了一个完整的数据库系统来存储复杂的数据。这可以用于存储从完整的用户记录到甚至是复杂的数据类型,如音频或视频文件。

您将在下面了解更多关于这些 API 的信息。

未来:Cache API

一些现代浏览器支持新的 Cache API。这个API是为存储特定HTTP请求的响应文件而设计的,它对于像存储离线网站文件这样的事情非常有用,这样网站就可以在没有网络连接的情况下使用。缓存通常与 Service Worker API 组合使用,尽管不一定非要这么做。
Cache 和 Service Workers 的使用是一个高级主题,我们不会在本文中详细讨论它,尽管我们将在下面的  离线文件存储 一节中展示一个简单的例子。

存储简单数据 — web storage

Web Storage API 非常容易使用 — 你只需存储简单的 键名/键值 对数据 (限制为字符串、数字等类型) 并在需要的时候检索其值。

基本语法

让我们来告诉你怎么做:

  1. 第一步,访问 GitHub 上的 web storage blank template (在新标签页打开此模板)。

  2. 打开你浏览器开发者工具的 JavaScript 控制台。

  3. 你所有的 web storage 数据都包含在浏览器内两个类似于对象的结构中: sessionStoragelocalStorage。 第一种方法,只要浏览器开着,数据就会一直保存 (关闭浏览器时数据会丢失) ,而第二种会一直保存数据,甚至到浏览器关闭又开启后也是这样。我们将在本文中使用第二种方法,因为它通常更有用。

  4. Storage.setItem() 方法允许您在存储中保存一个数据项——它接受两个参数:数据项的名字及其值。试着把它输入到你的JavaScript控制台(如果你愿意的话,可以把它的值改为你自己的名字!)

    localStorage.setItem('name','Chris');
  5. Storage.getItem() 方法接受一个参数——你想要检索的数据项的名称——并返回数据项的值。现在将这些代码输入到你的JavaScript控制台:

    var myName = localStorage.getItem('name');
    myName

    在输入第二行时,您应该会看到 myName变量现在包含name数据项的值。

  6. Storage.removeItem() 方法接受一个参数——你想要删除的数据项的名称——并从 web storage 中删除该数据项。在您的JavaScript控制台中输入以下几行:

    localStorage.removeItem('name');
    var myName = localStorage.getItem('name');
    myName

    第三行现在应该返回 null —  name 项已经不存在于 web storage 中。

数据会一直存在!

web storage 的一个关键特性是,数据在不同页面加载时都存在(甚至是当浏览器关闭后,对localStorage的而言)。让我们来看看这个:

  1. 再次打开我们的 Web Storage 空白模板,但是这次你要在不同的浏览器中打开这个教程!这样可以更容易处理。

  2. 在浏览器的 JavaScript 控制台中输入这几行:

    localStorage.setItem('name','Chris');
    var myName = localStorage.getItem('name');
    myName

    你应该看到 name 数据项返回。

  3. 现在关掉浏览器再把它打开。

  4. 再次输入下面几行:

    var myName = localStorage.getItem('name');
    myName

    你应该看到,尽管浏览器已经关闭,然后再次打开,但仍然可以使用该值。

为每个域名分离储存

每个域都有一个单独的数据存储区(每个单独的网址都在浏览器中加载). 你 会看到,如果你加载两个网站(例如google.com和amazon.com)并尝试将某个项目存储在一个网站上,该数据项将无法从另一个网站获取。

这是有道理的 - 你可以想象如果网站能够查看彼此的数据,就会出现安全问题!

更复杂的例子

让我们通过编写一个简单的工作示例来应用这些新发现的知识,让你了解如何使用网络存储。我们的示例将允许你输入一个名称,然后该页面将刷新,以提供个性化问候。这种状态也会页面/浏览器重新加载期间保持,因为这个名称存储在Web Storage 中。

你可以在 personal-greeting.html 中找到示例文件 —— 这包含一个具有标题,内容和页脚,以及用于输入您的姓名的表单的简单网站。

让我们来构建示例,以便了解它的工作原理。

  1. 首先,在您的计算机上的新目录中创建一个 personal-greeting.html 文件的副本。

  2. 接下来,请注意我们的HTML如何引用一个名为index.js的 JavaScript 文件(请参见第40行)。我们需要创建它并将 JavaScript 代码写入其中。在与HTML文件相同的目录中创建一个index.js文件。 

  3. 我们首先创建对所有需要在此示例中操作的HTML功能的引用 - 我们将它们全部创建为常量,因为这些引用在应用程序的生命周期中不需要更改。将以下几行添加到你的 JavaScript 文件中:

    // 创建所需的常量
    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. 接下来,我们需要包含一个小小的事件监听器,以在按下提交按钮时阻止实际的提交表单动作自身,因为这不是我们想要的行为。在您之前的代码下添加此代码段: 在你之前的代码后添加这段代码:

    // 当按钮按下时阻止表单提交
    form.addEventListener('submit', function(e) {
      e.preventDefault();
    });
  5. 现在我们需要添加一个事件监听器,当单击“Say hello”按钮时,它的处理函数将会运行。这些注释详细解释了每一处都做了什么,但实际上我们在这里获取用户输入到文本输入框中的名字并使用setItem()将它保存在网络存储中,然后运行一个名为nameDisplayCheck()的函数来处理实际的网站文本的更新。将此添加到代码的底部: 

    // run function when the 'Say hello' button is clicked
    submitBtn.addEventListener('click', function() {
      // 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. 此时,我们还需要一个事件处理程序,以便在单击“Forget”按钮时运行一个函数——且仅在单击“Say hello”按钮(两种表单状态来回切换)后才显示。在这个功能中,我们使用removeItem()从网络存储中删除项目name,然后再次运行nameDisplayCheck()以更新显示。将其添加到底部: 

    // run function when the 'Forget' button is clicked
    forgetBtn.addEventListener('click', function() {
      // 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()函数本身了。在这里,我们通过使用localStorage.getItem('name')作为测试条件来检查name 数据项是否已经存储在Web Storage 中如果它已被存储,则该调用的返回值为true如果没有,它会是false如果是true,我们会显示个性化问候语,显示表格的“forget”部分,并隐藏表格的“Say hello”部分。如果是false,我们会显示一个通用问候语,并做相反的事。再次将下面的代码添到底部:

    // 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
        let 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()函数。如果我们不这样做,那么个性化问候不会在页面重新加载后保持。将以下代码添加到代码的底部:

    document.body.onload = nameDisplayCheck;

你的例子完成了 - 做得好!现在剩下的就是保存你的代码并在浏览器中测试你的HTML页面。你可以在这里看到我们的完成版本并在线运行

注意: 在 Using the Web Storage API 中还有一个稍微复杂点儿的示例。

注意: 在完成版本的源代码中, <script src="index.js" defer></script> 一行里, defer 属性指明在页面加载完成之前,<script>元素的内容不会执行。

存储复杂数据 — IndexedDB

IndexedDB API(有时简称 IDB )是可以在浏览器中访问的一个完整的数据库系统,在这里,你可以存储复杂的关系数据。其种类不限于像字符串和数字这样的简单值。你可以在一个IndexedDB中存储视频,图像和许多其他的内容。

但是,这确实是有代价的:使用IndexedDB要比Web Storage API复杂得多。在本节中,我们仅仅只能浅尝辄止地一提它的能力,不过我们会给你足够基础知识以帮助你开始。

通过一个笔记存储示例演示

Here we'll run you through an example that allows you to store notes in your browser and view and delete them whenever you like, getting you to build it up for yourself and explaining the most fundamental parts of IDB as we go along.

这个 app 看起来像这样:

Each note has a title and some body text, each individually editable. The JavaScript code we'll go through below has detailed comments to help you understand what's going on.

开始

  1. First of all, make local copies of our index.html, style.css, and index-start.js files into a new directory on your local machine.
  2. Have a look at the files. You'll see that the HTML is pretty simple: a web site with a header and footer, as well as a main content area that contains a place to display notes, and a form for entering new notes into the database. The CSS provides some simple styling to make it clearer what is going on. The JavaScript file contains five declared constants containing references to the <ul> element the notes will be displayed in, the title and body <input> elements, the <form> itself, and the <button>.
  3. Rename your JavaScript file to index.js. You are now ready to start adding code to it.

数据库初始设置

Now let's look at what we have to do in the first place, to actually set up a database.

  1. Below the constant declarations, add the following lines:

    // Create an instance of a db object for us to store the open database in
    let db;

    Here we are declaring a variable called db — this will later be used to store an object representing our database. We will use this in a few places, so we've declared it globally here to make things easier.

  2. Next, add the following to the bottom of your code:

    window.onload = function() {
    
    };

    We will write all of our subsequent code inside this window.onload event handler function, called when the window's load event fires, to make sure we don't try to use IndexedDB functionality before the app has completely finished loading (it could fail if we don't).

  3. Inside the window.onload handler, add the following:

    // Open our database; it is created if it doesn't already exist
    // (see onupgradeneeded below)
    let request = window.indexedDB.open('notes', 1);

    This line creates a request to open version 1 of a database called notes. If this doesn't already exist, it will be created for you by subsequent code. You will see this request pattern used very often throughout IndexedDB. Database operations take time. You don't want to hang the browser while you wait for the results, so database operations are asynchronous, meaning that instead of happening immediately, they will happen at some point in the future, and you get notified when they're done.

    To handle this in IndexedDB, you create a request object (which can be called anything you like — we called it request so it is obvious what it is for). You then use event handlers to run code when the request completes, fails, etc., which you'll see in use below.

    Note: The version number is important. If you want to upgrade your database (for example, by changing the table structure), you have to run your code again with an increased version number, different schema specified inside the onupgradeneeded handler (see below), etc. We won't cover upgrading databases in this simple tutorial.

  4. Now add the following event handlers just below your previous addition — again inside the window.onload handler:

    // onerror handler signifies that the database didn't open successfully
    request.onerror = function() {
      console.log('Database failed to open');
    };
    
    // onsuccess handler signifies that the database opened successfully
    request.onsuccess = function() {
      console.log('Database opened successfully');
    
      // Store the opened database object in the db variable. This is used a lot below
      db = request.result;
    
      // Run the displayData() function to display the notes already in the IDB
      displayData();
    };

    The request.onerror handler will run if the system comes back saying that the request failed. This allows you to respond to this problem. In our simple example, we just print a message to the JavaScript console.

    The request.onsuccess handler on the other hand will run if the request returns successfully, meaning the database was successfully opened. If this is the case, an object representing the opened database becomes available in the request.result property, allowing us to manipulate the database. We store this in the db variable we created earlier for later use. We also run a custom function called displayData(), which displays the data in the database inside the <ul>. We run it now so that the notes already in the database are displayed as soon as the page loads. You'll see this defined later on.

  5. Finally for this section, we'll add probably the most important event handler for setting up the database: request.onupdateneeded. This handler runs if the database has not already been set up, or if the database is opened with a bigger version number than the existing stored database (when performing an upgrade). Add the following code, below your previous handler:

    // Setup the database tables if this has not already been done
    request.onupgradeneeded = function(e) {
      // Grab a reference to the opened database
      let db = e.target.result;
    
      // Create an objectStore to store our notes in (basically like a single table)
      // including a auto-incrementing key
      let objectStore = db.createObjectStore('notes', { 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');
    };

    This is where we define the schema (structure) of our database; that is, the set of columns (or fields) it contains. Here we first grab a reference to the existing database from e.target.result (the event target's result property), which is the request object. This is equivalent to the line db = request.result; inside the onsuccess handler, but we need to do this separately here because the onupgradeneeded handler (if needed) will run before the onsuccess handler, meaning that the db value wouldn't be available if we didn't do this.

    We then use IDBDatabase.createObjectStore() to create a new object store inside our opened database. This is equivalent to a single table in a conventional database system. We've given it the name notes, and also specified an autoIncrement key field called id — in each new record this will automatically be given an incremented value — the developer doesn't need to set this explicitly. Being the key, the id field will be used to uniquely identify records, such as when deleting or displaying a record.

    We also create two other indexes (fields) using the IDBObjectStore.createIndex() method: title (which will contain a title for each note), and body (which will contain the body text of the note).

So with this simple database schema set up, when we start adding records to the database each one will be represented as an object along these lines:

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

添加数据到数据库

Now let's look at how we can add records to the database. This will be done using the form on our page.

Below your previous event handler (but still inside the window.onload handler), add the following line, which sets up an onsubmit handler that runs a function called addData() when the form is submitted (when the submit <button> is pressed leading to a successful form submission):

// Create an onsubmit handler so that when the form is submitted the addData() function is run
form.onsubmit = addData;

Now let's define the addData() function. Add this below your previous line:

// 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
  let newItem = { title: titleInput.value, body: bodyInput.value };

  // open a read/write db transaction, ready for adding the data
  let transaction = db.transaction(['notes'], 'readwrite');

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

  // Make a request to add our newItem object to the object store
  var request = objectStore.add(newItem);
  request.onsuccess = function() {
    // 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.oncomplete = function() {
    console.log('Transaction completed: database modification finished.');

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

  transaction.onerror = function() {
    console.log('Transaction not opened due to error');
  };
}

This is quite complex; breaking it down, we:

  • Run Event.preventDefault() on the event object to stop the form actually submitting in the conventional manner (this would cause a page refresh and spoil the experience).
  • Create an object representing a record to enter into the database, populating it with values from the form inputs. note that we don't have to explicitly include an id value — as we expained early, this is auto-populated.
  • Open a readwrite transaction against the notes object store using the IDBDatabase.transaction() method. This transaction object allows us to access the object store so we can do something to it, e.g. add a new record.
  • Access the object store using the IDBTransaction.objectStore() method, saving the result in the objectStore variable.
  • Add the new record to the database using IDBObjectStore.add(). This creates a request object, in the same fashion as we've seen before.
  • Add a bunch of event handlers to the request and the transaction to run code at critical points in the lifecycle. Once the request has succeeded, we clear the form inputs ready for entering the next note. Once the transaction has completed, we run the displayData() function again to update the display of notes on the page.

显示数据

We've referenced displayData() twice in our code already, so we'd probably better define it. Add this to your code, below the previous function definition:

// Define the displayData() function
function displayData() {
  // Here we empty the contents of the list element each time the display is updated
  // If you ddn'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
  let objectStore = db.transaction('notes').objectStore('notes');
  objectStore.openCursor().onsuccess = function(e) {
    // Get a reference to the cursor
    let 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
      let listItem = document.createElement('li');
      let h3 = document.createElement('h3');
      let 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
      let 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.onclick = 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) {
        let 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');
    }
  };
}

Again, let's break this down:

  • First we empty out the <ul> element's content, before then filling it with the updated content. If you didn't do this, you'd end up with a huge list of duplicated content being added to with each update.
  • Next, we get a reference to the notes object store using IDBDatabase.transaction() and IDBTransaction.objectStore() like we did in addData(), except here we are chaining them together in one line.
  • The next step is to use IDBObjectStore.openCursor() method to open a request for a cursor — this is a construct that can be used to iterate over the records in an object store. We chain an onsuccess handler on to the end of this line to make the code more concise — when the cursor is successfully returned, the handler is run.
  • We get a reference to the cursor itself (an IDBCursor object) using let cursor = e.target.result.
  • Next, we check to see if the cursor contains a record from the datastore (if(cursor){ ... }) — if so, we create a DOM fragment, populate it with the data from the record, and insert it into the page (inside the <ul> element). We also include a delete button that, when clicked, will delete that note by running the deleteItem() function, which we will look at in the next section.
  • At the end of the if block, we use the IDBCursor.continue() method to advance the cursor to the next record in the datastore, and run the content of the if block again. If there is another record to iterate to, this causes it to be inserted into the page, and then continue() is run again, and so on.
  • When there are no more records to iterate over, cursor will return undefined, and therefore the else block will run instead of the if block. This block checks whether any notes were inserted into the <ul> — if not, it inserts a message to say no note was stored.

删除一条笔记

As stated above, when a note's delete button is pressed, the note is deleted. This is achieved by the deleteItem() function, which looks like so:

// 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 it use it with IDB; IDB key
  // values are type-sensitive.
  let 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
  let transaction = db.transaction(['notes'], 'readwrite');
  let objectStore = transaction.objectStore('notes');
  let request = objectStore.delete(noteId);

  // report that the data item has been deleted
  transaction.oncomplete = function() {
    // 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) {
      let listItem = document.createElement('li');
      listItem.textContent = 'No notes stored.';
      list.appendChild(listItem);
    }
  };
}
  • The first part of this could use some explaining — we retrieve the ID of the record to be deleted using Number(e.target.parentNode.getAttribute('data-note-id')) — recall that the ID of the record was saved in a data-note-id attribute on the <li> when it was first displayed. We do however need to pass the attribute through the global built-in Number() object, as it is currently a string, and otherwise won't be recognized by the database.
  • We then get a reference to the object store using the same pattern we've seen previously, and use the IDBObjectStore.delete() method to delete the record from the database, passing it the ID.
  • When the database transaction is complete, we delete the note's <li> from the DOM, and again do the check to see if the <ul> is now empty, inserting a note as appropriate.

So that's it! Your example should now work.

If you are having trouble with it, feel free to check it against our live example (see the source code also).

通过 IndexedDB 存储复杂数据

As we mentioned above, IndexedDB can be used to store more than just simple text strings. You can store just about anything you want, including complex objects such as video or image blobs. And it isn't much more difficult to achieve than any other type of data.

To demonstrate how to do it, we've written another example called IndexedDB video store (see it running live here also). When you first run the example, it downloads all the videos from the network, stores them in an IndexedDB database, and then displays the videos in the UI inside <video> elements. The second time you run it, it finds the videos in the database and gets them from there instead befoire displaying them — this makes subsequent loads much quicker and less bandwidth-hungry.

Let's walk through the most interesting parts of the example. We won't look at it all — a lot of it is similar to the previous example, and the code is well-commented.

  1. For this simple example, we've stored the names of the videos to fetch in an array opf objects:

    const videos = [
      { 'name' : 'crystal' },
      { 'name' : 'elf' },
      { 'name' : 'frog' },
      { 'name' : 'monster' },
      { 'name' : 'pig' },
      { 'name' : 'rabbit' }
    ];
  2. To start with, once the database is successfully opened we run an init() function. This loops through the different video names, trying to load a record identified by each name from the videos database.

    If each video is found in the database (easily checked by seeing whether request.result evaluates to true — if the record is not present, it will be undefined), its video files (stored as blobs) and the video name are passed straight to the displayVideo() function to place them in the UI. If not, the video name is passed to the fetchVideoFromNetwork() function to ... you guessed it — fetch the video from the network.

    function init() {
      // Loop through the video names one by one
      for(let i = 0; i < videos.length; i++) {
        // Open transaction, get object store, and get() each video by name
        let objectStore = db.transaction('videos').objectStore('videos');
        let request = objectStore.get(videos[i].name);
        request.onsuccess = function() {
          // 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(videos[i]);
          }
        };
      }
    }
  3. The following snippet is taken from inside fetchVideoFromNetwork() — here we fetch MP4 and WebM versions of the video using two separate WindowOrWorkerGlobalScope.fetch() requests. We then use the Body.blob() method to extract each response's body as a blob, giving us an object representation of the videos that can be stored and displayed later on.

    We have a problem here though — these two requests are both asynchronous, but we only want to try to display or store the video when both promises have fulfilled. Fortunately there is a built-in method that handles such a problem — Promise.all(). This takes one argument — references to all the individual promises you want to check for fulfillment placed in an array — and is itself promise-based.

    When all those promises have fulfilled, the all() promise fulfills with an array containing all the individual fulfillment values. Inside the all() block, you can see that we then call the displayVideo() function like we did before to display the videos in the UI, then we also call the storeVideo() function to store those videos inside the database.

    let mp4Blob = fetch('videos/' + video.name + '.mp4').then(response =>
      response.blob()
    );
    let 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(function(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. Let's look at storeVideo() first. This is very similar to the pattern you saw in the previous example for adding data to the database — we open a readwrite transaction and get an object store reference our videos, create an object representing the record to add to the database, then simply add it using IDBObjectStore.add().

    function storeVideo(mp4Blob, webmBlob, name) {
      // Open transaction, get object store; make it a readwrite so we can write to the IDB
      let objectStore = db.transaction(['videos'], 'readwrite').objectStore('videos');
      // Create a record to add to the IDB
      let record = {
        mp4 : mp4Blob,
        webm : webmBlob,
        name : name
      }
    
      // Add the record to the IDB using add()
      let request = objectStore.add(record);
    
      ...
    
    };
  5. Last but not least, we have displayVideo(), which creates the DOM elements needed to insert the video in the UI and then appends them to the page. The most interesting parts of this are those shown below — to actually display our video blobs in a <video> element, we need to create object URLs (internal URLs that point to the video blobs stored in memory) using the URL.createObjectURL() method. Once that is done, we can set the object URLs to be the vaues of our <source> element's src attributes, and it works fine.

    function displayVideo(mp4Blob, webmBlob, title) {
      // Create object URLs out of the blobs
      let mp4URL = URL.createObjectURL(mp4Blob);
      let webmURL = URL.createObjectURL(webmBlob);
    
      ...
    
      let video = document.createElement('video');
      video.controls = true;
      let source1 = document.createElement('source');
      source1.src = mp4URL;
      source1.type = 'video/mp4';
      let source2 = document.createElement('source');
      source2.src = webmURL;
      source2.type = 'video/webm';
    
      ...
    }

离线文件存储

The above example already shows how to create an app that will store large assets in an IndexedDB database, avoiding the need to download them more than once. This is already a great improvement to the user experience, but there is still one thing missing — the main HTML, CSS, and JavaScript files still need to downloaded each time the site is accessed, meaning that it won't work when there is no network connection.

This is where Service workers and the closely-related Cache API come in.

A service worker is a JavaScript file that, simply put, is registered against a particular origin (web site, or part of a web site at a certain domain) when it is accessed by a browser. When registered, it can control pages available at that origin. It does this by sitting between a loaded page and the network and intercepting network requests aimed at that origin.

When it intercepts a request, it can do anything you wish to it (see use case ideas), but the classic example is saving the network responses offline and then providing those in response to a request instead of the responses from the network. In effect, it allows you to make a web site work completely offline.

The Cache API is a another client-side storage mechanism, with a bit of a difference — it is designed to save HTTP responses, and so works very well with service workers.

注意: Service workers and Cache are supported in most modern browsers now. At the time of writing, Safari was still busy implementing it, but it should be there soon.

一个 service worker 例子

Let's look at an example, to give you a bit of an idea of what this might look like. We have created another version of the video store example we saw in the previous section — this functions identically, except that it also saves the HTML, CSS, and JavaScript in the Cache API via a service worker, allowing the example to run offline!

See IndexedDB video store with service worker running live, and also see the source code.

Registering the service worker

The first thing to note is that there's an extra bit of code placed in the main JavaScript file (see index.js). First we do a feature detection test to see if the serviceWorker member is available in the Navigator object. If this returns true, then we know that at least the basics of service workers are supported. Inside here we use the ServiceWorkerContainer.register() method to register a service worker contained in the sw.js file against the origin it resides at, so it can control pages in the same directory as it, or subdirectories. When its promise fulfills, the service worker is deemed registered.

  // 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(function() { console.log('Service Worker Registered'); });
  }

Note: The given path to the sw.js file is relative to the site origin, not the JavaScript file that contains the code. The service worker is at https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js. The origin is https://mdn.github.io, and therefore the given path has to be /learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js. If you wanted to host this example on your own server, you'd have to change this accordingly. This is rather confusing, but it has to work this way for security reasons.

安装 service worker

The next time any page under the service worker's control is accessed (e.g. when the example is reloaded), the service worker is installed against that page, meaning that it will start controlling it. when this occurs, an install event is fired against the service worker; you can write code inside the service worker itself that will respond to the installation.

Let's look at an example, in the sw.js file (the service worker). You'll see that the install listener is registered against self. This self keyword is a way to refer to the global scope of the service worker from inside the service worker file.

Inside the install handler we use the ExtendableEvent.waitUntil() method, available on the event object, to signal that the browser shouldn't complete installation of the service worker until after the promise inside it has fulfilled successfully.

Here is where we see the Cache API in action. We use the CacheStorage.open() method to open a new cache object in which responses can be stored (similar to an IndexedDB object store). This promise fulfills with a Cache object representing the video-store cache. We then use the Cache.addAll() method to fetch a series of assets and add their responses to the cache.

self.addEventListener('install', function(e) {
 e.waitUntil(
   caches.open('video-store').then(function(cache) {
     return 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'
     ]);
   })
 );
});

That's it for now, installation done.

响应未来的请求

With the service worker registered and installed against our HTML page, and the relevant assets all added to our cache, we are nearly ready to go. There is only one more thing to do, write some code to respond to further network requests.

This is what the second bit of code in sw.js does. We add another listener to the service worker global scope, which runs the handler function when the fetch event is raised. This happens whenever the browser makes a request for an asset in the directory the service worker is registered against.

Inside the handler we first log the URL of the requested asset. We then provide a custom response to the request, using the FetchEvent.respondWith() method.

Inside this block we use CacheStorage.match() to check whether a matching request (i.e. matches the URL) can be found in any cache. This promise fulfills with the matching reponse if a match is not found, or undefined if it isn't.

If a match is found, we simply return it as the custom response. If not, we fetch() the response from the network and return that instead.

self.addEventListener('fetch', function(e) {
  console.log(e.request.url);
  e.respondWith(
    caches.match(e.request).then(function(response) {
      return response || fetch(e.request);
    })
  );
});

And that is it for our simple service worker. There is a whole load more you can do with them — for a lot more detail, see the service worker cookbook. And thanks to Paul Kinlan for his article Adding a Service Worker and Offline into your Web App, which inspired this simple example.

测试离线示例

To test our service worker example, you'll need to load it a couple of times to make sure it is installed. Once this is done, you can:

  • Try unplugging your network/turning your Wifi off.
  • Select File > Work Offline if you are using Firefox.
  • Go to the devtools, then choose Application > Service Workers, then check the Offline checkbox if you are using Chrome.

If you refresh your example page again, you should still see it load just fine. Everything is stored offline — the page assets in a cache, and the videos in an IndexedDB database.

总结

现在就是这些。我们希望你能感觉到我们对客户端存储技术的介绍很有用。

相关链接

在本单元中

文档标签和贡献者

此页面的贡献者: lscnet, codeofjackie, utopia1991
最后编辑者: lscnet,