《JavaScript 高级程序设计》 第二十五章 客户端存储 学习记录

1、cookie

  • 用于在客户端存储会话信息。这个规范要求服务器在响应 HTTP 请求时,通过发送 Set-Cookie HTTP 头部包含会话信息
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value 
复制代码
  • 这个 HTTP 响应会设置一个名为”name”,值为”value”的 cookie。
  • 名和值在发送时都会经过 URL 编码。
  • 浏览器会存储这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服务器
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
复制代码

1、限制

  • 与特定域绑定
    • 设置后会与请求一起发送到创建它的域,能保证cookie中信息只被认可的接受者开放。
  • cookie能够占用的空间有限
    • 不超过300个cookie
    • 每个4kb(4096字节)
    • 现在每个域基本无限制

2、cookie的构成

  • cookie的参数
    • 名称
      • 唯一标识cookie的名称
      • 不区分大小写
      • cookie必须经过URL编码
      • 存储在cookie中的字符串值
      • 必须经过URL编码
      • cookie有效的域
      • 发送到这个域的请求都会包含对应cookie
      • 可以包含或不包含子域
      • 默认为设置cookie的域
    • 路径
      • 请求URL包含这个路径才会把cookie发送到服务器
    • 过期时间
      • 表示何时删除cookie的时间戳
      • 默认浏览器会话结束后会删除所有cookie
    • 安全标志
      • 设置后只有在使用SSL安全连接的情况下才会把cookie发送到服务器
  • 在Set-Cookie头部中使用分号加空格隔开
...
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
...
复制代码
  • 安全标志source时cookie中唯一的非名值对,只需要一个source
...
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
...
复制代码

3、JavaScript 中的cookie

  • document.cookie返回页面所有有效的cookie的字符串,分号分隔

  • 所有名和值都是 URL 编码的,因此必须使用 decodeURIComponent()解码

  • 设置值的时候,不会覆盖之前存在的任何cookie,除非设置了已有的cookie,设置cookie格式与Set-Cookie头部格式一样

    name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure
    复制代码
  • 只有cookie的名称和值是必需的

  • 最好是使用 encodeURIComponent()对名称和值进行编码

  • 要为创建的 cookie 指定额外的信息,只要像 Set-Cookie 头部一样直接在后面追加相同格式的字符串

  • CookieUtil 对象

class CookieUtil {
  static get (name) {
		let cookieName = `${encodeURIComponent(name)}=`
    let cookieStart = document.cookie.indexOf(cookieName)
    let 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}`
    }
    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 Data(0), path, domain, secure)
  }
}
复制代码

4、子cookie

  • 子 cookie 是在单个 cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5 
复制代码
  • 不用单独存储为自己的名/值对,能够在单域 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);
  }
};
复制代码

5、使用cookie注意事项

  • HTTP-only的cookie,可以在浏览器和服务端设置,但只能在服务端获取。
  • 因为所有cookie都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影响特定域浏览器请求的性能。
  • 最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。

2、Web Storage

  • localStorage 永久存储机制,sessionStorage 跨会话存储机制。

1、Storage类型

  • 用于保存名值对数据,直至上限。
    • clear() 删除所有值
    • getItem(name) 取得给定name的值
    • key(index) 取得给定数值位置的名称
    • removeItem(name) 删除给定name 的名值对
    • setItem(name, value) 设置给定name的值
  • 只能存储字符串

2、sessionStorage对象

  • sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。

  • 存储在 sessionStorage 中的数据不受页面刷新影响,可以在浏览器崩溃并重启后恢复。

  • sessionStorage 对象与服务器会话紧密相关,所以在运行本地文件时不能使用。

  • 存储在 sessionStorage 对象中的数据只能由最初存储数据的页面使用,在多页应用程序中的用处有限。

  • 可以通过使用 setItem()方法或直接给属 性赋值给它添加数据。

    // 使用方法存储数据 
    sessionStorage.setItem("name", "Nicholas"); 
    
    // 使用属性存储数据 
    sessionStorage.book = "Professional JavaScript"; 
    复制代码
  • 同步阻塞方式,数据会立即提交到存储。通过 Web Storage 写入的任何数据都可以立即被读取。

  • 可以使用 getItem()或直接访问属性名来取得

    // 使用方法取得数据
    let name = sessionStorage.getItem("name");
    
    // 使用属性取得数据
    let book = sessionStorage.book; 
    复制代码
  • 可以结合length属性或使用for-in遍历所有的值

  • 可以使用 delete 操作符直接删除对象属性,也可以使用 removeItem()方法

    // 使用 delete 删除值
    delete sessionStorage.name;
    
    // 使用方法删除值
    sessionStorage.removeItem("book");
    复制代码
  • sessionStorage 对象应该主要用于存储只在会话期间有效的小块数据

  • 跨会话持久存储数据,可以使用localStorage

3、localStorage对象

  • 在客户端会话持久存储数据的机制,访问同一个localStorage对象,页面必须来自同一个域(子域不行),在相同的端口上使用相同的协议。
  • 方法和sessionStorage相同。
  • 区别
    • 存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存
    • localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失

4、存储事件

  • 当Storage对象发生变化时,都会在文档上触发storage事件。
  • 使用属性或 setItem()设置值、使用 delete 或 removeItem()删除值,以及每次调用 clear()时都会触发这个事件。
  • 包含属性
    • domain 存储变化对应的域
    • key 被设置或删除的键
    • newValue 键被设置的新值,若被删除则为null
    • oldValue 键变化之前的值
window.addEventListener("storage", (event)=> {
	//...
})
复制代码

5、限制

  • 取决于特定浏览器,一般是按照每个源(协议、域和端口)设置的。
  • 大多数限制为每个源5MB

3、IndexedDB

  • 浏览器中存储结构化数据的一个方案
  • IndexedDB 的设计几乎完全是异步的。这些请求会异步执行, 产生成功的结果或错误。

1、数据库

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

  • IndexedDB 数据库就是在一个公共命名空间下的一组对象存储,类似于 NoSQL 风格的实现

  • 使用方式

    • 调用indexedDB.open()传入一个要打开的数据库名称
      • 已存在,发送一个打开它的请求
      • 不存在,发送创建并打开这个数据库的请求
      • 返回IDBRequest实例
        • 实例上添加onerror和onsuccess事件处理程序
    let db, request, version = 1
    request = indexDB.open("admin", version)
    request.onerror = (event) => {
      alert(`Failed to open ${event.target.errorCode}`)
    }
    request.onsuccess = (event) => {
      db = event.target.result
    }
    复制代码
    • onsuccessevent.target.result代表数据库实例
    • onerrorevent.target.errorCode会存储表示问题的错误码

2、对象存储

  • 创建对象存储时必须指定一个键,必须全局唯一
  • 如果数据库还不存在,open()操作会创建一个新数据库,然后触发 upgradeneeded事件
  • 可以为这个事件设置处理程序,并在处理程序中创建数据库模式。
  • 如果数据库存在,而你指定了一个升级版的版本号,则会立 即触发 upgradeneeded 事件,因而可以在事件处理程序中更新数据库模式
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // 如果存在则删除当前 objectStore。测试的时候可以这样做
  // 但这样会在每次执行事件处理程序时删除已有数据
  if (db.objectStoreNames.contains("users")) {
    db.deleteObjectStore("users");
  }
  db.createObjectStore("users", {
    keyPath: "username"
  });
};
// 第二个参数的 keyPath 属性表示应该用作键的存储对象的属性名
复制代码

3、事务

  • 创建数据库之后的操作都要通过事务来完成。
  • 通过transaction()创建,任何时候只要想读取或修改数据,都要通过事务把所有修改操作组织起来。
let transaction = db.transaction()
复制代码
  • 不指定参数的时候,对数据库所有对象存储有只读权限
  • 具体的方式是指定一个或多个要访 问的对象存储的名称
let transaction = db.transaction("users"); 
let transaction = db.transaction(["users", "anotherStore"]); 
复制代码
  • 每个事务都以只读方式访问数据。要修改访问模式,可以传入第二个参数
    • readOnly
    • readwrite
    • versionchange
let transaction = db.transaction("users", "readwrite");
复制代码
  • 使用 objectStore()方法并传入对象存储的名称以访问特定的对象存储
  • add()添加对象
  • put()更新对象
  • get()取得对象
  • delete()删除对象
  • clear()删除所有对象
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const request = store.get("007");
request.onerror = (event) => alert("Did not get the object!");
request.onsuccess = (event) => alert(event.target.result.firstName);
复制代码
  • 为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序
transaction.onerror = (event) => {
 // 整个事务被取消
};
transaction.oncomplete = (event) => {
 // 整个事务成功完成
}; 
复制代码

4、插入对象

  • add() put()写入数据。
  • 这两个方法都接收一个参数, 即要存储的对象,并把对象保存到对象存储。
  • 这两个方法只在对象存储中已存在同名的键时有区别。
  • add()会导致错误,而 put()会简单地重写该对象。
  • 更简单地说,可以把 add()想象成插入 新值,而把 put()想象为更新值。
// 初始化
for (let user in users) {
  store.add(user)
}
复制代码
// users 是一个用户数据的数组
let request,
  requests = [];
for (let user of users) {
  request = store.add(user);
  request.onerror = () => {
    // 处理错误
  };
  request.onsuccess = () => {
    // 处理成功
  };
  requests.push(request);
}
复制代码

5、通过游标查询

  • 事务可以通过一个已知键取得一条记录。

  • 如果想取得多条数据,则需要在事务中创建一个游标

  • 游标是一个指向结果集的指针。

  • 与传统数据库查询不同,游标不会事先收集所有结果。相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据

  • openCursor()方法创建游标。

const transaction = db.transaction("users"),
  store = transaction.objectStore("users"),
  request = store.openCursor();
request.onsuccess = (event) => {
  // 处理成功
};
request.onerror = (event) => {
  // 处理错误
};
复制代码
  • 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) { // 永远要检查
    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()来删除游标位置的记录
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 条记录
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!");
  }
};
复制代码

6、键范围

  • 键范围(key range) 可以让游标更容易管理。

  • 键范围对应 IDBKeyRange 的实例。有四种方式指定键范围,

    • only()方法并传入想要获取的键

      • 保证只获取想要键的值,类似与直接访问对象存储并调用get()
      const onlyRange = IDBKeyRange.only("007")
      复制代码
    • lowerBound()定义结果集的下限。下限表示游标开始的位置。

      // 从"007"记录开始,直到最后
      const lowerRange = IDBKeyRange.lowerBound("007");
      
      // 从"007"的下一条记录开始,直到最后
      const lowerRange = IDBKeyRange.lowerBound("007", true); 
      复制代码
    • upperBound()定义结果集的上限,指定游标不会越过的记录。

      // 从头开始,到"ace"记录为止
      const upperRange = IDBKeyRange.upperBound("ace");
      
      // 从头开始,到"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");
const 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!");
  }
};
复制代码

7、设置游标方向

  • openCursor() 可接收两个参数, IDBKeyRange 的实例和表示方向的字符串
  • 默认从对象存储的第一条记录开始,每次调用 continue()或 advance()都会向最后一条记录前进
  • 方向的字符串
    • next
      • 默认方向
    • nextunique
      • 正向 对象存储中有重复的记录,需要游标跳过那些重复的项
    • prev
      • 反向移动的游标,从最后一项开始向第一项移动
    • prevunique
      • 反向 对象存储中有重复的记录,需要游标跳过那些重复的项

8、索引

  • 对某些数据集,可能需要为对象存储指定多个键
  • 将用户 ID 作为主键,然后在用户名上创建索引
  • 要创建新索引,首先要取得对象存储的引用,调用 createIndex()
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.createIndex("username", "username", { unique: true });
复制代码
  • createIndex参数
    • 索引名称
    • 索性属性名称
    • 包含键unique的options对象,表示这个键是否在所有记录中唯一
  • createIndex()返回的是 IDBIndex 实例。在对象存储上调用index()方法可以得到同一个实例
const transaction = db.transaction("users");
const store = transaction.objectStore("users")
const index = store.index("username");
复制代码
  • 可以在索引上使用 openCursor()方法创建新游标
    • 这个游标与在对象存储上调用openCursor(创建的游标完全一样
    • 只是result.key属性保存的是索引键不是主键
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.index("username");
const request = index.openCursor();
request.onsuccess = (event) => {
  // 处理成功
};
复制代码
  • openKeyCursor()方法也可以在索引上创建特殊游标,只返回每条记录的主键。
    • 这个方法接收的 参数与 openCursor()一样。
    • 不同在于,event.result.key 是索引键,且 event.result.value 是主键而不是整个记录。
const transaction = db.transaction("users");
const store = transaction.objectStore("users");
const index = store.index("username");
const request = index.openKeyCursor();
request.onsuccess = (event) => {
  // 处理成功
  // event.result.key 是索引键,event.result.value 是主键
};
复制代码
  • get()方法传入索引键通过索引取得单条记录
  • 如果果想只取得给定索引键的主键,可以使用 getKey()方法
  • IDBIndex 对象的属性
    • name 索引名称
    • keyPath 调用createIndex()传入的属性路径
    • objectStore 索引对应的对象存储
    • unique 索引键是否唯一(布尔值)
  • 对象存储自身也有一个 indexNames 属性,保存着与之相关索引的名称。
  • 在对象存储上调用 deleteIndex()方法并传入索引的名称可以删除索

9、并发问题

  • 第一次打开数据库时,添加 onversionchange 事件处理程序非常重要。
  • 另一个同源标签页将数据库打开到新版本时,将执行此回调。
  • 对这个事件最好的回应是立即关闭数据库,以便完成版本升级。
let request, database;
request = indexedDB.open("admin", 1);
request.onsuccess = (event) => {
	database = event.target.result;
	database.onversionchange = () => database.close();
};
复制代码

10、限制

  • 信息不能跨域共享
  • 每个源都有可以存储的空间限制
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享