传统的HTML使用的是众所周知的cookie,各种浏览器都支持,直接用js就可以调用,很方便。但cookie也有它本身的缺陷与不足。比如存储空间小,每个站点大小限制在4 KB左右,又有时间期限,而且在请求网页的时候cookie会被附加在每个HTTP请求的header中,所以无形中增加了流量,在HTTP请求中的cookie是明文传递的,所以安全性成问题,当然你可以用SSL通道,这另当别论。cookie还很容易受到跨站脚本的攻击,在一个链接地址后面加上“?cookie=document.cookie”就可以很容易地获得用户的cookie信息。当然, HTML5的本地存储可能也会有跨站脚本攻击XSS的问题,这个不是本文讨论的主要内容,以后我们一起慢慢研究。   

  长久以来本地存储能力一直是桌面应用有别于Web应用的一个主要优势,前者可以自由的操作本地文件系统,存储和读取都很方便,比如ini、xml等本地配置文件,存于本地,不需要在每次用的时候都要去服务器上取配置数据。web应用就不同,为了安全,浏览器都不会让网页脚本去调用本地的文件系统,除了cookie,自然也不能存储或使用本地配置文件,用户的个性化设置一般都是存在浏览器上。再比如,用户在网页上编辑一篇很长的文章,编辑的过程中网页要自动帮用户保存。存哪?本地cookie很可能不够用,空间太小,而且还有时候期限,所以只能保存到服务器上去。 

  HTML5的新标准可以很好地解决上面的一些问题。当然,本地存储还可以解决更多我暂时没提到的问题,留给读者自己去探索。本地存储可以存储5M大小的数据,甚至还可以更多。 它主要有四种:localstorage , sessionstorage, webSQL , indexedDB。

1、localstorage 

  用window.localStorage,但浏览器可能不支持,它就有可能是空,所以我们先做个判断。

1 function getLocalStorage() {
2 try {
3 if( !! window.localStorage ) return window.localStorage;
4 } catch(e) {
5 return undefined;
6 }
7 }

(两个感叹号用于判断变量是否为null、undefined、''、0,个人理解,这是书写习惯的问题,把它去掉也没错。这里是为了规范if()里面一般是放bool类型的,而js比较灵活,上面提到的那四种语法上当作false来处理,在它们前面加上一个感叹号,则变成真正的bool类型,但是true,所以再加个感叹号,变回false,不是上面四种类型的都变true

存数据。

1 var db = getLocalStorage();
2 if(db) {
3 db.setItem('author', 'jasonling');
4 db.setItem('company', 'Tencent');
5 db.setItem('introduction', 'A code lover !');
6 }

  其实,localStorage就是键值对,它的值可以存大小多达5M的字符串。事实上,它是存到一个sqlite的文件中去,用sqlite打开可以看到里面自己建了一个表,里面有我们刚刚存的数据。

  chrome浏览器的这个sqlite文件存在:C:/Users/你的用户名/AppData/Local/Google/Chrome/User Data/Default/Local Storage 里面,如上图,用sqlite打开,用.schema可以看到一个表,这是chrome帮我们建的用来存储键值对的表ItemTable,查询表里的数据可以发现我们刚刚用程序存进去的值。

其它浏览器中,sqlite文件的存储位置,读者可以自己去探索探索。

localstorage的其它操作:

View Code
 1 db.setItem('author', 'jasonling');
2 db.setItem('company', 'Tencent');
3 db.setItem('introduction', 'A code lover !');
4 //取值
5 alert(localStorage['author']);
6 alert(db.getItem('company'));
7 //删除
8 db.removeItem('company');
9 alert(db.getItem('company'));
10 //当然也可以用db.setItem('company', '');来删除一个值,但这样删不彻底。
11 //清除
12 db.clear();
13 alert(localStorage['author']); //undefined

在浏览器上同时打开两个页面(Lynda.com上的一个示例),如图:

  这两个页面都是一样的,当我用在一个页面中进行修改,另一个页面的数据会不会自动修改呢?

显然不会,为了达到自动更新的效果,我们得用事件监听器。

一样的道理,先要判断浏览器支不支持:

1 //Get the addEventListener
2 function getAddEventHandler() {
3 try {
4 if( !! window.addEventListener ) return window.addEventListener;
5 } catch(e) {
6 return undefined;
7 }
8 }

写一个handler函数.

 1 //EventStatus
2 function eventStatus(s) {
3 if(s) element('eventResult').innerHTML = s;
4 else element('eventResult').innerHTML = 'Event status';
5 }
6
7 //Event handler
8 function eventHandler(e) {
9 eventStatus('Event triggered: ' + e.url + ' ' +
10 e.storageArea.traveler + ' ' + e.storageArea.destination + ' ' + e.storageArea.transportation);
11 dispResults();
12 }

eventStatus用来作判断,当字符串s有问题时,就是else,当它没问题时就if。为了进一步说明,我写了下面一个例子:

1 function test(s) {
2 if(a) {
3 alert('if');
4 } else {
5 alert('else');
6 }
7 }
8 var a = window.abc; //window.abc不存在,是有问题的,所以,可以想像,下面的结果是else
9 test(a);

  这个其实是js的一个技巧,有问题的字符串,在if里面当false处理。这要和未定义的变量放if里面区别开来,如果一个变量未定义,那么不管放哪,js都会停止执行。如,如果上面没有定义b,即没有var b; 这句,下面突然出现了个if(b)那么程序到这里就会中断。大部分浏览器是这样的。  回到正题上来:

1 var addEL = getAddEventHandler(); //这是上面写的一个函数 
2 if(addEL) {
3 addEL('storage',eventHandler,false); //给storage事件加上监听器
4 } else {
5 element('eventResult').innerHTML = 'This browser does not support event listeners';
6 }
7 dispResults(); //这是自定义的一个函数,读者不用管,作用是显示结果

  这样,每次local storage里面的数据变化时,浏览器里面的数据就会自动变化了,因为storage事件被监听后,浏览器就会时时监视local storage里的数据,如果变化,就会触发事件,修改页面。不管几个页面都会做出相应的修改。

要注意一点,这个事件只有在同一个浏览器程序里面才有效(比如你不能在chrome和firefox各打开一个页面,然后等着事件生效),因为不同浏览器的sqlite文件不一样,各自修改自己的数据,当然不会对其它浏览器的数据造成影响。

经过上面的介绍,大家可以看出,忽略事件的内容外,localstorage的操作其实也很简单,和cookie一样很容易操作。下面介绍的几个会一个比一个难,这里先做好心里准备。

2、sessionStorage

  localStorage的数据是页面共享的,但有些情况,我们需要一个浏览器中的不同页面可以单独操作自己的数据。这时我们就可以用sessionStorage了,它存储的数据只有当前页面可以访问。 

第一步还是一样,判断浏览器支不支持:

1 function getSessionStorage() {
2 try {
3 if( !! window.sessionStorage ) return window.sessionStorage;
4 } catch(e) {
5 return undefined;
6 }
7 }

然后:

1 var db = getSessionStorage();
2 if(db) {
3 db.setItem('author','jasonling');
4 db.setItem('company', 'Tencent');
5 db.setItem('introduction', 'A code lover!');
6 }
7 //和localStorage一样的操作

  然后打开浏览器,同时打开两个网页做测试,我们可以看到,这两个页面中的数据不会相互影响。还是用Lynda.com的一个例子来展示:

  再怎么刷新这两个页面,他们之间的数据还是不会相互影响。

  那么,sessionStorage把数据存哪了?经过测试发现,它保存在

C:\Users\你的用户名\AppData\Local\Google\Chrome\User Data\Default\Current Session 这个文件里面。但它不是一个sqlite文件,因为用sqlite打不开。具体怎么存的,作者本人也没研究出来,如有高手知道,请留言告之。

3、webSQL 

  其实就是sqlite数据库,在js中可以像java在本地调用mysql数据库一样的方便,可以自己创建数据库,自己建表,自己往里面增删改数据。

  第一步还是要判断一下浏览器是否支持:

1 function getOpenDatabase() {
2 try {
3 if( !! window.openDatabase ) return window.openDatabase;
4 else return undefined;
5 } catch(e) {
6 return undefined;
7 }
8 }

如果支持,就试着打开数据库连接:

 1 function prepareDatabase() {
2 var odb = getOpenDatabase();
3 if(!odb) {
4 dispError('Web SQL Not Supported');
5 return undefined;
6 } else {
7 var db = odb( 'testDatabase', '1.0', 'A Test Database', 10 * 1024 * 1024 );
8 db.transaction(function (t) {
9 t.executeSql( createSQL, [], function(t, r) {}, function(t, e) {
10 alert('create table: ' + e.message);
11 });
12 });
13 return db;
14 }
15 }

  其中,odb('testDatabase', '1.0','A Test Database',10*1024*1024);试着打开(新建)一个数据库数据库,四个参数分别表示:数据库名,数据库版本,数据库描述,和它预定的大小。

   db.transaction那一句,是WebSQL中最常用的语法。createSQL是一个用来创建数据库的字符串:

var createSQL = 'CREATE TABLE IF NOT EXISTS tTravel (' + 'id INTEGER PRIMARY KEY,' + 'traveler TEXT,' + 'destination TEXT,' + 'transportation TEXT' + ')';

  transaction这个函数,我在网上找了很多资料,都说是后面只能带一个参数,但我测试之后,发现是可以带三个参数,第二个是错误处理函数,两参(t,e),分别表示transaction和error,第三个成功回调函数,无参。。重点是第一个参数,是一个方程,四个参数:

function(t) {

  t.executeSql("要执行的sql语句_需要参数的地主用?代替",  [参数_用逗号隔开],  function(t,r){成功回调函数_t表示transaction_r是result},  function(t,e){出错回调函数_t表示transaction_e是error});

}

比如用用户名ling和密码mypwd来登陆,可以这样写: 

1 function(t) {
2 t.executeSql('select * from tUser where name=? and pwd=?',['ling','mypwd'],
3 function(t,r) {
4 alert('登陆成功');
5 },
6 function(t,e) {
7 alert(e.message);
8 }
9 }

很方便,完全可以使用sql语句来操作。

  回到原来的话题,var db = prepareDatabase(); 来得到我们在数据库中建的表tTravel。接下去我们就可以用它来操作这个表。下面举一些例子:(其中有些这是Lynda.com提供的一些例子,bwTable是它自己实现的一个漂亮的表格,用来显示数据,读者不用深究这个,我会把最后的代码一起传上来)  

(1)计算表里面有几行记录:

 1 //Get the count of rows
2 function countRows(){
3 if(!db) return ;
4 db.readTransaction(function(t) {
5 t.executeSql('SELECT count(*) AS c FROM tTravel',[],function(t,r) {
6 var c = r.rows.item(0).c; //result 结果的第一行的c那一列
7 element('rowCount').innerHTML = c? c:0;
8 },function(t,e) {
9 alert('countRows:' + e.message);
10 });
11 });
12 }

  语法和上面提到的一样,只是这边用的是readTransaction,这是为了保证不对表进行写操作,这是一种安全的举措,当然也可以用transaction。

(2)取出表中的数据:

 1 if(db) {
2 db.readTransaction(function(t){
3 t.executeSql('SELECT * FROM tTravel ORDER BY LOWER(traveler)',[],function(t,r) {
4 var mytab = new bwTable();
5 mytab.setHeader(['Traveler','Destination','Transportation','']);
6
7 for(var i = 0; i < r.rows.length; i ++){
8 var row = r.rows.item(i);
9 mytab.addRow([row.traveler,row.destination,row.transportation,rowBtn(row.id,row.traveler)]);
10 }
11 element('results').innerHTML = mytab.getTableHTML();
12 element("travelForm").elements['traveler'].focus();
13 },function(t,e) {
14 alert("Can't get the data");
15 });
16 });
17 }

(3)往表里添加数据:

 1 if(db) {
2 db.transaction(function(t) {
3 t.executeSql('INSERT INTO tTravel VALUES(NULL,?,?,?)',
4 [traveler,destination,transportation],
5 function(t,r) {resetForm();},//执行成功时把表格清空,自己实现的一个函数
6 function(t,e){
7 alert("insert rows:"+e.message);
8 });
9 });
10 }

(4)更新数据:

1 db.transaction(function(t){
2 t.executeSql('update tTravel set traveler=?,destination=?,transportation=? where id=?',
3 [traveler, destination, transportation,inputKey]);
4 },function(t,e){
5 alert('Update row:'+e.message);
6 },function(){resetForm();});

(5)删除数据:

1 if(db) {
2 db.transaction(function(t){
3 t.executeSql('delete from tTravel where id=?',[id],
4 function(t,r){ alert('SUCCESSFULLY!');});
5 });
6 }

  其实很容易,就是简单地用sql来操作数据库。

  chrome的webSQL本地数据存在这个目录下:C:\Users\你的用户名\AppData\Local\Google\Chrome\User Data\Default\databases文件夹,里面一般会有file__0等类似的文件夹,可以用sqlite打开来看看。

4、indexedDB

  有一篇文章《Indexed DB:未来一切 Web 应用的基石》在总体上介绍了一下indexedDB,读者可以去网搜一搜,读一读,大概了解一下它的前世今生!我摘录了《HTML5之IndexedDB使用详解》里面的一些内容来简单的介绍一下:它是HTML5-WebStorage的重要一环,是一种轻量级NOSQL数据库,w3c为IndexedDB定义了很多接口,其中Database对象被定义为IDBDataBase。而得到IDBDataBase用的是工厂方法,即从IDBFactory中取得。浏览器对象中,实现了IDBFactory的只有indexedDB这个实例。IndexedDB中,几乎所有的操作都是采用了command->request->result的方式。比如查询一条记录,返回一个request,在request的result中得到查询结果。又比如打开数据库,返回一个request,在request的result中得到返回的数据库引用。(摘录结束)

  indexedDB的使用比前三种复杂了些,下面我们一部分一部分来了解。

  首先要注意的一点是,indexedDB的页面只有放到服务器上才能正常访问,单独双击页面很可能不会成功。但即使这样,indexedDB的数据还是存在用户本地的。目前测试了一下,chrome上delete操作有点问题,不过读者可以在firefox上测试,可以正常跑通。

  第一步也是判断并获得indexedDB对象:

function getIndexDB() {
try {
//由于浏览器不同内核中,indexedDB的对象名不同,所以比较麻烦:
if(! window.indexedDB ) window.indexedDB = window.mozIndexedDB || window.webkitIndexedDB;
//webkitIndexedDB内核的浏览器中,IDBTransaction的名字有些差异
if('webkitIndexedDB' in window) window.IDBTransaction = window.webkitIDBTransaction;
if( !! window.indexedDB) {
return window.indexedDB;
}
else {
return undefined;
}
}
catch (e) {
return undefined;
}
}

  然后打开indexedDB,比较复杂:

 1 function openDB(){
2 var iDB = getIndexDB();
3 if(!iDB) {
4 dispError('IndexDB not supported!');
5 return ;
6 } else {
7 try {
8 //打开或创建一个indexedDB,名travelDB,后面那参数是描述。
9 var request = iDB.open('travelDB','demo travelDB');
10 request.onerror = function(event) { //request.onerror是打开失败时的处理函数
11 dispError('Failed to open IndexDB database');
12 }
13 request.onsuccess = function(event) { //request.onerror是打开成功时的回调函数
14 db = request.result;
15 //不要以为到这里很复杂,其实到上一步就是:
16 //window.indexedDB.open('travelDB','demo travelDB').result
17 //只是我们在一步步的做安全处理 ,下面是request在获取结果失败时的处理
18 db.onerror = function(event){dispError( 'Database error: ' + event.target.errorCode );}
19 //indexedDB比较麻烦的一点是还要设置版本,如果不版本不统一,
20 //在之后某些步骤调用时,会有问题。一般都把它设成1.0
21 if(db.version != '1.0') {
22                     var req = db.setVersion('1.0'); //Set the version
23 //设置了version之后 还要进行错误处理
24 req.onerror = function(event){alert('version error!');}
25 req.onsuccess = function(event) {
26 //alert('Creating the object store');
27 //创建一个ObjectStore,id是主键,自动增长
28 var objStore = db.createObjectStore('oTravel',
29 {keyPath:'id',autoIncrement:true});
30 //创建一个index:traveler,不唯一
31 objStore.createIndex('traveler','ciTraveler',{unique:false});
32 }
33 }
34 }
35 } catch(e) {
36 dispError('IndexDB supported, but can\'t open the database\n'+e.message);
37 }
38 }
39 }

  其中db是在函数外定义的一个变量:var db;这样就可以在全局使用。

  下面再举几个查询、插入、修改、删除的例子:

(1)查询

 1 if(db){    
2 //先得到我们刚刚创建的objectStore,transaction里面的可以有两个参数,
3 //第一个参数为要关联的数据库名称数组,第二个为打开此数据库的方式(如只读),
4 //下面会讲,它缺省时表示 打开的方式为只读
5 //然后由transaction得到名为oTravel的objectStore
6 var objStore = db.transaction(['oTravel']).objectStore('oTravel');
7 //可以用objStore来打开游标,也可以用index来打开游标,它们的区别下面会讲
8 //var indexTraveler = objStore.index('traveler');
9 objStore.openCursor().onsuccess = function(event) { //open the cursor of the index
10 var cursor = event.target.result; //由event来获得游标
11 if(cursor) {
12 var v = cursor.value;
13 alert(v.traveler + '乘坐' + v.transportation + '' + v.destination);
14 cursor.continue();
15 //continue其实就是让游标往下移,并试着open,成功时还是执行这个函数,
16 //所以这个其实是一个隐藏的循环,直到cursor读完为止
17 }
18 }
19 }

  transaction的第二个参数表示打开的方式,主要有以下几种:

IDBTransaction.READ_ONLY              只读

IDBTransaction.READ_WRITE            可读可写

IDBTransaction.VERSION_CHANGE    版本升级

用的最多的是前两种。不设置时则默认为READ_ONLY  

(2)插入

  由上面例子可以体验到一些nosql数据库的优点,直接对对象进行操作,取出的是对象,写入的也是对象,如下:

1 curRec = {traveler:traveler, destination:destination,
2 transportation:transportation,ciTraveler:traveler.toLowerCase()};
3 db.transaction(['oTravel'],IDBTransaction.READ_WRITE).objectStore('oTravel').add(curRec);

  很简单,就是把对象插入数据库中。

(3)修改

其实就是先把原来相应的那条数据给删除,然后加入这条修改后的数据

 1 curRec = {traveler:traveler, destination:destination,
2 transportation:transportation,ciTraveler:traveler.toLowerCase()};
3 var objStore = db.transaction('oTravel',IDBTransaction.READ_WRITE).objectStore('oTravel');
4 //alert(key);
5 var request;
6 if (objStore.delete) {
7 request = objStore.delete(key);
8 } else {
9 // FF4 not up to spect
10 request = objStore.remove(key);
11 }
12 request.onerror = function(e) {
13 dispError('Remove Error !');
14 }
15 request.onsuccess = function(e) {
16 objStore.add(curRec);
17 };

(4)删除

  删除时要先得到id,把id传入数据库引擎就知道删除哪一条数据了,步骤和上面差不多。

 1 if(confirm('Are you sure to delete traveler:'+traveler)) {
2 var objStore = db.transaction('oTravel',IDBTransaction.READ_WRITE).objectStore('oTravel');
3 //alert(id);
4 var request;
5 if (objStore.delete) {
6 request = objStore.delete(id);
7 } else {
8 // FF4 not up to spect
9 request = objStore.remove(id);
10 }
11 request.onerror = function(e) {
12 dispError('Remove Error !');
13 }
14 }

 

  上面讲到的,用objStore来打开游标,也可以用index来打开游标。有一篇文章讲得很清楚,我把它摘过来供读者参考。

数据本地化存储》这文章比较长,可以搜 openCursor 来找到你想要的内容

  

使用游标
使用get()方法需要知道存储对象的key值,但若不知道key值,要看存储对象,就可以使用游标,如下是使用游标对数据库进行遍历:

var objectStore = db.transaction("customers").objectStore("customers"); 

objectStore.openCursor().onsuccess = function(event) { 

  var cursor = event.target.result;
  if (cursor) {
    alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  }
  else {
    alert("No more entries!");
  }
}; 
openCursor()方法有许多参数,首先你可设置遍历的Key的范围,其次可以设置游标遍历的方向,可参见后面关于IndexedDB异步API的介绍。Continue();表示继续遍历。

使用索引
在数据库中,所有的数据都是以key值来存储的,若要通过name等其他属性查看存储对象,需要遍历每个SSN并将它的name提取出判断是否为要查看的对象,但可以通过index而更为简单的实现,如:

var index = objectStore.index("name"); index.get("Donna").onsuccess = function(event) { 

  alert("Donna's SSN is " + event.target.result.ssn);

}; 
我们在index中使用cursor来遍历存储的数据,并根据不同的cursor打开方式,返回不同的遍历结果,如下两种方式:

 
//方式一
index.openCursor().onsuccess = function(event) { 
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
    cursor.continue();
  }
}; 
//方式二
index.openKeyCursor().onsuccess = function(event) { 
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the SSN.
    // No way to directly get the rest of the stored object.
    alert("Name: " + cursor.key + ", "SSN: " + cursor.value);
    cursor.continue();
  }
};
关于游标遍历的范围和方向
如果想要限制游标的遍历范围,可以使用“key range”的对象,并将它做为openCursor()和openKeyCursor()的第一个参数,这样的范围可以是单个键值、或是一个最低边界和最高边界的范围,并规定是否包括范围,如下:

// Only match "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

//Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};
另外,还可以规定游标遍历的方向,默认的是上升的方向,若使用相反的方向,可以将PREV作为openCursor()或是openKeyCursor()的第二个参数,如下:

objectStore.openCursor(null, IDBCursor.PREV).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};
需要注意的是,在索引中使用游标时,由于可能有多个键值是相同的,如果想过滤多余的相同键值,将NEXT_NO_DUPLICATE 或是PREV_NO_DUPLICATE做为它的第二个参数,这时候总是返回最低边界的那一个对象,如下:

index.openKeyCursor(null, IDBCursor.NEXT_NO_DUPLICATE).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

  

  chrome的indexedDB本地数据存在:C:\Users\你的用户名\AppData\Local\Google\Chrome\User Data\Default\IndexedDB这个目录下,它会根据站点的名字来命名,其实上面三种也是用站点名字来命名,只是在本地用双击的方式访问时,会写成file之类的名字。

  如果读者对indexedDB还是不太了解的话,可以参考上面提到的这篇文章:《HTML5之IndexedDB使用详解》,它写得比较详细。

(最后,我会把本文中所讲到的示例文件都上传到我的博客中了,可以下载运行)

posted on 2011-08-04 13:07  Jasonling  阅读(2032)  评论(1编辑  收藏  举报