HTTP cookie 通常也叫做 cookie,最初用于在客户端行会话信息。客户端通过 HTTP 头部的 Set-Cookie 设置 cookie 信息。

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

他是以 键 = 值 的形式进行存储,多个值以逗号隔开:'id=123; max-age=2', 'abc=456; domain=test.com'

# 限制

cookie 是与特定域绑定的。设置了 cookie 后,会跟请求一起发送到所创建的域。不会被其他域访问。 cookie 的限制主要有以下几点:

  • 不超过 300 个 cookie;
  • 每个 cookie 不超过 4096 字节;
  • 每个域不超过 20 个 cookie;
  • 每个域不超过 81920 字节;

每个域能设置的 cookie 总数也是受限的,但不同浏览器的限制不同。例如:

  • 最新版 IE 和 Edge 限制每个域不超过 50 个 cookie;
  • 最新版 Firefox 限制每个域不超过 150 个 cookie;
  • 最新版 Opera 限制每个域不超过 180 个 cookie;
  • Safari 和 Chrome 对每个域的 cookie 数没有硬性限制。

如果 cookie 总数超过了单个域的上限,浏览器就会删除之前设置的 cookie。IE 和 Opera 会按照最近最少使用(LRU,Least Recently Used)原则删除之前的 cookie,Firefox 好像会随机删除之前的 cookie。

如果创建的 cookie 超过最大限制,则该 cookie 会被静默删除。注意,一个字符通常会占 1 字节。如果使用多字节字符(如 UTF-8 Unicode 字符),则每个字符最多可能占 4 字节。

cookie 在浏览器中是由以下参数构成的:

  • 名称:唯一标识 cookie 的名称。cookie 名不区分大小写。cookie 名必须经过 URL 编码。

  • 值:存储在 cookie 里的字符串值。这个值必须经过 URL 编码。

  • 域:cookie 有效的域。发送到这个域的所有请求都会包含对应的 cookie。这个值可能包含子域(如www.wrox.com),也可以不包含(如.wrox.com 表示对 wrox.com 的所有子域都有效)。如果不明确设置,则默认为设置 cookie 的域。

  • 路径:请求 URL 中包含这个路径才会把 cookie 发送到服务器。例如,可以指定 cookie 只能由http://www.wrox.com/books/访问,因此访问 http://www.wrox.com/下的页面就不会发送 cookie,即使请求的是同一个域。

  • 过期时间:表示何时删除 cookie 的时间戳(即什么时间之后就不发送到服务器了)。默认情况下,浏览器会话结束后会删除所有 cookie。不过,也可以设置删除 cookie 的时间。这个值是 GMT 格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除 cookie 的具体时间。这样即使关闭浏览器 cookie 也会保留在用户机器上。把过期时间设置为过去的时间会立即删除 cookie。

  • 安全标志:设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器。例如,请求 https://www.wrox.com 会发送 cookie,而请求 http://www.wrox.com则不会。

这些参数在 Set-Cookie 头部中使用分号加空格隔开,比如:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
Other-header: other-header-value

上面设置了对 www.wrox.com 及其他 wrox.com 的子域(如 p2p.wrox.com)有效。

安全标志 secure 是 cookie 中唯一的非名/值对,只需一个 secure 就可以了。比如:

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value

域、路径、过期时间和 secure 标志用于告诉浏览器什么情况下应该在请求中包含 cookie。这些参数并不会随请求发送给服务器,实际发送的只有 cookie 的键/值对。

可以通过 document.cookie 获取到页面中所有有效的 cookie 的字符串。所有的键值通过 URL 编码的,需要使用 decodeURIComponent() 解码。

同样设置值也是使用 document.cookie 设置,追加到原有的 cookie 字符串后面。格式也是跟 Set-Cookie 头部的格式一样:

document.cookie = 'name=jiegiser'
// 最好使用 encodeURIComponent 进行编码
document.cookie = `${encodeURIComponent('name')}=${encodeURIComponent('jiegiser')}`

设置的值在每次客户端向服务器发送请求时都会被带上,在浏览器关闭时就会被删除。

如果需要覆盖之前添加的 cookie,只需要添加同名的 cookie 即可。要为创建的 cookie 指定额外的信息,只要像 Set-Cookie 头部一样直接在后面追加相同格式的字符串即可:

document.cookie = `${encodeURIComponent('name')}=${encodeURIComponent('jiegiser')};domain=.wrox.com;path=/`

下面代码是封装 cookie 的基本操作:

class CookieUtil {
  static get(name) {
    let cookieName = `${encodeURIComponent(name)}=`,
        cookieStart = document.cookie.indexOf(cookieName),
        cookieValue = null
    if (cookieStart > -1) {
      let cookieEnd = document.cookie.indexOf(';', cookieStart)
      if (cookieEnd === -1) {
        cookieEnd = document.cookie.length
      }
      cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd))
    }
    return cookieValue
  }

  static set(name, value, expires, path, domain, secure) {
    let cookieText = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
    if (expires instanceof Date) {
      cookieText += `; expires=${expires.toGMTString()}`
    }
    if (path) {
      cookieText += `; path=${path}`
    }
    if (domain) {
      cookieText += `; domain=${domain}`
    }
    if (secure) {
      cookieText += `; secure`
    }
    document.cookie = cookieText
  }
  static unset(name, path, domain, secure) {
    CookieUtil.set(name, '', new Date(0), path, domain, secure)
  }
}

没有直接删除已有 cookie 的方法。为此,需要再次设置同名 cookie(包括相同路径、域和安全选项),但要将其过期时间设置为某个过去的时间。

为绕过浏览器对每个域 cookie 数的限制,有些开发者提出了子 cookie 的概念。子 cookie 是在单个 cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。最常用的子 cookie 模式如下:

name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5

子 cookie 的格式类似于查询字符串。这些值可以存储为单个 cookie,而不用单独存储为自己的键/值对。结果就是网站或 Web 应用程序能够在单域 cookie 数限制下存储更多的结构化数据。

对子 cookie 的操作:

class SubCookieUtil {
  static get(name, subName) {
    let subCookies = SubCookieUtil.getAll(name)
    return subCookies ? subCookies[subName] : null
  }
  static getAll(name) {
    let cookieName = encodeURIComponent(name) + '=',
        cookieStart = document.cookie.indexOf(cookieName),
        cookieValue = null,
        cookieEnd,
        subCookies,
        parts,
        result = {}
    if (cookieStart > -1) {
      cookieEnd = document.cookie.indexOf(';', cookieStart)
      if (cookieEnd == -1) {
        cookieEnd = document.cookie.length
      }
      cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd)
      if (cookieValue.length > 0) {
        subCookies = cookieValue.split('&')
        for (let i = 0, len = subCookies.length; i < len; i++) {
          parts = subCookies[i].split('=')
          result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1])
        }
        return result
      }
    }
    return null
  }

  static set(name, subName, value, expires, path, domain, secure) {
    let subcookies = SubCookieUtil.getAll(name) || {}
    subcookies[subName] = value
    SubCookieUtil.setAll(name, subcookies, expires, path, domain, secure)
  }
  static setAll(name, subcookies, expires, path, domain, secure) {
    let cookieText = encodeURIComponent(name) + '=',
        subcookieParts = new Array(),
        subName
    for (subName in subcookies) {
      if (subName.length > 0 && subcookies.hasOwnProperty(subName)){
        subcookieParts.push('${encodeURIComponent(subName)}=${encodeURIComponent(subcookies[subName])}')
      }
    }
    if (cookieParts.length > 0) {
      cookieText += subcookieParts.join('&')
      if (expires instanceof Date) {
        cookieText += `; expires=${expires.toGMTString()}`
      }
      if (path) {
        cookieText += `; path=${path}`
      }
      if (domain) {
        cookieText += `; domain=${domain}`
      }
      if (secure) {
        cookieText += '; secure'
      }
    } else {
      cookieText += `; expires=${(new Date(0)).toGMTString()}`
    }
    document.cookie = cookieText
  }

  static unset(name, subName, path, domain, secure) {
    let subcookies = SubCookieUtil.getAll(name)
    if (subcookies) {
      delete subcookies[subName] // 删除
      SubCookieUtil.setAll(name, subcookies, null, path, domain, secure)
    }
  }
  static unsetAll(name, path, domain, secure) {
    SubCookieUtil.setAll(name, null, new Date(0), path, domain, secure)
  }
}
  • HTTP-only 的 cookie。只能在服务器上读取以及设置值。HTTP-only 可以在浏览器设置,也可以在服务器设置。

  • 尽量设置 cookie 小一些,以避免请求时的性能为题。

  • 不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中。

# Web Storage

Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。主要有两个目标:

  • 提供在 cookie 之外的存储会话数据的途径;
  • 提供跨会话持久化存储大量数据的机制。

Web Storage 的第 2 版定义了两个对象: localStorage 和 sessionStorage 。 localStorage 是永久存储机制, sessionStorage 是跨会话的存储机制。这两种浏览器存储 API 提供了在浏览器中不受页面刷新影响而存储数据的两种方式。

# Storage 类型

Storage 类型用于保存名/值对数据,有以下方法:

  • clear() :删除所有值;不在 Firefox 中实现。
  • getItem(name) :取得给定 name 的值。
  • key(index) :取得给定数值位置的名称。
  • removeItem(name) :删除给定 name 的名/值对。
  • setItem(name, value) :设置给定 name 的值。

通过 length 属性可以确定 Storage 对象中保存了多少名/值对。

# sessionStorage 对象

sessionStorage 对象是 Storage 的实例;

sessionStorage 对象只存储会话数据,浏览器关闭后,数据会消失。存储在 sessionStorage 中的数据不受页面刷新影响。

sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的。

// 使用方法存储数据
sessionStorage.setItem('name', 'jiegiser')
// 使用属性存储数据
sessionStorage.book = 'javascript'

// 使用方法取得数据
sessionStorage.getItem('name')
// 使用属性取得数据
let book = sessionStorage.book

// 可以结合 sessionStorage 的 length 属性和 key() 方法遍历所有的值:
for(let i = 0, len = sessionStorage.length; i < len; i++) {
  let key = sessionStorage.key(i)
  let value = sessionStorage.getItem(key)
  console.log(`${key} = ${value}`)
}

// 使用 for in;这里不会返回内置方法或 length 属性。
for (let key in sessionStorage) {
  let value = sessionStorage.getItem(key)
  console.log(`${key} = ${value}`)
}

// 删除数据
// 使用 delete 删除值
delete sesionStorage.name

// 使用方法删除值
sessionStorage.removeItem('book')

# localStorage 对象

localStorage 对象取代了 globalStorage ,作为在客户端持久存储数据的机制。要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端 口上使用相同的协议。

// 使用方法存储数据
localStorage.setItem('name', 'jiegiser')

// 使用属性存储数据
localStorage.book = 'javascript'

// 使用方法获取数据
let name = localStorage.getItem('name')

// 使用属性获取数据
let book = locatStorage.book

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。 localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失。

# 存储事件

每当 Storage 对象发生变化时,都会在文档上触发 storage 事件。使用属性或 setItem() 设置值、使用 delete 或 removeItem() 删除值,以及每次调用 clear() 时都会触发这个事件。这个事件的事件对象有如下 4 个属性。

  • domain :存储变化对应的域。
  • key :被设置或删除的键。
  • newValue :键被设置的新值,若键被删除则为 null 。
  • oldValue :键变化之前的值。
// 监听 storage 事件
window.addEventListener('storage', (event) => {
  console.log(`storage changed for ${event.domain}`, event)
})

对于 sessionStorage 和 localStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。

# 限制

不同浏览器给 localStorage 和 sessionStorage 设置了不同的空间限制,但大多数会限制为每个源 5MB。

一般来说,客户端数据的大小限制是按照每个源(协议、域和端口)来设置的,因此每个源有固定大小的数据存储空间。分析存储数据的页面的源可以加强这一限制。

# IndexedDB

IndexedDB的设计几乎完全是异步的。大多数操作以请求的形式执行,这些请求会异步执行,产生成功的结果或错误。绝大多数 IndexedDB 操作要求添加 onerror 和 onsuccess 事件处理程序来确定输出。

# 数据库

与传统数据库最大的区别在于,IndexedDB 使用对象存储而不是表格保存数据。

使用 IndexedDB 数据库的第一步是调用 indexedDB.open() 方法,并给它传入一个要打开的数据库名称。如果给定名称的数据库已存在,则会发送一个打开它的请求;如果不存在,则会发送创建并打开这个数据库的请求。这个方法会返回 IDBRequest 的实例,可以在这个实例上添加 onerror 和 onsuccess 事件处理程序。如下:

let db,
    request,
    version = 1

// 指定版本好需要为整数
request = indexedDB.open('admin', version)
request.onerror = (event) => {
  console.log(`Failed to open: ${event.target.errorCode}`)
}
request.onsuccess = (event) => {
  console.log(event.target)
  db = event.target.result
}

上面将创建的数据库的实例保存在了 db 变量中,所有与数据库相关的操作都要通过 db 对象本身来进行。如果打开数据库期间发生错误, event.target.errorCode 中就会存储表示问题的错误码。

# 对象存储

建立了数据库连接之后,下一步就是使用对象存储。 存储数据:

let user = {
  username: "007",
  firstName: "James",
  lastName: "Bond",
  password: "foo"
};

如果需要存储上面的数据,可以很容易看出最适合作为对象存储键的 username 属性。用户名必须全局唯一,它也是大多数情况下访问数据的凭据。这个键很重要,因为创建对象存储时必须指定一个键。

数据库的版本决定了数据库模式,包括数据库中的对象存储和这些对象存储的结构。如果数据库还不存在, open() 操作会创建一个新数据库,然后触发 upgradeneeded 事件。可以为这个事件设置处理程序,并在处理程序中创建数据库模式。如果数据库存在,而你指定了一个升级版的版本号,则会立即触发 upgradeneeded 事件,因而可以在事件处理程序中更新数据库模式。

下面的代码演示了为存储上述用户信息如何创建对象存储:

request.onupgradeneeded = (event) => {
  const db = event.taregt.result

  // 如果存在则删除当前 objectStore。测试的时候可以这样做, 但这样会在每次执行事件处理程序时删除已有数据

  if (db.objectStoreNames.contains('users')) {
    db.deleteObjectStore('users')
  }
  // 这里第二个参数的 keyPath 属性表示应该用作键的存储对象的属性名。
  db.createObjectStore('users', {
    keyPath: 'usename'
  })
}

# 事务

创建了对象存储之后,剩下的所有操作都是通过事务完成的。事务要通过调用数据库对象的 transaction() 方法创建。任何时候,只要想要读取或修改数据,都要通过事务把所有修改操作组织起来。最简单的情况下,可以像下面这样创建事务:

let transaction = db.transaction()

如果不指定参数,则对数据库中所有的对象存储有只读权限。更具体的方式是指定一个或多个要访问的对象存储的名称:

let transaction = db.transaction('users')

这样可以确保在事务期间只加载 users 对象存储的信息。如果想要访问多个对象存储,可以给第一个参数传入一个字符串数组:

let transaction = db.transaction(['users', 'anotherStore'])

如前所述,每个事务都以只读方式访问数据。要修改访问模式,可以传入第二个参数。这个参数应该是下列三个字符串之一: "readonly" 、 "readwrite" 或 "versionchange" 。比如:

// 这样事务就可以对 users 对象存储读写了
let transaction = db.transaction('users', 'readwrite')

有了事务的引用,就可以使用 objectStore() 方法并传入对象存储的名称以访问特定的对象存储。然后,可以使用 add() 和 put() 方法添加和更新对象,使用 get() 取得对象,使用 delete() 删除对象,使用 clear() 删除所有对象。其中, get() 和 delete() 方法都接收对象键作为参数,这 5 个方法都创建新的请求对象。来看下面的例子:

const transaction = db.transaction('users')
      store = transaction.objectStore('users')
      request = store.get('007')

request.onerror = (event) => {
  console.log('Did not get the object!')
}

reuqest.onsuccess = (event) => {
  console.log(event.target.result.firstName)
}

因为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序: onerror 和 oncomplete 。这两个事件可以用来获取事务级的状态信息:

transaction.onerror = (event) => {
  // 整个事务被取消
}

transaction.oncomplete = (event) => {
  // 整个事务成功完成
}

注意,不能通过 oncomplete 事件处理程序的 event 对象访问 get() 请求返回的任何数据。因此,仍然需要通过这些请求的 onsuccess 事件处理程序来获取数据。

# 插入对象

拿到了对象存储的引用后,就可以使用 add() 或 put() 写入数据了。这两个方法都接收一个参数,即要存储的对象,并把对象保存到对象存储。 如果添加的数据在数据库已经存在,add() 会导致错误,而 put() 会简单地重写该对象。add() 就是插入新值,put() 就是更新值。

// users 是一个用户数据的数组
for(let user of users) {
  store.add(user)
}

每次调用 add() 或 put() 都会创建对象存储的新更新请求。如果想验证请求成功与否,可以把请求对象保存到一个变量,然后为它添加 onerror 和 onsuccess 事件处理程序:

// users 是一个用户数据的数组
let request,
    requests = []

for(let user of users) {
  requset = store.add(user)
  request.onerror = () => {
    // 处理错误
  }
  request.onsuccess = () => {
    // 处理成功
  }
  requests.push(request)
}

# 通过游标查询

上面的事务可以通过已知键取得一条记录。如果想取得多条数据,则需要在事务中创建一个游标。游标是一个指向结果集的指针。与传统数据库查询不同,游标不会事先收集所有结果。相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据。

需要在对象存储上调用 openCursor() 方法创建游标。与其他 IndexedDB 操作一样, openCursor() 方法也返回一个请求,因此必须为它添加 onsuccess 和 onerror 事件处理程序。例如:

const transaction = db.transaction('users')
      store = transaction.objectStore('users'),
      request = store.openCursor()

request.onsuccess = (eent) => {
  // 处理成功
}
request.onerror = (event) => {
  // 处理错误
}

在调用 onsuccess 事件处理程序时,可以通过 event.target.result 访问对象存储中的下一条记录,这个属性中保存着 IDBCursor 的实例(有下一条记录时)或 null (没有记录时)。这个 IDBCursor 实例有几个属性。

  • direction :字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值。可能的值包括:NEXT("next") 、 NEXTUNIQUE("nextunique") 、 PREV("prev") 、 PREVUNIQUE("prevunique") 。
  • key :对象的键。
  • value :实际的对象。
  • primaryKey :游标使用的键。可能是对象键或索引键

如下代码:

request.onsuccess = (event) => {
  const cursor = event.target.result
  if (cursor) {
    // 注意,这个例子中的 cursor.value 保存着实际的对象。
    console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`)
  }
}

游标可用于更新个别记录。 update() 方法使用指定的对象更新当前游标对应的值。

request.onsuccess = (event) => {
  const cursor = event.target.result;
  let value,
      updateRequest;
  if (cursor) { // 永远要检查
    if (cursor.key == "foo") {
      value = cursor.value; // 取得当前对象
      value.password = "magic!"; // 更新密码
      updateRequest = cursor.update(value); // 请求保存更新后的对象
      updateRequest.onsuccess = () => {
        // 处理成功
      };
      updateRequest.onerror = () => {
        // 处理错误
      };
    }
  }
};

也可以调用 delelte() 来删除游标位置的记录,与 update() 一样,这也会创建一个请求:

request.onsuccess = (event) => {
  const cursor = event.target.result;
  let value,
      deleteRequest;
  if (cursor) { // 永远要检查
    if (cursor.key == "foo") {
      deleteRequest = cursor.delete(); // 请求删除对象
      deleteRequest.onsuccess = () => {
        // 处理成功
      };
      deleteRequest.onerror = () => {
        // 处理错误
      };
    }
  }
};

如果事务没有修改对象存储的权限, update() 和 delete() 都会抛出错误。 默认情况下,每个游标只会创建一个请求。要创建另一个请求,必须调用下列中的一个方法。

  • continue(key) :移动到结果集中的下一条记录。参数 key 是可选的。如果没有指定 key ,游标就移动到下一条记录;如果指定了,则游标移动到指定的键。
  • advance(count) :游标向前移动指定的 count 条记录。

这两个方法都会让游标重用相同的请求,因此也会重用 onsuccess 和 onerror 处理程序,直至不再需要。例如,下面的代码迭代了一个对象存储中的所有记录:

request.onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) { // 永远要检查
    console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
    cursor.continue(); // 移动到下一条记录
  } else {
    console.log("Done!");
  }
};

调用 cursor.continue() 会触发另一个请求并再次调用 onsuccess 事件处理程序。在没有更多记录时, onsuccess 事件处理程序最后一次被调用,此时 event.target.result 等于 null 。

# 键范围

使用键范围(key range)可以让游标更容易管理。键范围对应 IDBKeyRange 的实例。有四种方式指定键范围,第一种是使用 only() 方法并传入想要获取的键: 这个范围保证只获取键为 "007" 的值。使用这个范围创建的游标类似于直接访问对象存储并调用 get("007") 。

const onlyRange = IDBKeyRange.only("007");

第二种键范围可以定义结果集的下限。下限表示游标开始的位置。例如,下面的键范围保证游标从 "007" 这个键开始,直到最后:

// 从"007"记录开始,直到最后
const lowerRange = IDBKeyRange.lowerBound("007");

// 如果想从 "007" 后面的记录开始,可以再传入第二个参数 true
// 从"007"的下一条记录开始,直到最后
const lowerRange = IDBKeyRange.lowerBound("007", true);

第三种键范围可以定义结果集的上限,通过调用 upperBound() 方法可以指定游标不会越过的记录。下面的键范围保证游标从头开始并在到达键为 "ace" 的记录停止:

// 从头开始,到"ace"记录为止
const upperRange = IDBKeyRange.upperBound("ace");

// 如果不想包含指定的键,可以在第二个参数传入 true :
// 从头开始,到"ace"的前一条记录为止
const upperRange = IDBKeyRange.upperBound("ace", true);

要同时指定下限和上限,可以使用 bound() 方法。这个方法接收四个参数:下限的键、上限的键、可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限。下面是几个例子:

// 从"007"记录开始,到"ace"记录停止
const boundRange = IDBKeyRange.bound("007", "ace");
// 从"007"的下一条记录开始,到"ace"记录停止
const boundRange = IDBKeyRange.bound("007", "ace", true);
// 从"007"的下一条记录开始,到"ace"的前一条记录停止
const boundRange = IDBKeyRange.bound("007", "ace", true, true);
// 从"007"记录开始,到"ace"的前一条记录停止
const boundRange = IDBKeyRange.bound("007", "ace", false, true);


// 定义了范围之后,把它传给 openCursor() 方法,就可以得到位于该范围内的游标:
const store = db.transaction("users").objectStore("users"),
range = IDBKeyRange.bound("007", "ace");
request = store.openCursor(range);
request.onsuccess = function(event){
  const cursor = event.target.result;
  if (cursor) { // 永远要检查
    console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
    cursor.continue(); // 移动到下一条记录
  } else {
    console.log("Done!");
  }
};
// 这个例子只会输出从键为 "007" 的记录开始到键为 "ace" 的记录结束的对象,比上一节的例子要少。

# 设置游标方向

openCursor() 方法实际上可以接收两个参数,第一个是 IDBKeyRange 的实例,第二个是表示方向的字符串。通常,游标都是从对象存储的第一条记录开始,每次调用 continue() 或 advance() 都会向最后一条记录前进。这样的游标其默认方向为 "next" 。如果对象存储中有重复的记录,可能需要游标跳过那些重复的项。为此,可以给 openCursor() 的第二个参数传入 "nextunique" :

const transaction = db.transaction('users')
      store = transaction.objectStore('users')
      request = store.openCursor(null, 'nextunique')

也可以创建在对象存储中反向移动的游标,从最后一项开始向第一项移动。此时需要给 openCursor() 传入 "prev" 或 "prevunique" 作为第二个参数(后者的意思当然是避免重复)。例如:

const transaction = db.transaction('users')
      store = transaction.objectStore('users')
      request = store.openCursor(null, 'prevunique')

# 索引

对某些数据集,可能需要为对象存储指定多个键。例如,如果同时记录了用户 ID 和用户名,那可能需要通过任何一种方式来获取用户数据。为此,可以考虑将用户 ID作为主键,然后在用户名上创建索引。要创建新索引,首先要取得对象存储的引用,然后像下面的例子一样调用 createIndex() :

const transaction = db.transaction('users')
      store = transaction.objectStore('users')
      index = store.createIndex('username', 'username', {
        unique: true
      })

createIndex() 的第一个参数是索引的名称,第二个参数是索引属性的名称,第三个参数是包含键 unique 的 options 对象。这个选项中的 unique 应该必须指定,表示这个键是否在所有记录中唯一。因为 username 可能不会重复,所以这个键是唯一的。createIndex() 返回的是 IDBIndex 实例。在对象存储上调用 index() 方法也可以得到同一个实例。例如,要使用一个已存在的名为 "username" 的索引,可以像下面这样:

const transaction = db.transaction('users')
      store = transaction.objectStore('users')
      index = store.index('username')

索引非常像对象存储。可以在索引上使用 openCursor() 方法创建新游标,这个游标与在对象存储上调用 openCursor() 创建的游标完全一样。只是其 result.key 属性中保存的是索引键,而不是主键。下面看一个例子:

const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      index = store.index("username"),
      request = index.openCursor();
request.onsuccess = (event) => {
  // 处理成功
};

使用 openKeyCursor() 方法也可以在索引上创建特殊游标,只返回每条记录的主键。这个方法接收的参数与 openCursor() 一样。最大的不同在于, event.result.key 是索引键,且 event.result.value 是主键而不是整个记录。

const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      index = store.index("username"),
      request = index.openKeyCursor();
request.onsuccess = (event) => {
// 处理成功
// event.result.key 是索引键,event.result.value 是主键
};

// 可以使用 get() 方法并传入索引键通过索引取得单条记录,这会创建一个新请求:
const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      index = store.index("username"),
      request = index.get("007");
request.onsuccess = (event) => {
// 处理成功
};
request.onerror = (event) => {
// 处理错误
};

如果想只取得给定索引键的主键,可以使用 getKey() 方法。这样也会创建一个新请求,但 result.value 等于主键而不是整个记录:

const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      index = store.index("username"),
      request = index.getKey("007");
request.onsuccess = (event) => {
// 处理成功
// event.target.result.key 是索引键,event.target.result.value 是主键
};

在这个 onsuccess 事件处理程序中,event.target.result.value 中应该是用户 ID。

任何时候,都可以使用 IDBIndex 对象的下列属性取得索引的相关信息。

  • name :索引的名称。
  • keyPath :调用 createIndex() 时传入的属性路径。
  • objectStore :索引对应的对象存储。
  • unique :表示索引键是否唯一的布尔值。

对象存储自身也有一个 indexNames 属性,保存着与之相关索引的名称。使用如下代码可以方便地了解对象存储上已存在哪些索引:

const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      indexNames = store.indexNames
for (let indexName in indexNames) {
  const index = store.index(indexName);
  console.log(`Index name: ${index.name} KeyPath: ${index.keyPath} Unique: ${index.unique}`);
}

以上代码迭代了每个索引并在控制台中输出了它们的信息。在对象存储上调用 deleteIndex() 方法并传入索引的名称可以删除索引:

const transaction = db.transaction("users"),
      store = transaction.objectStore("users"),
      store.deleteIndex("username");

因为删除索引不会影响对象存储中的数据,所以这个操作没有回调。

# 限制

IndexedDB 的很多限制实际上与 Web Storage 一样。首先,IndexedDB 数据库是与页面源(协议、域和端口)绑定的,因此信息不能跨域共享。这意味着 www.wrox.com和 p2p.wrox.com 会对应不同的数据存储。 其次,每个源都有可以存储的空间限制。当前 Firefox 的限制是每个源 50MB,而 Chrome 是 5MB。移动版 Firefox 有 5MB 限制,如果用度超出配额则会请求用户许可。Firefox 还有一个限制——本地文本不能访问 IndexedDB 数据库。

评 论:

更新: 12/27/2020, 4:59:16 PM