JavaScript高程笔记——客户端存储

如今愈来愈多的网站是动态网站,经常须要将后端数据传输给前端保存或者更新到页面中,尤为是用户偏好设置,保存在客户端不只能够减小请求耗时,也能下降服务端的压力。javascript

客户端(这里通常指浏览器)目前主要包括三类存储方式:cookie, Web Storage 和 IndexedDB。其中 Web Storage 又包含 Local Storage 和 Session Storage。html

(如今主流浏览器还支持 Web SQL,这里暂时不作介绍...主要是不大了解)前端

1. cookie

cookie,全名叫 HTTP cookie,是第一个客户端存储解决方案,最初用于在客户端存储回话信息。后来主要用在三个方面:java

  • 会话状态管理(如用户登陆状态、购物车、游戏分数或其它须要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

1.1 组成

Cookie是一段不超过4KB的小型文本数据,主要由如下七个部分组成:web

  1. Name:cookie的名称
  2. Value:cookie的值
  3. Path:定义该web站点上能够访问该cookie的目录(或者说路径)
  4. Expires:cookie的有效期,有缺省(会话期cookie)和非缺省(持久性cookie)两种状态
  5. Domain:服务器域名
  6. Secure:指定是否使用HTTPS安全协议发送Cookie
  7. HttpOnly:用于防止客户端脚本经过document.cookie属性访问Cookie

1.2 安全策略

Cookie 目前通常用做存储用户的登陆信息和登陆状态,一般都会设置一个过时时间(若是不设置在浏览器关闭时就会清除该 Cookie),这个时间格式为格林尼治标准时间。可是为了防止信息泄露,这个过时时间通常不会太长,若是用户期间有过再次进入系统,则会更新该过时时间。typescript

这一点与后端的 token 过时相似。数据库

Secure 属性和 HttpOnly 属性能够用来确保 Cookie 被正确发送。设置 Secure 属性,能够指定使用 HTTPS 协议来发送加密请求到服务端,所以能够很好的防止"中间人攻击"。而 HttpOnly 属性能够阻止客户端使用 Document.cookie 来访问带 HttpOnly 属性的 cookie,该属性能够有效的防止 “XSS 跨站点脚本” 攻击。后端

可是 HttpOnly 没法阻止客户端从新写入新的 cookie(新写入的 cookie 也没法设置 HttpOnly 属性)api

Domain 属性和 Path 属性定义了 cookie 的做用域,即 cookie 能够发送给哪些 URL。浏览器

Domain 属性指定了能够接受该 cookie(或者说该 cookie 能够发送给哪些)的主机。不指定时默认是 origin,而且不包含子域名;若是要指定的话,则通常会包含子域名,例如:设置了 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。

Path 属性则定义了主机(服务端)哪些路径能够接受该cookie,而且会包含该路径的全部子路径。

目前的全部主流浏览器(除 IE 8 -)以外,都支持了一个新属性 SameSite

SameSite Cookie 容许服务器要求某个 cookie 在跨站请求时不会被发送,(其中 Site 由可注册域定义),从而能够阻止跨站请求伪造攻击(CSRF)。

SameSite 属性可接收3个值: "None", "Strict" 和 "Lax"。

  • None: 浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。
  • Strict: 浏览器将只在访问相同站点时发送 cookie。(在原有 Cookies 的限制条件上的增强,如上文 “Cookie 的做用域” 所述)
  • Lax: 与 Strict 相似,但用户从外部站点导航至URL时(例如经过连接)除外

1.3 漏洞与缺陷

  1. 僵尸Cookies

“僵尸Cookies” 是指Cookie的一种极端使用,这种类型的 Cookie 很难删除,或者说删除后会自动重建,通常是使用 Web Storage API 或者 Flash 本地共享来达到这种目的的,可是这些技术违反了用户隐私和用户控制的原则。

  1. 恶意 Cookies

这类 Cookie 一般是经过在 Cookie 植入特殊的标记语言,好比 "< >"用来指示该段内容是 HTML 代码,这些代码能够定义网页格式,也能够用来执行代码段等等。

这种攻击方式最多见的就是 XSS(跨站脚本)攻击。

  1. Cookie捕获/重放

指攻击者经过木马等恶意程序,或者跨站脚本等手段窃取用户硬盘或者内存中的Cookies;或者经过在局域网中监听网络通讯、攻击中间的网络路由器等将用户的请求欺骗重定向到攻击者的主机等等手段也能够窃取用户 Cookie。

在捕获(窃取)到用户 Cookie 以后,也能够从新发送(重放) 该 Cookie 到服务器,假冒原用户的身份发起攻击。

  1. 会话定置

“会话定置” 则是像受害者主机注入攻击者控制的恶意 Cookies,使受害者以攻击者的身份登陆网站窃取会话信息;或者伪造与网站同域的站点来欺骗受害者访问该伪造网站等。

  1. CSRF攻击

CSRF(Cross-Site Request Forgery, 跨站请求伪造)攻击,是一种挟制用户在当前已登陆的Web应用程序上执行非本意的操做的攻击方法,指攻击者经过一些技术手段欺骗用户的浏览器去访问一个本身曾经认证过的网站并运行一些操做(如发邮件,发消息,甚至财产操做如转帐和购买商品)。

CSRF攻击并不能直接获取用户帐户的控制权和用户的任何信息,而是欺骗浏览器,让浏览器以用户的名义来执行操做或者请求。

  1. Cookie 的容量很小(大部分浏览器的限制都是不超过 4KB),而且个数也有限,在发送请求时也会跟随在请求头内一块儿发送

因此 Cookie 一般不适合用来存储大容量数据,只做为用户信息和登陆状态的关键字的保存方式,用来实现客户端与服务器之间通讯时的用户认证。

1.4 读取和设置

一般 JavaScript 访问和设置 Cookie 都是使用 document.cookie 来读取和设置。

直接使用 document.cookie 会打印出该站点下全部可访问的 cookie 的 name=value; 格式的字符串,而且只有 name 和 value 两个属性。

使用 document.cookie = "newCookieName=newCookieValue 的时候则会查找站点下全部 Cookie中是否有和 newCookieName 同名的 cookie,有则更新原cookie,没有则新建一个 cookie 。

2. Web Storage

Web Storage 最初的目标有两个:一个是提供一个除 Cookie 外新的会话数据存储途径;另外一个是提供跨会话持久化存储大容量数据的机制。

Web Storage 分为两类:LocalStorage 和 SessionStorage,09年以后这两个对象都在 window 对象上提供了一个访问地址,可直接使用 window.localStorage 或者 window.sessionStorage 来访问对应的 Storage。

这两种方式最大的不一样在于:LocalStorage 是永久存储机制,SessionStorage 是跨会话的存储机制。

LocalStorage 和 SessionStorage 都是特定于页面协议的。

2.1 LocalStorage

LocalStorage 在浏览器上体现为 window 对象的一个只读属性,而且这个属性的值指向浏览器的 localStorage 对象,容许脚本访问当前 Document 源(同域名同端口,而且不包含子域名)下的 Storage。

LocalStorage 主要用于持久化存储数据,其中的数据老是以键值对的形式存储的,而且全部的键和值都会自动转为字符串形式

使用方式

获取当前源下的 localStorage 对象能够直接使用一个变量接收:

const myLocalStorage = window.localStorage; // 通常状况下 window 能够省略
复制代码

这个对象提供了四个方法,用来增删改查某个键值对和清空本地 localStorage

const myLocalStorage = window.localStorage;

myLocalStorage.setItem('newStorage', 'newValue'); // 添加/修改

let newValue = myLocalStorage.getItem('newStorage'); // 读取

myLocalStorage.removeItem('newStorage'); // 移除
 
myLocalStorage.clear(); // 清空
复制代码

2.2 SessionStorage

sessionStorage 对象主要只用来存储会话数据,只会存储到浏览器关闭。

sessionStoragelocalStorage 同样, 浏览器上都体现为 window 对象的一个只读属性,而且这个属性的值指向浏览器的 sessionStorage 对象,容许脚本访问当前 Document 源(同域名同端口,而且不包含子域名)下的 Storage。

注意:

1.同源的不一样标签页不必定会共享 sessionStorage,只有经过点击页面内连接,或者使用 window.open 打开的新的同源标签页才会和原来的页面共享一个 sessionStorage。直接经过地址栏输入地址打开的页面,即便地址同样,可是也是不一样的 sessionStorage

2.在浏览器中刷新页面并不会丢失以前设置的 sessionStorage

使用方式

sessionStoragelocalStorage 同样,都提供了四个方法用来增删改查和清空本地数据。

const mySessionStorage = window.localStorage;

mySessionStorage.setItem('newStorage', 'newValue'); // 添加/修改

let newValue = mySessionStorage.getItem('newStorage'); // 读取

mySessionStorage.removeItem('newStorage'); // 移除
 
mySessionStorage.clear(); // 清空
复制代码

2.3 事件

每当 Storage 对象发生变化时( sessionStoragelocalStorage 上的任何更改),都会在文档上触发 storage 事件。

这个事件对应的事件实例对象有4个属性:

  1. domain:本地存储对应的域
  2. key:被新增/修改/删除的键名
  3. newValue:被设置的新值,删除时为null
  4. oldValue:变化以前的值
window.addEventListener("storage", event => alert('Storage changed for ${event.domain}'));
复制代码

须要注意的是,这个事件并不会区分是 sessionStorage 仍是 localStorage 发生的改变。

2.4 异同点

由前面的内容能够了解到,sessionStoragelocalStorage 最大的区别就是存储时效的不一样,当咱们在须要作本地数据的持久化时,一般会将数据(例如用户的个性化配置等)保存在 localStorage 中。

localStoragesessionStorage 在存储量上大体相同,上限都是 5MB 左右,具体状况根据浏览器的不一样可能略有出入;而且这二者并不会跟随 http 网络请求被发送;在设计时也为用户(开发者)提供了良好易用的 API 和事件。

3. IndexedDB

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

3.1 核心概念

IndexedDB 是一个事务型数据库系统,相似于基于 SQL 的 RDBMS(Relational Database Management System,关系数据库管理系统)。可是 IndexedDB 是基于 JavaScript 的面向对象的数据库,主要用来存储和检索用 为索引的 对象,而且支持二进制数据。

IndexedDB 在设计的时候几乎全是异步结构,因此在操做(调用)IndexedDB 的时候基本上都是以请求的形式执行,而且会返回成功时的结果或者失败时的错误。

IndexedDB 支持事务,意味着在一个完整的操做过程当中,只要发生错误,便会退回到该事务发生以前的状态,避免数据的部分改变。

IndexedDB 跟 Web Storage 同样也受同源策略的限制,可是同源下不一样标签页会共享存储数据与相关事件(这一点可能会形成并发问题,后面会讲解)。

3.2 如何使用

IndexedDB 是一个比较复杂的 API,涉及很多概念。它把不一样的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各类对象接口。

  • 数据库:IDBDatabase 对象,存储一系列数据的容器对象
  • 对象仓库:IDBObjectStore 对象,相似关系数据库的表格
  • 索引: IDBIndex 对象
  • 事务: IDBTransaction 对象
  • 操做请求:IDBRequest 对象
  • 指针: IDBCursor 对象
  • 主键集合:IDBKeyRange 对象
  • ...

---- 摘自 阮一峰的网络日志: 浏览器数据库 IndexedDB 入门教程

使用 IndexedDB 的基本模式就是:

  1. 打开/建立 数据库
  2. 在数据库中建立一个对象仓库
  3. 启动一个事务,并发送一个事务请求来执行对应的数据库操做
  4. 经过监听对应类型的 DOM 事件来等待或者判断数据库操做的执行结果
  5. 根据操做结果进行后续操做

根据以上步骤,能够演示一个基础的 IndexedDB 数据库操做流程。

第一步: 打开/建立数据库

const IndexedDB = window.indexedDB;

let db, dbRequest;

dbRequest = indexedDB.open("test", 1);
dbRequest.onerror = event => (alert(`Failed to open: ${event.target.errorCode}`));
dbRequest.onsuccess = event => (db = event.target.result);
复制代码

这里首先是建立一个打开(在不存在 test 这个数据库的时候则是建立)数据库的 IDBRequest 请求实例,并在这个实例上添加 onerroronsuccess 事件处理的回调函数,来处理请求失败或者成功时的事件(几乎全部的请求都须要处理这两个事件的回调函数)。

open(dbName: String, version: Number = 1) 能够接受第二个正整数参数 version,用来指定对应数据库的版本,若是已经存在了这个数据库的话,则会将该数据库进行升级(第二步会对升级形成的影响进行说明)。

第二步: 建立数据仓库

在第一步的 open 操做中,会建立一个新的数据库(或者读取原有的仓库,后面为了简洁,都会省略获取部分,除非有特殊说明),而且在建立完成后,会触发一个 upgradeneeded 事件。 open 操做返回的请求实例 dbRequest 中也会有一个 upgradeneeded 属性。咱们能够经过设置该属性为一个回调函数,在回调中建立须要的数据仓库。

dbRequest.onupgradeneeded = function(event) {
    // 保存 IDBDataBase 接口
    let db = event.target.result;

    // 首先利用 contains 判断是否已经存在 person 仓库
	if (!db.objectStoreNames.contains('person')) {
        // 为该数据库建立一个对象仓库
        let objectStore = db.createObjectStore("person", { keyPath: "myKey" });
    }
};
复制代码

这一步完成以后,咱们就在这个数据库中建立了一个名字叫 person 的数据仓库,相似关系数据库中的一个数据表。

createObjectStore(storeName: String, options: Object) 方法接收两个参数,第一个是仓库名,第二个是键的配置项,可配置的有两个属性: keyPathautoIncrement

keyPath 属性可设置一个字符串值,表示该数据仓库下的每条数据使用哪个字段来做为键。

autoIncrement 属性可设置一个布尔值,表示是否开启键生成器;开启时默认从1开始,新数据会在以前的键的基础上加1做为新的键,且不会减少。

这两个属性通常只设置一个,生成或者选取的键相似于关系表中的“主键”,或者说“索引”

第3、4、五步:建立事务进行数据库操做并根据返回结果进行后续操做

在数据库和数据仓库都建立完成以后,剩下的全部操做几乎都须要用事务来完成。每个系列操做,都会对应一个事务实例。

实例化是一个事务使用 db.transaction() 方法:

// 为了说明实例化方法须要的参数,这里用 TS 来举例
let myTransaction: IDBTransacation = db.transaction(storeNames: string[], mode?: "readonly" | "readwrite" | "versionchange", options?: { error(): void });
复制代码

mode 参数的不一样值对应不一样的操做权限,具体内容请查看 MDN IDBTransactionMDN IndexedDB 增长、读取和删除数据

由于事务自己也是一个请求,因此也须要配置对应的事件处理函数 onseccessonerror,除了这两个事件外事务还有另一个事件处理函数 oncomplete,这个函数一般用来代替 onsuccess 用来处理事务请求成功以后的事件(某些事务没法经过 oncomplete 事件返回的 event 对象来访问数据时,则仍是只能用 onsuccess)。

可使用 add()put() 方法添加和更新对象,使用 get() 取得对象,使用 delete() 删除对象,使用 clear() 删除全部对象。其中,get()delete() 方法都接收对象键做为参数,这 5 个方法都建立新的请求对象。

下面是经过事务来进行增删改查操做的例子:

const transaction = db.transaction(['person'], "readwrite");
const objectStore = transaction.objectStore('person');

const errorFunction = () => console.log('事务处理失败');

// 读取
const read = function() {
    const readRequest = objectStore.get(1);
    readRequest.onerror = errorFunction;
    readRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('Name: ' + request.result.name);
            console.log('Age: ' + request.result.age);
        } else {
            console.log('未得到数据记录');
        }
    };
}

// 增长
const add = function() {
    const addRequest = objectStore.add({ id: 1, name: '张三', age: 24 });
    addRequest.onerror = errorFunction;
    addRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('数据写入成功');
        } else {
            console.log('数据写入失败');
        }
    };
}

// 修改
const update = function() {
    const updateRequest = objectStore.put({ id: 1, name: '张三', age: 24 });
    updateRequest.onerror = errorFunction;
    updateRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('数据修改为功');
        } else {
            console.log('数据修改失败');
        }
    };
}

// 删除
const delete = function() {
    const deleteRequest = objectStore.delete(1);
    deleteRequest.onerror = errorFunction;
    deleteRequest.onsuccess = function(event) {
        if (request.result) {
            console.log('数据删除成功');
        } else {
            console.log('数据删除失败');
        }
    };
}

// 遍历
const traverse = function() {
    const traverseRequest = objectStore.openCursor();
    traverseRequest.onerror = errorFunction;
    traverseRequest.onsuccess = function(event) {
        console.log('遍历结束');
    };
}

// 清空
const clear = function() {
    const clearRequest = objectStore.clear();
    clearRequest.onerror = errorFunction;
    clearRequest.onsuccess = function(event) {
        console.log('清空成功');
    };
}
复制代码
相关文章
相关标签/搜索