浏览器本地存储

前言

浏览器存储数据的几种方式

  • Cookies
  • Local Storage & Session Storage
  • WebSQL & IndexedDB

HTTP Cookie(也叫 Web cookie 或者浏览器 Cookie)是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。比较经典的,可以它用来确定两次请求是否来自于同一个浏览器,从而能够确认和保持用户的登录状态(类似 token)。对于购物网站而言,cookie 是非常重要的,为了实现购物车功能,把已选物品加入 cookie,可以实现不同页面之间数据的同步,同时在提交订单的时候又会把这些 cookie 传到后台。

Cookie 主要用在以下三个方面:

  • 会话状态管理(如用户登录状态、购物车)
  • 个性化设置(如用户自定义设置)
  • 浏览器行为跟踪(如跟踪分析用户行为)

当服务器收到 HTTP 请求(request)时,可以在响应头(headers)里面增加一个 Set-Cookie 头部。浏览器收到响应(response)之后会取出 Cookie 信息并保存,之后对该服务器每一次请求中都通过 Cookie 请求头部将 Cookie 信息发送给服务器。另外,Cookie 的过期时间、域、路径、有效期、站点都可以根据需要来指定
格式:

Set-Cookie: <cookie 名称> = <cookie 值 >

服务器告诉客户端要保存 Cookie 信息, 响应的数据里面应该包含 Set-Cookie 头,浏览器收到之后会将 Cookie 保存,比如:

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie = choco
Set-Cookie: tasty_cookie = strawberry

对该服务器发起的每一次新的请求,浏览器都会将之前保存的 Cookie 信息通过 Cookie 请求头发送给服务器

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie = choco; tasty_cookie = strawberry

2. 类型

  • 会话期 Cookie (session cookie)
    会话期 Cookie 是最简单的 Cookie:浏览器关闭之后它会被自动删除
  • 持久 Cookie
    持久 Cookie 可以指定一个特定的过期时间(Expires)或者有效期(Max-Age)
Set-Cookie: name = SmartestEE; expires = Sat, 02 May 2017 23:38:25 GMT // max-age = 3600 * 72
  • 安全类型 Cookie
    安全类型可以设置 secure 选项,该选项只是一个标记而没有值。只有在使用 SLL 和 HTTPS 协议向服务器发起请求时,才能确保 Cookie 被安全地发送到服务器。
Set-Cookie: name = Nicholas; secure
  • HttpOnly 类型 Cookie
    HttpOnly 类型可以设置 HttpOnly 选项,HTTP-only 类型的 Cookie 不能使用 Javascript 通过 Document.cookie 属性来访问,从而能够在一定程度上阻止跨域脚本攻击(XSS)。HttpOnly 标志并没有给你提供额外的加密或者安全性上的能力,当整个机器暴露在不安全的环境时,切记绝不能通过 HTTP Cookie 存储、传输机密或者敏感信息。JavaScript 可以通过跨站脚本攻击(XSS)的方式来窃取 Cookie
Set-Cookie: name = Nicholas; HttpOnly

Domain 和 Path 指令定义了 Cookie 的作用域,即需要发送 Cookie 的 URL 集合。

  • Domain 指令规定了需要发送 Cookie 的主机名。如果没有指定,默认为当前的文档地址上的主机名(但是不包含子域名)。如果指定了 Domain,则一般包含子域名。

    如果设置了 Domain=mozilla.org,则 Cookie 包含在子域名中(如 developer.mozilla.org)。

  • Path 指令表明需要发送 Cookie 的 URL 路径。字符 %x2F (即 “/“) 用做文件夹分隔符,子文件夹也会被匹配到。

    如设置 Path=/docs,则下面这些地址都将匹配到:”/docs”,”/docs/Web/“,”/docs/Web/HTTP”

通过 Document.cookie 属性可以来创建新的 Cookie,也能够通过该属性来访问未被指定 HttpOnly 标志的 Cookie。

document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// logs "yummy_cookie=choco; tasty_cookie=strawberry"

一个完整支持 unicode 的 cookie 读取 / 写入器

var docCookies = {
  // docCookies.getItem(name), 读取一个 cookie。如果 cookie 不存在返回 null
  // encodeURIComponent 转义除了字母、数字、(、)、. 、! 、~ 、* 、' 、- 和 _ 之外的所有字符
  getItem: function (sKey) {
    return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
  },

  // docCookies.setItem(name, value, [end], [path], [domain], [secure]), 创建或覆盖一个 cookie
  // end (可选) 最大时间的秒数 (一年为 31536e3, 永不过期的 cookie 为 Infinity) ,或者过期时间的 GMTString 格式或 Date 对象; 如果没有定义则会在会话结束时过期 (number – 有限的或 Infinity – string, Date object or null)。
  setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
    if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
      return false;
    }
    var sExpires = "";
    if (vEnd) {
      switch (vEnd.constructor) {
        case Number:
          sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
          break;
        case String:
          sExpires = "; expires=" + vEnd;
          break;
        case Date:
          sExpires = "; expires=" + vEnd.toUTCString();
          break;
      }
    }
    document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ?"; path="+ sPath :"") + (bSecure ? "; secure" : "");
    return true;
  },

  // docCookies.removeItem(name, [path], domain), 删除一个 cookie
  removeItem: function (sKey, sPath, sDomain) {
    if (!sKey || !this.hasItem(sKey)) {
      return false;
    }
    document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ?"; path="+ sPath :"");
    return true;
  },

  // docCookies.hasItem(name), 检查一个 cookie 是否存在
  hasItem: function (sKey) {
    return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
  },

  // docCookies.keys(), 返回一个这个路径所有可读的 cookie 的数组
  keys: function () {
    var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
    for (var nIdx = 0; nIdx < aKeys.length; nIdx++) {
      aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
    }
    return aKeys;
  }
};

用法示例:

docCookies.setItem("test0", "Hello world!");
docCookies.setItem("test3", "Hello world!", new Date(2027, 2, 3), "/blog");
docCookies.setItem("test5", "Hello world!", "Tue, 06 Dec 2022 13:11:07 GMT", "/home");
docCookies.setItem("test6", "Hello world!", 150);
docCookies.setItem("test7", "Hello world!", 245, "/content");
docCookies.setItem("test8", "Hello world!", null, null, "example.com");
docCookies.setItem("test9", "Hello world!", null, null, null, true);
docCookies.setItem("test1;=", "Safe character test;=", Infinity);
  • cookie 会随着每次 HTTP 请求头信息一起发送,无形中增加了网络流量
  • cookie 能存储的数据容量有限,根据浏览器类型不同而不同,通常只有 4KB。所有超出该限制的 cookie 都会被截掉并且不会发送至服务器
  • 每个域名下的 cookie 数量也有限制,根据浏览器类型不同而不同

HTML5 的 DOM 存储分成两种:SessionStorage 和 LocalStorage

DOM 存储的机制是通过存储字符串类型的键 / 值对。

  • SessionStorage 是一种会话级别的缓存,关闭浏览器会数据会被清除。它的作用域是窗口级别的,也就是说不同窗口间的 sessionStorage 数据不能共享的。
  • LocalStorage 是持久化存储,不会自动删除
属性方法说明
.length返回 storage 中的键值对个数
.key(n)返回 storage 中第 n 个元素对的键值(第一个元素是 0)
.getItem(key)返回键值 key 对应的值
.key返回键值 key 对应的值
.setItem(key, value)添加数据,键值为 key,值为 value
.removeItem(key)移除键值为 key 的数据
.clear()清除所有数据

特点:

  • Storage 提供几 MB 的空间,根据浏览器类型不同而不同,chrome 为 5MB。
  • 键值对为文本类型,存储对象时要配合 JSON.stringify() 和 JSON.parse() 使用。
  • 不同于 cookie,Storage 的访问限制更高一些,只有当前设定 Storage 的域名下才能访问。
    SessionStorage 是以 tab 为级别的 session,刷新页面可以访问到之前的 sessionStorage,关闭再打开页面,无法访问到之前的 sessionStorage。
    LocalStorage 两种情况下都可以访问,而且下次再打开浏览器仍可以访问
  • 低版本浏览器不支持。

WebSQL & IndexedDB

websql 的标准,官方已经不打算维护了,转而维护了新的 indexeddb,但是 websql 兼容性好,而且是移动端几乎完全可用。indexeddb 的兼容性没那么好,android4.4 之前以及 ios7 以前都无法直接使用,但可以用 polyfill 脚本做移动端兼容。

各浏览器兼容性

websql 更像是关系型数据库,并且使用 sql 语句进行操作。

indexeddb 更像是 nosql(非关系型数据库),直接使用 js 的方法操作数据即可。

  • 也是永久存储
  • 访问限制性:indexeddb 和 websql 均是在创建数据库的域名下才能访问。而且不能指定访问域名。
  • 两种存储的方式是没有大小限制的

WebSQL 三个核心方法:

  • openDatabase:这个方法使用现有的数据库或者新建的数据库创建一个数据库对象
  • transaction:控制一个事务,以及基于这种情况执行提交或者回滚
  • executeSql:执行实际的 SQL 查询
var db = openDatabase(' 数据库名称 ', '版本号 ', '描述文本 ', 数据库大小, [创建回调]);

例子:

var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024);
var msg;

// 插入
db.transaction((tx) => {
    tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
    tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1,"SmartestEE")');
});

// 更新
db.transaction(function (tx) {
    tx.executeSql('UPDATE LOGS SET log=\'test\'WHERE id=1');
});

// 读取
db.transaction((tx) => {
tx.executeSql('SELECT * FROM LOGS', [], (tx, results) => {
    var len = results.rows.length, i;
    for (i = 0; i < len; i++){
        msg = "<p><b>" + results.rows.item(i).log + "</b></p>";
        document.querySelector('#status').innerHTML +=  msg;
    }
}, null);
});

// 删除
db.transaction((tx) => {
    tx.executeSql('DELETE FROM LOGS  WHERE id=1');
});

IndexedDB

特点:

  1. 键值对储存。内部采用对象空间(object store)存放数据,支持所有 js 类型的数据
  2. 异步。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的
  3. 支持事务。事务中一步出错整个事务都会回滚
  4. 同域限制。只能自身域名创建的 indexedDB 才可以访问
  5. 储存空间大。单个域名下的数据库超过 50M 的时候浏览器会弹窗向用户请求,不影响之后继续存储。
  6. 支持二进制储存。也就是可以存储图片和文件,用 IndexedDB 存储图片和文件
  • 判断是否可用
var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;

if (!indexedDB) {
  console.log('indexedDB 不可用 ')
}
  • 打开数据库
var local = indexedDB.open(' 数据库名 ', [版本号]);
var db;

local.onerror = (ect) => {
  // 打开错误
}

local.onsuccess = (evt) => {
  // 打开成功,定义一个数据库对象
  db = evt.target.result;
  // 也可以 db = local.result
}
  • 创建和更新数据库版本号
    要更新数据库的 schema,也就是创建或者删除对象存储空间,需要实现 onupgradeneeded 处理程序,这个处理程序将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。
local.onupgradeneeded = (evt) => {
  db = evt.target.result;
  db.createObjectStore('objectStore', { keyPath: "chatID"}); // 创建一个对象空间,keyPath 指定唯一的 key,再加上 autoIncrement: true 自动增加 key

  objectStore.createIndex("name", { unique: false});
  // createIndex 方法创建索引, 配合 index() 方法实用方便查询
}

在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建。版本号是一个 unsigned long long 数字, 不能用浮点数。

  • 操作数据
    操作数据前都得定义一个事务,第一个参数数组指定这个事务跨越哪些对象存储空间,第二个参数指定模式(不加默认只读),事务具有三种模式(只读,读写,和版本变更),只读事务可以并发运行 。
transaction.oncomplete = function(event) {
  // 当所有的数据都操作完成时执行一些操作
};

transaction.onerror = function(event) {
  // 错误处理!
};
var request = db.transaction(["objectStore"], "readwrite").objectStore("objectStore").add({chatID: chatID, messageList: list});

request.onerror = (evt) => { }
request.onsuccess = (evt) => { }
var store = db.transaction(["objectStore"], "readwrite").objectStore("objectStore");
var request = store.delete(key); // delete(key) 删除指定数据,store.clear() 清空整个对象空间,db.deleteObjectStore('objectStore') 删除对象空间 (得在 onupgradeneeded 方法中使用),indexedDB.deleteDatabase(" 数据库名称 ") 删库

request.onsuccess = (evt) => { }
var request = db.transaction(["objectStore"], "readwrite").objectStore("objectStore").get(key);

request.onerror = (evt) => {
  // 错误处理
};

request.onsuccess = (evt) => {
  // 对 request.result 或者 evt.target.result 做些操作!
  console.log(request.result);
};
var store = db.transaction(["objectStore"], "readwrite").objectStore("objectStore");
var request = store.get(key);

request.onerror = (evt) => {
  // 错误处理
};

request.onsuccess = (evt) => {
  // 对 request.result 或者 evt.target.result 做些操作!
  let data = evt.target.result;
  data.messageList = [{chatID: '123456', message: 'hello guys'}];
  store.put(data);
};
  1. 遍历

openCursor 方法,它在当前对象仓库里面建立一个读取光标(cursor)。

openCursor 方法还可以接受第二个参数,表示遍历方向,默认值为 next,其他可能的值为 prev、nextunique 和 prevunique。后两个值表示如果遇到重复值,会自动跳过。

var t = db.transaction(["test"], "readonly");
var store = t.objectStore("test");
var cursor = store.openCursor();

cursor.onsuccess = function(e) {
    var res = e.target.result;
    if(res) {
        console.log("Key", res.key);
        console.dir("Data", res.value);
        res.continue();
    }
}
  • 关闭数据库
db.close()

用于一个 react 聊天项目的本地消息记录存储的 IndexedDB 使用示例

封装的函数用法跟 localStorage 的属性方法类似

IndexedDB.js

var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
var userID = localStorage.getItem('userID'), db;

indexedDB.open('LocalMessageDataBase', 1).onupgradeneeded = evt => {
  // console.log('establish database');
  db = evt.target.result;
  db.createObjectStore(userID, { keyPath: 'chatID' });
};

export function getLocalData(chatID) {
  let localDB = indexedDB.open('LocalMessageDataBase', 1), data = [];

  return new Promise((resolve, reject) => {
    localDB.onsuccess = evt => {
      // console.log('open database to getdata');
      db = evt.target.result;
      if (chatID) {
        let request = db
          .transaction([userID])
          .objectStore(userID)
          .get(chatID);

        request.onerror = evt => {
          console.log('get error:', evt);
          reject(evt);
        };

        request.onsuccess = evt => {
          // console.log('get data:', evt.target.result);
          data = request.result || [];
          resolve(data);
        };
      }
    };
  });
}

export function setLocalData(chatID, list) {
  let localDB = indexedDB.open('LocalMessageDataBase', 1);

  localDB.onsuccess = evt => {
    // console.log('open database to setdata');
    db = evt.target.result;
    let store = db.transaction([userID], 'readwrite').objectStore(userID);
    let request = store.get(chatID);

    request.onerror = evt => {
      throw evt;
    };

    request.onsuccess = evt => {
      let data = evt.target.result;

      if (data) {
        // console.log('update data');
        data.messageList = list;
        store.put(data);
      } else {
        // console.log('add data');
        store.add({ chatID: chatID, messageList: list });
      }
    };
  };
}

export function deleteLocalData(chatID) {
  let localDB = indexedDB.open('LocalMessageDataBase', 1);

  localDB.onsuccess = evt => {
    db = evt.target.result;
    let request = db
      .transaction([userID], 'readwrite')
      .objectStore(userID)
      .delete(chatID);

    request.onerror = evt => {
      console.log('delete error', evt);
    };

    request.onsuccess = evt => {
      console.log('delete data');
    };
  };
}

调用函数只需引入 IndexedDB.js

import {getLocalData, setLocalData, deleteLocaldata} = 'path/IndexedDB';

getLocalData(chatID).then((data) => {
  console.log(data); // 获取聊天记录
})

setLocalData(chatID, list); // 存入新的聊天消息记录

deleteLocaldata(chatID); // 删除指定对象的消息记录
文章目录
  1. 前言
  2. Cookie
    1. 1. 创建 cookie
    2. 2. 类型
    3. 3. Cookie 的作用域
    4. 4. JavaScript 通过 Document.cookies 访问设置 Cookie
    5. 5. cookie 缺陷
  3. HTML5 的 DOM 存储分成两种:SessionStorage 和 LocalStorage
  4. WebSQL & IndexedDB
    1. WebSQL 三个核心方法:
    2. IndexedDB
  5. 用于一个 react 聊天项目的本地消息记录存储的 IndexedDB 使用示例