1. Cookie
cookie名和值在传送时都必须是URL编码的,并且绑定在特定域名下,以后给创建它的域名发送请求时,都会包含这个cookie。
每个域的cookie总数是有限的,不同浏览器规定不同。当超过单个域名限制之后还要设置cookie,浏览器就会清除之前设置的cookie,清除方案各浏览器自家决定。并且浏览器对于cookie的尺寸也有所限制。
cookie的构成
cookie由以下几块信息构成:
名称:不区分大小写,且必须经过URL编码。
值:必须经过URL编码。
域:cookie对于哪个域有效,所有向该域发送的请求都会包含这个cookie信息。这个值可以包含子域(如www.wrox.com),也可不包含(如.wrox.com,则对于wrox.com的所有子域都有效)。若没有明确规定,那个这个域被认作来自设置cookie的那个域。
路径:指定域中的哪个路径应该向服务器发送cookie。
失效时间:cookie何时被删除。
安全标识:指定后,cookie只有在使用SSL连接的时候才发送到服务器。
JavaScript操作cookie
使用document.cookie操作cookie,获取属性时,会返回当前页面可用的(根据域、路径、失效时间、安全设置)所有cookie的字符串,一系列由分号隔开的键值对,所有的名和值都经过了URL编码,所以必须使用decodeURIComponent(document.cookie)进行解码。
设置的时候,document.cookie属性可以设置为一个新的cookie字符串,这个cookie字符串会被解释并添加到现有cookie集合中,除非设置cookie的名字已存在,否则并不会覆盖原有cookie。设置cookie格式为(和Set-Cookie头相同的格式):
name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure
这些参数中,只有cookie的名和值必须。如:
document.cookie = encodeURIComponent("name") + "=" +encodeURIComponent("Nicholas");
以下代码封装了对cookie的读取,写入,删除。
var CookieUtil = { get: function (name){ var cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null, cookieEnd; if (cookieStart > -1){ cookieEnd = document.cookie.indexOf(";", cookieStart); if (cookieEnd == -1){ cookieEnd = document.cookie.length; } cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd)); } return cookieValue; }, set: function (name, value, expires, path, domain, secure) { var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value); if (expires instanceof Date) { cookieText += "; expires=" + expires.toGMTString(); } if (path) { cookieText += "; path=" + path; } if (domain) { cookieText += "; domain=" + domain; } if (secure) { cookieText += "; secure"; } document.cookie = cookieText; }, unset: function (name, path, domain, secure){ this.set(name, "", new Date(0), path, domain, secure); } };
解决cookie数的限制
为了绕开浏览器的单域名下的cookie数的限制,一些开发人员使用了成为子cookie的概念,也就是存放在单个cookie中的更小段的数据,以cookie值存储多个键值对。常见格式如下:
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
这种方式需要防止超过单个cookie的长度限制。
针对上面的格式,操作方式封装如下:
var SubCookieUtil = { get: function (name, subName){ var subCookies = this.getAll(name); if (subCookies){ return subCookies[subName]; } else { return null; } }, getAll: function(name){ var cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null, cookieEnd, subCookies, i, 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 (i=0, len=subCookies.length; i < len; i++){ parts = subCookies[i].split("="); result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); } return result; } } return null; }, set: function (name, subName, value, expires, path, domain, secure) { var subcookies = this.getAll(name) || {}; subcookies[subName] = value; this.setAll(name, subcookies, expires, path, domain, secure); }, setAll: function(name, subcookies, expires, path, domain, secure){ var 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 (subcookieParts.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; }, unset: function (name, subName, path, domain, secure){ var subcookies = this.getAll(name); if (subcookies){ delete subcookies[subName]; this.setAll(name, subcookies, null, path, domain, secure); } }, unsetAll: function(name, path, domain, secure){ this.setAll(name, null, new Date(0), path, domain, secure); } };
存在的问题
影响性能:所有的cookie都会随浏览器作为请求头发送,所以在cookie中存储大量信息会影响到达特定域的请求性能。
不安全:储存的任何数据都可以被他人访问
2. Web存储机制
Web Storage的目的是克服由cookie带来的一些限制,当数据需要被严格控制在客户端时,无须持续的将数据发回服务器。
Web Storage的主要目标:
- 提供一种在cookie之外存储会话数据的途径
- 提供一种存储大量可以跨会话存在的数据的机制
最初的Web Storage规范包含了两种对象的定义:sessionStorage和globalStorage。
这两个对象在支持的浏览器中都是以window对象属性的形式存在的。支持的浏览器有:IE8+, Firefox3.5+, Chrome4+和Opera10.5+。
Firefox2和3基于早期规范的内容实现了Web Storage,当时只实现了globalStorage,没有实现sessionStorage。
Storage类型
Storage的实例有如下方法:
- clear(): 删除所有值,Firefox中没有实现。
- getItem(name)
- key(index): 获取index位置处的值的名字
- removeItem(name)
- setItem(name, value)
还可以使用length属性来判断有多少名值对存放在Storage对象中,但无法判断对象中所有数据的大小,IE8提供了一个remainingSpace属性,用于获取还可以使用的存储空间的字节数。
sessionStorage对象
sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。
存储在sessionStorage中的数据可以跨页面刷新而存在,并且如果浏览器支持,浏览器崩溃并重启之后依然可用(IE不支持)。
由于sessionStorage对象绑定于某个服务器会话,所以文件在本地运行时不可用。存储在sessionStorage中的数据只能由最初给对象存储数据的页面访问到,所以多页面应用有限制。
sessionStorage对象其实是Storage的一个实例。
不同浏览器的写入数据方面略有不同,Firefox和WebKit实现饿了同步写入,所以添加到存储空间的数据是被立刻提交的。而IE的实现是异步写入数据,所以在设置数据和将数据实际写入磁盘存有延迟。对于大量数据,会发现IE要比其他浏览器更快的恢复执行,因为IE跳过了实际的磁盘写入过程。
若要遍历sessionStorage中的值,可以结合length属性和key()方法进行:
for (var i=0, len = sessionStorage.length; i < len; i++){ var key = sessionStorage.key(i); var value = sessionStorage.getItem(key); alert(key + "=" + value); }
还可使用for-in来遍历:
for (var key in sessionStorage){ var value = sessionStorage.getItem(key); alert(key + "=" + value); }
sessionStorage的对象应该主要用于针对会话的小段数据的存储,如果需要跨会话存储数据,那么应该使用localStorage。
globalStorage对象
已被localStorage取代。
localStorage对象
要访问同一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一个协议,在同一个端口上。
同sessionStorage一样,localStorage也是Storage的实例。
存储localStorage中的数据保留到通过JavaScript删除或者是用户清除浏览器缓存。
为了兼容只支持globalStorage的浏览器,可使用如下函数:
function getLocalStorage(){ if (typeof localStorage == "object"){ return localStorage; } else if (typeof globalStorage == "object"){ return globalStorage[location.host]; } else { throw new Error("Local storage not available."); } }
storage事件
对Storage对象进行的任何修改(通过属性或setItem()保存数据,使用delete操作符或removeItem()删除数据,或者调用clear()方法),都会在文档上触发storage事件。该事件的event有如下属性:
- domain: 发生变化的存储空间的域名
- key
- newValue: 如果是设置值,则是新值;若是删除,则是null
- oldValue
3. IndexedDB
Indexed Database API,是在浏览器中保存结构化数据的一种数据库。IndexedDB设计的操作完全是异步进行的,因此,大多数操作都会以请求方式进行,但这些操作会在后期执行,如果成功则返回结果,如果失败则返回错误。为确保适当的处理结果,差不多每次IndexedDB操作,都需要注册onerror或onsuccess事件处理程序。
由于API的可变性,IndexDB这个对象在各浏览器中也是不同的,各浏览器都使用了供应商前缀,IE10中叫msIndexedDB,Firefox4中叫mozIndexedDB,Chrome中叫webkitIndexedDB,使用时最好都执行如下代码:
var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;
数据库
IndexedDB就是一个数据库,只不过使用对象保存数据而非表,一个IndexedDB数据库,就是一组位于相同命名空间下的对象的集合。
要使用IndexedDb,首先需要打开它,使用indexDB.open(dbName),如果传入的数据库已存在则直接打开,否则,会创建再打开。
indexedDB.open()会返回一个IDBRequest对象,可在该对象上添加onerror和onsuccess事件处理程序。
var request, database; request = indexedDB.open("admin"); request.onerror = function(event){ alert("Something bad happened while trying to open: " + event.target.errorCode); }; request.onsuccess = function(event){ database = event.target.result; };
这两个事件处理程序中,event.target都指向request对象,如果成功请求,event.target.result是一个IDBDatabase对象;如果出错,则event.target.errorCode中将保存一个错误码,如下:
IDBDatabaseException.UNKNOWN_ERR(1):意外错误,无法归类。
IDBDatabaseException.NON_TRANSIENT_ERR(2):操作不合法。
IDBDatabaseException.NOT_FOUND_ERR(3):未发现要操作的数据库。
IDBDatabaseException.CONSTRAINT_ERR(4):违反了数据库约束。
IDBDatabaseException.DATA_ERR(5):提供给事务的数据不能满足要求。
IDBDatabaseException.NOT_ALLOWED_ERR(6):操作不合法。
IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7):试图重用已完成的事务。
IDBDatabaseException.ABORT_ERR(8):请求中断,未成功。
IDBDatabaseException.READ_ONLY_ERR(9):试图在只读模式下写入或修改数据。
IDBDatabaseException.TIMEOUT_ERR(10):在有效时间内未完成操作。
IDBDatabaseException.QUOTA_ERR(11):磁盘空间不足。
对象存储空间
对象存储空间就相当于关系型数据库中的表,其中的对象就相当于记录。
如果要保存一条如下的用户记录:
var user = { username: "007", firstName: "James", lastName: "Bond", password: "foo" };
若username全局唯一,则使用username作为这个对象存储空间的键,保存方式如下:
var store = db.createObjectStore("users", { keyPath: "username" });
第二个参数中的keyPath属性就是就是空间中要保存对象的键。
接下来就可以使用add()或put()方法向其中添加数据,这两个方法都只接收要保存的对象这一个参数。如果保存键值相同的对象,add()会返回错误,put()会重写原有对象,相当于insert和update。
事务
任何时候,想要读取或修改数据,都要通过事务来组织所有操作。可在数据库对象上调用transaction()方法创建事务:
var transaction = database.transaction(); // 只加载 users 存储空间中的数据 var transaction = database.transaction("users"); // 访问多个对象存储空间 var transaction = database.transaction(["users", "anotherStore"]);
上面几种事务都是以只读方式访问数据。database.transaction()的第二个参数表示访问模式,由IDBTransaction接口定义的如下常量表示:
READ_ONLY(0)
READ_WRITE(1)
VERSION_CHANGE(2)
IE10+和 Firefox 4+实现的是IDBTransaction,但在 Chrome 中则叫 webkitIDBTransaction,所以使用下面的代码可以统一接口:
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
下面创建一个能够读取users存储空间的事务:
var transaction = db.transaction("users", IDBTransaction.READ_WRITE);
使用objectStore()方法并传入存储空间的名称,就可以访问特定的存储空间。可使用put()、get()、add()、delete()、clear()等,如:
var request = db.transaction("users").objectStore("users").get("007"); request.onerror = function(event){ alert("Did not get the object!"); }; request.onsuccess = function(event){ var result = event.target.result; alert(result.firstName); //"James" };
由于一个事物可以完成多个请求,所以事物本身也有事件处理程序:onerror和oncomplete。
使用游标查询
通过已知的键可以检索单个对象,但要检索多个对象,则需要在事物内部创建游标。游标就是一个指向结果集的指针,首先会指向结果中的第一项,在接到查找下一项的命令时,才会指向下一项。
IDBCursor的实例有以下几个属性:
direction:数值,表示游标移动的方向。
IDBCursor.NEXT(0):为默认值表示下一项
IDBCursor.NEXT_NO_DUPLICATE(1):表示下一个不重复的项
DBCursor.PREV(2):表示前一项
IDBCursor.PREV_NO_DUPLICATE:表示前一个不重复的项。
key:对象的键。
value:实际的对象。
primaryKey:游标使用的键。可能是对象键,也可能是索引键(稍后讨论索引键)。
可以在对象存储空间上调用openCursor()创建游标,且该方法返回一个请求对象,因此也可指定事件处理程序。如:
var store = db.transaction("users").objectStore("users"), request = store.openCursor(); request.onsuccess = function(event){ //处理成功 }; request.onerror = function(event){ //处理失败 };
在onsuccess事件处理程序执行时,可通过event.target.result取得存储空间中的下一个对象,在结果集中有下一项时,这个属性中保存一个IDBCursor的实例,在没有下一项时,这个属性的值为null。
要检索一个结果的信息,可以这样做:
request.onsuccess = function(event){ var cursor = event.target.result; if (cursor){ console.log("Key: " + cursor.key + ", Value: " + JSON.stringify(cursor.value)); } };
使用游标更新记录:
调用update()方法可以用指定的对象更新当前游标的value,同样的,调用update()方法也会创建一个新请求,也可指定事件处理程序。
下面使用游标更新记录:
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.update(value); //请求保存更新 updateRequest.onsuccess = function(){ //处理成功 }; updateReqeust.onerror = function(){ //处理失败 }; } } };
使用游标删除记录:
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!"); } };
键范围
键范围为使用游标增添了灵活性。
键范围由IDBKeyRange的实例表示,IE10+和Firefox4+支持标准的IDBKeyRange类型,Chrome中的名字叫webkitIDBKeyRange。那,使用前最好考虑不同浏览器间的差异:
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
有四种定义键范围的方式:
1)only()方法,传入想要取得的对象的键
var onlyRange = IDBKeyRange.only("007");
这种方式与直接访问存储空间并调用get("007")差不多。
2)lowerBound(), 指定结果集的下界,也就是游标开始的位置。
//从键为"007"的对象开始,然后可以移动到最后 var lowerRange = IDBKeyRange.lowerBound("007"); // 如果想忽略键为"007"的对象,从它的下一个对象开始,那么可以传入第二个参数 true: //从键为"007"的对象的下一个对象开始,然后可以移动到最后 var lowerRange = IDBKeyRange.lowerBound("007", true);
3)upperRange(),指定结果集的上界,也就是游标不能超过哪个键。
//从头开始,到键为"ace"的对象为止 var upperRange = IDBKeyRange.upperBound("ace"); // 如果不想包含键为指定值的对象,同样,传入第二个参数 true: //从头开始,到键为"ace"的对象的上一个对象为止 var upperRange = IDBKeyRange.upperBound("ace", true);
4)bound(),同时指定上下界。可接受4个参数:上界、下界、是否跳过上界(可选)、是否跳过下界(可选)。
//从键为"007"的对象开始,到键为"ace"的对象为止 var boundRange = IDBKeyRange.bound("007", "ace"); //从键为"007"的对象的下一个对象开始,到键为"ace"的对象为止 var boundRange = IDBKeyRange.bound("007", "ace", true); //从键为"007"的对象的下一个对象开始,到键为"ace"的对象的上一个对象为止 var boundRange = IDBKeyRange.bound("007", "ace", true, true); //从键为"007"的对象开始,到键为"ace"的对象的上一个对象为止 var boundRange = IDBKeyRange.bound("007", "ace", false, true);
在定义键范围后,把它传给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中的常量,Firefox4+和Chrome的实现不同,因此需消除差异:
var IDBCursor = window.IDBCursor || window.webkitIDBCursor;
索引
要创建索引,首先引用对象存储空间,然后调用createIndex()方法,如下:
var store = db.transaction("users").objectStore("users"), index = store.createIndex("username", "username", { unique: false});
createIndex()的第一个参数为索引名,第二个参数是索引的属性的名字,第三个参数是一个包含unique属性的对象,表示键在所有记录中是否唯一。
createIndex()返回IDBIndex的实例,在对象存储空间上调用index()也能返回同一个实例。
要使用一个已经存在的名为"username"的索引,可以这样做:
var store = db.transaction("users").objectStore("users"), index = store.index("username");
在索引上调用 openCursor()方法也可以创建新的游标,除了将来会把索引键而非主键保存在 event.result.key 属性中之外,这个游标与在对象存储空间上调用openCursor()返回的游标完全一样。如:
var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.openCursor(); request.onsuccess = function(event){ //处理成功 };
在索引上也能创建一个特殊的只返回每条记录主键的游标,那就要调用 openKeyCursor()方法。这个方法接收的参数与 openCursor()相同。而最大的不同在于,这种情况下 event.result.key 中仍然保存着索引键,而 event.result.value 中保存的则是主键,而不再是整个对象。
var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.openKeyCursor(); request.onsuccess = function(event){ //处理成功 // event.result.key 中保存索引键,而 event.result.value 中保存主键 };
同样,使用 get()方法能够从索引中取得一个对象,只要传入相应的索引键即可;当然,这个方法也将返回一个请求。
var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.get("007"); request.onsuccess = function(event){ //处理成功 }; request.onerror = function(event){ //处理失败 };
要根据给定的索引键取得主键,可以使用 getKey()方法。这个方法也会创建一个新的请求,但event.result.value 等于主键的值,而不是包含整个对象。
var store = db.transaction("users").objectStore("users"), index = store.index("username"), request = index.getKey("007"); request.onsuccess = function(event){ //处理成功 //event.result.key 中保存索引键,而 event.result.value 中保存主键 };
任何时候,通过 IDBIndex 对象的下列属性都可以取得有关索引的相关信息。
name:索引的名字。
keyPath:传入 createIndex()中的属性路径。
objectStore:索引的对象存储空间。
unique:表示索引键是否唯一的布尔值。
另外,通过对象存储对象的 indexName 属性可以访问到为该空间建立的所有索引。通过以下代码就可以知道根据存储的对象建立了哪些索引。
var store = db.transaction("users").objectStore("users"), indexNames = store.indexNames, index, i = 0, len = indexNames.length; while(i < len){ index = store.index(indexNames[i++]); console.log("Index name: " + index.name + ", KeyPath: " + index.keyPath + ", Unique: " + index.unique); }
在对象存储空间上调用 deleteIndex()方法并传入索引的名字可以删除索引:
var store = db.transaction("users").objectStore("users"); store.deleteIndex("username");
因为删除索引不会影响对象存储空间中的数据,所以这个操作没有任何回调函数。
并发问题
虽然网页中的 IndexedDB 提供的是异步 API,但仍然存在并发操作的问题。如果浏览器的两个不同的标签页打开了同一个页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库的问题就有可能发生。把数据库设置为新版本有可能导致这个问题。因此,只有当浏览器中仅有一个标签页使用数据库
的情况下,调用 setVersion()才能完成操作。
刚打开数据库时,要记着指定 onversionchange 事件处理程序。当同一个来源的另一个标签页调用 setVersion()时,就会执行这个回调函数。处理这个事件的最佳方式是立即关闭数据库,从而保证版本更新顺利完成。例如:
var request, database; request = indexedDB.open("admin"); request.onsuccess = function(event){ database = event.target.result; database.onversionchange = function(){ database.close(); }; };
每次成功打开数据库,都应该指定 onversionchange 事件处理程序。调用 setVersion()时,指定请求的 onblocked 事件处理程序也很重要。在你想要更新数据库的版本但另一个标签页已经打开数据库的情况下,就会触发这个事件处理程序。此时,最好先通知用户关闭其他标签页,然后再重新调用 setVersion()。例如:
var request = database.setVersion("2.0"); request.onblocked = function(){ alert("Please close all other tabs and try again."); }; request.onsuccess = function(){ //处理成功,继续 };
其他标签页中的 onversionchange 事件处理程序也会执行。
限制
对 IndexedDB 的限制很多都与对 Web Storage 的类似。首先, IndexedDB 数据库只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。换句话说, www.wrox.com 与 p2p.wrox.com的数据库是完全独立的。
其次,每个来源的数据库占用的磁盘空间也有限制。 Firefox 4+目前的上限是每个源 50MB,而Chrome 的限制是 5MB。移动设备上的 Firefox 最多允许保存 5MB,如果超过了这个配额,将会请求用户的许可。
Firefox 还有另外一个限制,即不允许本地文件访问 IndexedDB。 Chrome 没有这个限制。