Web Storage

Agenda

  • Web Storage
    • 什么是 Web Storage ?
    • 浏览器支持情况
    • 特性检测
    • Web Storage 快速示例
    • 使用 Web Storage
      • 删除数据项
      • 查找所有数据项
      • 保存数值
      • 保存对象
      • 响应存储变化

什么是 Web Storage ?

Web 应用的数据可以保存在两个地方,一个是 Web 服务器,一个是 Web 客户端(用户的计算机,浏览器的一个目录)。这两个地方都有各自适合保存的数据:

  • 敏感信息,以及不希望别人篡改的数据,需要在服务端处理的数据等,需要一直保存在 Web 服务器 - 安全,高效,可靠
  • 一些不太重要的信息 - 用户偏好,应用状态;需要在 session 内(不同页面间)共享的数据;离线状态下需要保存的用户数据,需要放在用户本地

HTML5 之前我们想要在用户本地保存信息,基本上只能使用 cookie(主要是在浏览器和服务器之间保存身份信息)。cookie 保存少量数据很方便,但是它也有一些不好的地方。

Cookie 的问题

cookie 可以用来持久化存储少量的数据,但是它具有一些问题:

  • 操作 cookie 的语法不够优雅,看起来比较繁琐。JavaScript Cookies
  • 每一次 HTTP Request 都要来来回回传递 cookie,会使 Web application 变慢。
  • cookie 不加密的安全性问题;容易被垃圾广告利用,跟踪信息 ...
  • 要处理过期的 cookie 数据
  • 信息量最大不能超过 4KB,对于实用性而言太小了。

本地存储的需求

我们真正需要的本地存储是:

  • 在访客的计算机上保存数据更加方便 - 最好通过几个简单的 JavaScript 对象就可以对它们进行操作(增删改查)
  • 还要有充裕的存储空间 - 最好能到几兆甚至几十兆
  • 而且可以无限期的保存在用户计算机,即使在页面刷新的情况下也能将存储的数据持久化
  • 最后存储的数据不需要每次都发送给服务器,除非我们自己想要把它发到服务器

HTML5 新增的本地存储功能,都可以满足我们真正想要的存储!这个就叫做 Web Storage, 通过它的特点我们可以看出这个功能特别适合开发离线应用。离线应用的数据可以“自给自足”,无论用户能否上网,都可以在本地保存用户信息。

Web Storage 介绍和基本概念

  • HTML5 的 Web Storage 可以让 Web 页面在客户端浏览器中以键值对的形式在本地存储数据。
  • 这些数据可以是临时的(浏览器一关就自动删除),或者是长期存在的(无论多少天之后打开网站,仍然可以访问这些数据)。
  • Web Storage 的意思是供 Web 使用的 Storage,不是存储在 Web 上的,都是存储在本地的。所以某些浏览器厂商又把它叫做“本地存储”或者“DOM存储”。
  • Web Storage 和很多其他的规范一样,原先是 HTML5 规范的一部分,后来被 W3C 归类划分给抽离出来形成了单独的一份标准。

两种 Web Storage

Web Storage 又分为两种,分别对应两个 JavaScript 对象:

  • 本地存储,对应的是 localStorage 对象。用于长期保存的数据,浏览器关闭几天以后再打开数据还在。
  • 会话存储,对应的是 sessionStorage 对象。用于临时保存针对一个窗口(或标签页)的数据。在访客关闭窗口或是标签页之前,这些数据是存在的;关闭之后,浏览器就会删除这些数据。只要不关闭,哪怕跳转到别的网站再跳回来,数据还是存在的。

从代码的角度来看,localStoragesesseionStorage 的使用完全相同,不同的是数据寿命。从名字上我们就可以看出,sessionStorage 主要用于保存哪些要从一个页面传递到下一个页面的数据;localStorage 主要用于保存希望用户未来还能看到和用到的数据,比如用户偏好。

Web Storage 的数据访问

localStoragesessionStorage 都是和用户所在的域联系在一起的。也就是只有同一个域的页面才可以访问共同的数据。

另外同样因为数据是保存在同一台计算机的同一个浏览器中,因此只有从同一台计算机的同一个浏览器才能够访问到同样的本地数据。

localStorage的大小:大概是 5 MB。需要更大的空间可以考虑 IndexedDB,起步 50 MB,如果用户同意还可以更大。

浏览器支持情况

Firefox 甚至能在浏览器 crash 之后再恢复的时候把 sessionStorage 也恢复了!

http://caniuse.com/#search=webstorage

特性检测

function supports_html5_webstorage() {
	return ('localStorage' in window) && window['localStorage'] != null;
}     	

也可以使用 Modernizr 库进行检测

if (Modernizr.localStorage) {
	// window.localStorage 可用!
} else {
	// 没有本地HTML5存储支持
}    	

存储数据

Web Storage 是基于 key/value 形式的,存储和检索数据都通过指定的 key。基本语法如下:

      	localStorage[keyname] = data;
      

比如我们想保存用户名,那么 key 就可以叫做 user_name:

      	// 取得文本框
				var userName = document.querySelector('#userName');
        
        // 保存文本框中的用户名
      	localStorage['user_name'] = nameInput.value;
      

读取用户数据和保存用户数据一样简单:

      	alert("你保存的是:" + localStorage['user_name']);
      

检测某个键值是否为空

有可能某个 key 下面并没有保存数据。要检测某个 key 的值是否为空,可以直接检测它是否等于 null:

	var userName = document.querySelector('#userName');
	if (localStorage['user_name'] == null) {
		userName.value = "没有找到哦";
	} else {
		userName.value = localStorage['user_name'];
	}
      

使用 sessionStorage

sessionStorage 的使用也非常简单,只是要使用 sessionStorage 对象:

      	// 取得 Email 地址
        var email = document.getElementById('emailAddress');
        
        // 保存 Email 地址
        sessionStorage['email_address'] = email;
      

Web Storage 测试注意的问题

  • 测试 Web Storage 时,很多浏览器中只有从 Web 服务器上打开的页面才能够读写 Web Storage,无论服务器是本地还是远程的。关键是不能从本地硬盘直接打开文件测试。
  • 问题就在于前面提到的,Web Storage 存储空间是和域绑定在一起的,需要据此进行限制。
  • 从本地硬盘直接打开 Web Storage 页面的会发生什么情况要视浏览器而定。有可能完全不支持 Web Storage 了,localStorage 和 sessionStorage 都不见了(IE);也有可能可以使用,但是所有操作都会失败(Firefox);还有可能是大部分功能都支持,但是有些功能(比如 onStorage 事件)失效。
  • 所以我们最好都架一个服务器来进行这些功能的测试,类似情况还发生在 File API身上。

使用 Developer Tools 调试 Web Storage

可以使用 Chrome 的 Developer Tools -> Resource -> Local Storage / Session Storage 查看目前浏览器存储的内容,也可以通过 Developer Tools 删除指定的 key/value 对(甚至可以直接添加):simplestorage.html

另外两种写法

除了像这样写:

      	localStorage['user_name'] = userName.value;
      

还可以这样:

      	localStorage.user_name = userName.value;
      

或这样:

      	localStorage.setItem('user_name', userName.value);
      

建议按第一种写就好了。

使用 Web Storage

删除,检索,处理不同的数据类型和响应存储变化

删除数据项

删除数据项很简单,只要调用 removeItem() 方法,传入键名,就可以删除不想要的数据项:

      	localStorage.removeItem("user_name");
      

然后还有清空网站在本地保存的所有数据的 clear() 方法:

      	sessionStorage.clear();
      

查找所有数据项

要检索一个数据项,只要知道键名就可以了。不知道任何键名的话,可以使用 key() 方法从 localStoragesessionStorage 中取得当前网站保存的所有数据项。key.html

    function findAllItems() {
        // 取得用于显示数据项的 <ul> 元素
        var itemList = document.querySelector('#itemList');
        // 清空列表
        itemList.innerHTML = '';        
        // 遍历所有数据项
        if (localStorage) {
            for (var i=0; i<localStorage.length; i++) {
                // 取得当前位置数据项的 key
                var key = localStorage.key(i);                
                // 取得该键保存的数据值
                var item = localStorage[key];                
                // 用以上数据项创建一个列表项,并添加到页面中
                var newItem = document.createElement('li');
                newItem.innerHTML = key + ': ' + item;
                itemList.appendChild(newItem);
            }
        }
    }    
      

保存数值

通过 localStoragesessionStorage 保存数据时,该数据会自动转换为文本。对于数值就可能有问题:

    localStorage['age'] = 20;
    age = localStorage['age'];
    age += 10;
    // 结果是 2010,因为取出来的也是文本,相加的时候只要有一个文本,js 就会把它作为文本相加
    alert(age);      

正确的写法是:

	age = Number(localStorage['age']);
	age += 10;
	// 这样就可以正确的计算出结果是 30
    

保存对象

数值类型可以使用 Number() 函数转换,文本和日期可以自动转换,或者设定好标准格式进行转换。

对象也会被转换成字符串存储,这时候需要用到 JSON。否则的话就会存成字符串 object 了。

    var person = {
        name: 'tom',
        age: 30,
        job: 'Software Engineer'
    };
    
    sessionStorage['person'] = JSON.stringify(person);
    
    this_person = JSON.parse(sessionStorage['person']);
    alert(this_person.name);
    alert(this_person.age);
    alert(this_person.job);
    

响应存储变化 - storage 事件

通过捕获 storage 事件可以跟踪存储区的改动,也就是说任何时候调用 setItem(), removeItem() 或者 clear() 方法时,如果真的发生了数据改动,都会在 window 对象上触发storage 事件。

这样我们就可以在同一个页面,或者同一站点的不同页面知道存储发生了变化。还记得前面的遍历所有数据项的 key.html 吗?我们把它稍微改造一下,让 storage 事件来自动触发 findAllItems() 方法。这样存储有变化时页面就会自动更新。onstorage.html

if (window.addEventListener) {
	window.addEventListener('storage', findAllItems, false);
} else {
	window.attachEvent('onstorage', findAllItems);
}
      

StorageEvent 对象

onStorage 的回调函数被调用时,会传入一个 StorageEvent 对象作为参数,IE下面的事件对象存储在 window.event上:

function findAllItems(e) {
	if (!e) { e = window.event; }
}
      

StorageEvent 对象的属性

属性类型描述
key字符串加入,修改,或者删除的键名
oldValue任意之前的数据(如果是被覆盖的情况)或者null(如果有新数据项加入)
newValue任意新数据或者null(如果数据项被删除)
url字符串调用这个触发数据区变动的函数所在的页面地址

storage 事件是无法撤销存储区的改动的。在回调方法中,没有办法停止正在发生的对存储区的改动。

Web Storage 限制

  • 每个域默认拥有5MB或2.5MB存储空间。
  • 数据最终是以字符串形式,而不是它原来的数据类型进行存储。(例如存储大型整数或者浮点数时,在存储区,会把每个数字存储为单个字符)
  • 存储数据如果超过存储空间的配额,就会抛出 QUOTA_EXCEED_ERR异常

IndexedDB介绍

什么是IndexedDB?

  • Indexed Database API, 或者简称IndexedDB,是在浏览器中保存结构化数据的一种数据库。为了替代目前已经被废弃的Web SQL Database API。
  • IndexedDB的思想是创建一套API,方便保存和读取JavaScript对象,同时还支持查询及搜索。
  • IndexedDB设计的操作完全是异步进行的。几乎每一次IndexedDB操作,都需要注册onerror或onsuccess事件处理程序,以确保适当地处理结果。
  • 浏览器前缀,IndexedDB在IE10中叫 msIndexedDB, 在Firefox中叫mozIndexedDB, 在Chrome中叫webkitIndexedDB。可以在代码前面加上下面这行代码:
  • var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;        
            

打开数据库

IndexedDB就是一个数据库,IndexedDB最大的特色是使用对象保存数据,而不是使用表来保存数据。一个IndexedDB数据库,就是一组位于相同命名空间下的对象的集合。

使用IndexedDB前首先要打开它,即把要打开的数据库名传给indexDB.open()

  • 如果传入的数据库已经存在,就会发送一个打开它的请求;
  • 如果传入的数据库还不存在,就会发送一个创建并打开它的请求;
  • 总之,调用indexedDB.open()会返回一个IDBRequest对象,在这个对象上可以添加onerror和onsuccess事件处理程序。

打开数据库的相关事件

打开数据库的代码示例

var request, database;
request = indexedDB.open("admin");
request.onerror = function(event) {
  alert("打开数据库错误:" + event.target.errorcode);
};
request.onsuccess = function(event) {
  database = event.target.result;
}
      
  • event.target都指向request对象;
  • 如果响应的是onsuccess事件,那么event.target.result中将有一个数据库实例对象(IDBDatabse);
  • 如果发生了错误,那么event.target.errorCode中将保存一个错误码,表示问题的性质。

数据库版本号

默认情况下,IndexedDB数据库是没有版本号的。最好一开始就调用setVersion()方法为数据库指定一个版本号(传入一个表示版本号的字符串)。

if (database.version != "1.0") {
  request = database.setVersion("1.0");
  request.onerror = function(event) {
    alert("设置版本号时发生错误:" + event.target.errorCode);
  };
  request.onsuccess = function() {
    alert("数据库初始化完成,数据库名:" + database.name + ", 版本:" + database.version);
  };
} else {
  alert("数据库已经初始化过了。数据库名称:" + database.name + ",版本号:" + database.version);
}
      

创建对象存储空间

建立完数据库连接以后,就要创建对象存储空间。

假设我们要保存用户记录,user对象格式可能类似于:

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

创建对象存储空间时,必须指定一个全局唯一个键,这里我们可以用"username"。下面是为了保存用户记录而创建对象存储空间的示例:

var store = database.createObjectStore("users", {keyPath: "username"});      
      

keyPath属性,就是空间中要保存的对象的一个属性,这个属性将作为存储空间的键来使用。

添加数据

  • 获得了对象存储空间的引用之后,就可以使用add()put()方法向其中添加数据。这两个方法都接收一个参数,即要保存的对象,然后这个对象就会被保存到存储空间中。
  • 这两个方法的区别在于,如果空间中已经包含了键值相同的对象:
    • add()会返回错误;
    • put()则会重写原有对象;
  • 也就是说可以把add()理解成插入新值,put()理解成更新原有的值。
  • 比如,我们可以用下面的方法来初始化对象存储空间,把返回的请求对象保存在一个变量中,然后再指定onerror或onsuccess事件处理程序,来验证请求是否成功完成:

    //users中保存着一批的用户对象
    var i=0, request, requests[], len = users.length;
    while(i < len) {
      request = store.add(users[i++]);
      request.onerror = function() {  // 错误处理
      };
      request.onsuccess = function() {  // 成功
      };
      requests.push(request);
    }
          

使用事务

在数据库对象上调用transaction()方法就可以创建事务。任何时候,想要读取或修改数据,都要通过事务来组织所有的操作。

下面的代码保证只加载users存储空间中的数据,以便通过事务进行访问:

var transaction = db.transaction("users");
	      

如果要访问多个对象存储空间,可以传入字符串数组:

var transaction = db.transaction(["users", "anotherStore"]);
        

上面的两个事务都是以只读的方式访问数据。要修改访问方式,必须在创建事务时传入第二个参数。

访问模式

第二个参数表示访问模式,用IDBTransaction接口定义的如下常量表示:

  • READ_ONLY(0)表示只读;
  • READ_WRITE(1)表示读写;
  • VERSION_CHANGE(2)表示改变

IE10+和Firefox4+实现的叫做IDBTransaction, 但在Chrome中则叫webkitIDBTransaction, 所以使用下面的代码可以统一接口:

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
	      

有了这行代码就可以比较方便地为transaction()指定第二个参数:

var transaction = db.transaction("users", IDBTransaction.READ_WRITE);
        

这个事务就可以读写users存储空间了。

访问存储空间

拿到事务以后,使用objectStore()方法并传入存储空间的名称,就可以访问特定的存储空间。然后就可以:

  • 使用add()put()方法添加数据;
  • 使用get()可以取得值;
  • 使用delete()可以删除对象;
  • 使用clear()可以删除所有对象;

get()和delete()方法都接收一个对象键作为参数。所有的这5个方法都会返回一个新的请求对象。例如:

var request = db.transaction("users").objectStore("users").get("007");
request.onerror = function(event) {
  alert("获取对象失败!");
};
request.onsuccess = function(event) {
  var result = event.target.result;
  alert(result.firestName);
};
	      

事务的事件处理函数

一个事务可以完成任何多个请求。

事务本身也有事件处理程序:onerroroncomplete。这两个事件可以提供事务级的状态信息。

transaction.onerror = function(event) {
  // 整个事务都被取消了
};
transaction.oncomplete = fucntion(event) {
  // 事务成功完成
};
	      

使用游标查询

  • 使用事务可以直接通过已知的键来检索单个对象;在需要检索多个对象的情况下,需要在事务内部创建游标。
  • 游标就是指向结果集的一个指针,游标指针首先会指向结果集中的第一项,在接到查找下一项的指令时,才会指向下一项。
  • 在对象存储空间上调用openCursor()方法可以创建游标。openCursor()方法返回的也是一个请求对象,也需要为该对象指定onsuccess和onerror事件处理函数
  • var store = db.transaction("users").objectStore("users"),
        request = store.openCursor();
        
    request.onsuccess = function(event) {
      // 处理成功
    };
    request.onerror = function(event) {
      // 处理失败
    };
    	      

IDBCursor

  • 在前面的onsuccess事件处理程序执行时,可以通过event.target.result取得存储空间中的下一个对象。
  • 在结果集中有下一项时,result是一个IDBCursor的实例;
  • 如果没有下一项,result为null;
  • IDBCursor实例具有以下几个属性:
    • key: 对象的键;
    • value:实际的对象;
    • direction:数值,表示游标走动的方向。
      • 默认是IDBCursor.NEXT(0), 表示下一项。
      • IDBCursor.NEXT_TO_DUPLICATE(1), 表示下一个不重复的项;
      • IDBCursor.PREV(2)表示前一项;
      • IDBCursor.PREV_NO_DUPLICATE表示前一个不重复的项。
    • primaryKey:游标使用的键,有可能是对象键,也有可能是索引键(后面会讨论索引)

检索结果信息

要检索某一个结果信息,可以像下面这样:

request.onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
  }
};
	      

因为result.value是一个对象,所以显示前要先转成JSON字符串。

使用游标更新记录

cursor.update()

调用update()方法可以使用指定的对象更新当前游标的value:

request.onsuccess = function(event) {
  var cursor = event.target.result, value, updateRequest;
  if (cursor) {
    if (cursor.key == "foo") {
      value = cursor.value;  // 取得当前值
      value.password = "magic";  // 更新密码
      
      updateRequest = cursor.udpate(value); // 请求保存更新
      updateRequest.onsuccess = function() {
        // 处理成功
      };
      updateRequest.onerror = function() {
        // 处理失败
      };
    }
  }
};
	      

使用游标删除记录

cursor.delete()

调用delete()方法可以删除相应的记录:

request.onsuccess = function(event) {
  var cursor = event.target.result, value, deleteRequest;
  if (cursor) {  // 检查一下
    if (cursor.key == "foo") {
      deleteRequest = cursor.delete(); // 请求删除当前项
      deleteRequest.onsuccess = function() {
        // 处理成功
      };
      deleteRequest.onerror = function() {
        // 处理失败
      };
    }
  }
};
	      

注意:如果当前的事务没有修改对象存储空间的权限,update()和delete()会抛出错误。

移动游标

默认情况下每个游标只发起一次请求;要想发起另一次请求,必须调用下面的一个方法:

  • continue(key): 移动到结果集的下一项。参数key是可选的,不指定这个参数,游标移动到下一项;指定这个参数的话,游标会移动到指定键的位置。
  • advance(count): 向前移动count指定的项数。

遍历示例

下面的例子遍历了对象存储空间中的所有项:

request.onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {  // 检查一下
    console.log("key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
    cursor.continue();  // 移动到下一项
  } else {
  	console.log("Done!");
  }
};
	      

调用continue()会触发另一次请求,进而再次调用onsuccess处理程序。如果没有更多项可以遍历时,event.target.result的值为null。

键范围

IDBKeyRange

  • 通过游标查找数据的方式比较有限,键范围(key range)为使用游标增添了一些灵活性。
  • 键范围由IDBKeyRange的实例表示。
  • 支持标准IDBKeyRange的浏览器有IE10+和Firefox4+, Chrome中的名字叫webkitIDBKeyRange。
  • 考虑到不同浏览器中的差异,也是要声明一个本地的类型:
  • var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
    	      

键范围

only()方法

第一种键范围方式是使用only()方法,传入想要取得对象的键:

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

这个范围保证只取得键值为"007"的对象。使用这个范围创建的游标和直接访问存储空间并调用get("007")差不多。

键范围

lowerBound()方法

第二种定义键范围的方法是指定结果集的下界。下界表示游标开始的位置。

下面代码的键范围可以保证游标从键为"007"的对象开始,然后继续前移,直到最后一个对象:

// 从键为“007”的对象开始,然后可以移动到最后
var lowerRange = IDBKeyRange.lowerBound("007");
      

如果想要忽略键为"007"的对象本身,从它的下一个对象开始,可以传入第二个参数true:

// 从键为“007”的下一个对象开始,然后可以移动到最后      
var lowerRange = IDBKeyRange.lowerBound("007", true);
			

键范围

upperBound()方法

第三种定义键范围的方法是指定结果集的上界,也就是指定游标不能超过哪个键。

指定上界使用upperRange()方法。

// 从头开始,到取得键为"ace"的对象为止      
var upperRange = IDBKeyRange.upperBound("ace");
      

如果不想包含键为指定值的对象,同样传入第二个参数true:

// 从头开始,到取得键为"ace"的上一个对象为止      
var upperRange = IDBKeyRange.upperBound("ace", true);
			

键范围

bound()方法

使用bound()方法可以同时指定上下界。

这个方法可以接收四个参数:表示下界的键,表示上界的键,可选的表示是否跳过下界的布尔值和可选的表示是否跳过上界的布尔值。

var upperRange = IDBKeyRange.bound("007", "ace");
var upperRange = IDBKeyRange.bound("007", "ace", true);
var upperRange = IDBKeyRange.bound("007", "ace", true, true);
var upperRange = IDBKeyRange.bound("007", "ace", false, true);
      

使用KeyRange的openCursor()方法

定义好了key range之后,就可以把它传给openCursor()方法,就能得到一个符合相应约束条件的游标。

var store = db.transaction("users").objectStore("users"),
  range = IDBKeyRange.bound("007", "ace"),
  request = store.openCursor(range);
     
request.onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value));
    cursor.continue(); // 移动到下一项
  } else {
    console.log("Done!");
  }
};     
      

设定游标方向

  • openCursor()可以接收两个参数,一个是刚才的IDBKeyRange实例,第二个是表示方向的数值常量,也就是前面讲到的IDBCursor中的常量。
  • 正常情况下,游标都是从存储空间的第一项开始,调用continue()或advance()前进到最后一项。游标的默认方向值是IDBCursor.NEXT。
  • 也可以创建一个游标,从最后一个对象开始,逐个迭代,直到第一个对象,这时要传入的常量是:IDBCursor.PREV。kuailef
  • var store = db.transaction("users").objectStore("users"),
      request = store.openCursor(null, IDBCursor.PREV);
      // null表示使用默认的值范围,也就是包含所有对象。
          

扩展阅读

<Thank you!>

Feel free to contact me if you have any question.