「这是我参与11月更文挑战的第 9 天,活动详情查看:2021最后一次更文挑战」。
在HTML5之前,应用程序数据只能存储在 cookie
中,并且会包含在每个服务器请求中。与 cookie
不同,浏览器本地存储限制要大得多(至少5MB),并且信息不会被传输到服务器。本文将要介绍的本地存储包括:localStorage
、Web SQL Database
、IndexedDB
。
localStorage
localStorage
在一般浏览器支持的是5M大小,在不同的浏览器中 localStorage
会有所不同。
localstorage
的API有两个:localStorage
和sessionStorage
,存在于window
对象中:localStorage
对应window.localStorage
,sessionStorage
对应window.sessionStorage
。
localStorage
和sessionStorage
的区别主要是在于其生存期,localStorage
属于永久性存储,而sessionStorage
当会话结束的时候,sessionStorage
中的键值对会被清空。
localstorage
为标准的键值对(Key-Value,简称KV)数据类型,简单也易扩展,只要以某种编码方式把想要存储进localstorage
的对象给转化成字符串,就能轻松支持。
由于浏览器的安全策略,localstorage是无法跨域的,也无法让子域名继承父域名的localstorage数据。
在使用方面,sessionStorage
和localStorage
方法是一样的。
初始化
在浏览器中使用需要判断兼容性,判断浏览器是否支持。
if(!window.localStorage){
alert("浏览器支持localstorage");
return false;
}
数据存储操作
数据写入的方法就是直接给window.localStorage
添加一个属性,例如:window.localStorage.a
或者 window.localStorage["b"]
。它的读取、写、删除操作方法很简单,是以键值对的方式存在的,如下:
const storage=window.localStorage;
//写入a字段
storage["a"]=1;
//写入b字段
storage.b=1;
//写入c字段
storage.setItem("c",3);
console.log(typeof storage["a"]);
console.log(typeof storage["b"]);
console.log(typeof storage["c"]);
storage.getItem("a");
运行后再开发工具下可以看到:
写入数据推荐使用setItem()
,读取数据推荐使用getItem()
,清除键值对使用removeItem()
。如果希望一次性清除所有的键值对,可以使用clear()
。另外,HTML5还提供了一个key()
方法,可以在不知道有哪些键值的时候使用,如下:
const storage = window.localStorage;
function showStorage(){
for(var i=0;i<storage.length;i++){
//key(i)获得相应的键,再用getItem()方法获得对应的值
console.log(storage.key(i)+ " : " + storage.getItem(storage.key(i)) + "<br>");
}
}
需要注意的是,HTML5本地存储只能存字符串,任何格式存储的时候都会被自动转为字符串,所以读取的时候,需要自己进行类型的转换。
对于json
数据侧存储需要调用JSON.stringify()
将其转为字符串后才能存储,读取出来后调用JSON.parse()
将字符串转为json格式。
storage事件
事件可以对键值对的改变进行监听,使用方法如下:
if(window.addEventListener){
window.addEventListener("storage",handle_storage,false);
}else if(window.attachEvent){
window.attachEvent("onstorage",handle_storage);
}
function handle_storage(e){
if(!e){e=window.event;}
//逻辑
}
对于事件变量e,是一个StorageEvent
对象,提供了一些实用的属性,可以很好的观察键值对的变化,如下表:
Property | Type | Description |
---|---|---|
target | EventTarget | 事件目标(DOM 树中的最大目标) |
type | DOMString | 事件的类型 |
bubbles | Boolean | 事件通常是否会出现泡沫 |
cancelable | Boolean | 事件是否可取消 |
key | DOMString (string) | 键更改时 |
oldValue | DOMString (string) | 正在更改键的旧值 |
newValue | DOMString (string) | 正在更改键的新值 |
url | DOMString (string) | 键更改的文档的地址 |
storageArea | Storage | 受影响的存储对象 |
Web SQL Database
Web SQL Database实际上已经被废弃
虽然Html5已经提供了功能强大的localStorage
和sessionStorage
,但是他们两个都只能提供存储简单数据结构的数据,对于复杂的Web应用的数据却无能为力。逆天的是Html5提供了一个浏览器端的数据库支持,允许直接通JavaScript的API在浏览器端创建一个本地的数据库,而且支持标准的SQL
的CRUD
操作,让离线的Web应用更加方便的存储结构化的数据。接下里介绍一下本地数据的相关API和用法。
- 第一步:
openDatabase
方法:创建一个访问数据库的对象。 - 第二步:使用第一步创建的数据库访问对象来执行
transaction
方法,通过此方法可以设置一个开启事务成功的事件响应方法,在事件响应方法中可以执行SQL. - 第三步:通过
executeSql
方法执行查询,当然查询可以是:CRUD
。
openDatabase方法
初次调用时创建数据库,以后就是建立连接了。
//Demo:获取或者创建一个数据库,如果数据库不存在那么创建之
const dataBase = openDatabase("student", "1.0", "学生表", 1024 * 1024, function () { });
openDatabase
方法打开一个已经存在的数据库,如果数据库不存在,它还可以创建数据库。几个参数意义分别是:
- 数据库名称。
- 数据库的版本号,目前来说传个1.0就可以了,当然可以不填;
- 对数据库的描述。
- 设置分配的数据库的大小(单位是kb)。
- 回调函数(可省略)。
db.transaction
方法可以设置一个回调函数,此函数可以接受一个参数就是我们开启的事务的对象。然后通过此对象可以进行执行Sql脚本,跟下面的步骤可以结合起来。
通过executeSql方法执行查询
ts.executeSql(sqlQuery,[value1,value2..],dataHandler,errorHandler)
参数说明:
qlQuery
:需要具体执行的sql语句,可以是create
、select
、update
、delete
;[value1,value2..]
:sql语句中所有使用到的参数的数组,在executeSql
方法中,将s>
语句中所要使用的参数先用“?”代替,然后依次将这些参数组成数组放在第二个参数中dataHandler
:执行成功是调用的回调函数,通过该函数可以获得查询结果集;errorHandler
:执行失败时调用的回调函数;
IndexedDB
Web Storage(Local Storage和 Session Storage)与
IndexedDB
。Web Storage
使用简单字符串键值对在本地存储数据,方便灵活,但是对于大量结构化数据存储力不从心,IndexedDB
是为了能够在客户端存储大量的结构化数据,并且使用索引高效检索的API。
异步API
在IndexedDB
大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求——响应的模式,比如打开数据库的操作
const request=window.indexedDB.open('testDB');
这条指令并不会返回一个DB对象的句柄,我们得到的是一个IDBOpenDBRequest
对象,而我们希望得到的DB
对象在其result
属性中
这条指令请求的响应是一个 IDBDatabase
对象,这就是IndexedDB
对象
除了result
,IDBOpenDBRequest
接口定义了几个重要属性
onerror
: 请求失败的回调函数句柄onsuccess
:请求成功的回调函数句柄onupgradeneeded
:请求数据库版本变化句柄
所谓异步API是指并不是这条指令执行完毕,我们就可以使用request.result来获取indexedDB对象了,就像使用ajax一样,语句执行完并不代表已经获取到了对象,所以我们一般在其回调函数中处理。
创建数据库
刚才的语句已经展示了如何打开一个indexedDB
数据库,调用indexedDB.open
方法就可以创建或者打开一个indexedDB
。看一个完整的处理
function openDB(name) {
const request = window.indexedDB.open(name);
request.onerror = function (e) {
console.log("OPen Error!");
};
request.onsuccess = function (e) {
myDB.db = e.target.result;
};
}
const myDB = {
name: "test",
version: 1,
db: null,
};
openDB(myDB.name);
代码中定义了一个myDB
对象,在创建indexedDB
request
的成功毁掉函数中,把request
获取的DB
对象赋值给了myDB
的db
属性,这样就可以使用myDB.db
来访问创建的indexedDB
了。
version
我们注意到除了onerror
和onsuccess
,IDBOpenDBRequest
还有一个类似回调函数句柄——onupgradeneeded
。这个句柄在我们请求打开的数据库的版本号和已经存在的数据库版本号不一致的时候调用。
indexedDB.open()
方法还有第二个可选参数,数据库版本号,数据库创建的时候默认版本号为1
,当传入的版本号和数据库当前版本号不一致的时候onupgradeneeded
就会被调用,当然不能试图打开比当前数据库版本低的version
,否则调用的就是onerror
了,修改一下刚才例子
function openDB (name,version) {
const version=version || 1;
const request=window.indexedDB.open(name,version);
request.onerror=function(e){
console.log(e.currentTarget.error.message);
};
request.onsuccess=function(e){
myDB.db=e.target.result;
};
request.onupgradeneeded=function(e){
console.log('DB version changed to '+version);
};
}
const myDB={
name:'test',
version:3,
db:null
};
openDB(myDB.name,myDB.version);
由于刚才已经创建了版本为1
的数据库,打开版本为3
的时候,会在控制台输出:DB version changed to 3
关闭与删除数据库
关闭数据库可以直接调用数据库对象的close方法
function closeDB(db){
db.close();
}
删除数据库使用indexedDB
对象的deleteDatabase
方法
function deleteDB(name){
indexedDB.deleteDatabase(name);
}
简单调用
const myDB={
name:'test',
version:3,
db:null
};
openDB(myDB.name,myDB.version);
setTimeout(function(){
closeDB(myDB.db);
deleteDB(myDB.name);
},500);
由于异步API愿意,不能保证能够在closeDB
方法调用前获取db
对象(实际上获取db对象也比执行一条语句慢得多),所以用了setTimeout
延迟了一下。当然我们注意到每个indexedDB
实例都有onclose
回调函数句柄,用以数据库关闭的时候处理。
object store
有了数据库后我们自然希望创建一个表用来存储数据,但indexedDB
中没有表的概念,而是objectStore
,一个数据库中可以包含多个objectStore
,objectStore
是一个灵活的数据结构,可以存放多种类型数据。也就是说一个objectStore
相当于一张表,里面存储的每条数据和一个键相关联。
可以使用每条记录中的某个指定字段作为键值(keyPath),也可以使用自动生成的递增数字作为键值(keyGenerator
),也可以不指定。选择键的类型不同,objectStore
可以存储的数据结构也有差异
键类型 | 存储数据 |
---|---|
不使用 | 任意值,但是每添加一条数据的时候需要指定键参数 |
keyPath | Javascript对象,对象必须有一属性作为键值 |
keyGenerator | 任意值 |
都使用 | JavaScript对象,如果对象中有keyPath指定的属性则不生成新的键值,如果没有自动生成递增键值,填充keyPath指定属性 |
事务
在对新数据库做任何事情之前,需要开始一个事务。事务中需要指定该事务跨越哪些object store。
事务具有三种模式
- 只读:
read
,不能修改数据库数据,可以并发执行 - 读写:
readwrite
,可以进行读写操作 - 版本变更:
verionchange
const transaction=db.transaction([students','taecher']); //打开一个事务,使用students 和teacher object store
const objectStore=transaction.objectStore('students'); //获取students object store
给object store添加数据
调用数据库实例的createObjectStore
方法可以创建object store
,方法有两个参数:store name
和键类型。调用store
的add
方法添加数据。有了上面知识,可以向object store
内添加数据了
keyPath
因为对新数据的操作都需要在transaction
中进行,而transaction
又要求指定object store
,所以只能在创建数据库的时候初始化object store
以供后面使用,这正是onupgradeneeded
的一个重要作用,修改一下之前代码
function openDB (name,version) {
const version=version || 1;
const request=window.indexedDB.open(name,version);
request.onerror=function(e){
console.log(e.currentTarget.error.message);
};
request.onsuccess=function(e){
myDB.db=e.target.result;
};
request.onupgradeneeded=function(e){
const db=e.target.result;
if(!db.objectStoreNames.contains('students')){
db.createObjectStore('students',{keyPath:"id"});
}
console.log('DB version changed to '+version);
};
}
这样在创建数据库的时候就为其添加了一个名为students
的object store
,准备一些数据以供添加
const students=[{
id:1001,
name:"Byron",
age:24
},{
id:1002,
name:"Frank",
age:30
},{
id:1003,
name:"Aaron",
age:26
}];
function addData(db,storeName){
const transaction=db.transaction(storeName,'readwrite');
const store=transaction.objectStore(storeName);
for(var i=0;i<students.length;i++){
store.add(students[i]);
}
}
openDB(myDB.name,myDB.version);
setTimeout(function(){
addData(myDB.db,'students');
},1000);
这样就在students object store
里添加了三条记录,以id
为键,在chrome控制台看看效果
keyGenerate
function openDB (name,version) {
const version=version || 1;
const request=window.indexedDB.open(name,version);
request.onerror=function(e){
console.log(e.currentTarget.error.message);
};
request.onsuccess=function(e){
myDB.db=e.target.result;
};
request.onupgradeneeded=function(e){
const db=e.target.result;
if(!db.objectStoreNames.contains('students')){
db.createObjectStore('students',{autoIncrement: true});
}
console.log('DB version changed to '+version);
};
}
查找数据
可以调用object store
的get方法通过键获取数据,以使用keyPath
做键为例:
function getDataByKey(db,storeName,value){
const transaction=db.transaction(storeName,'readwrite');
const store=transaction.objectStore(storeName);
const request=store.get(value);
request.onsuccess=function(e){
var student=e.target.result;
console.log(student.name);
};
}
更新数据
可以调用object store
的put
方法更新数据,会自动替换键值相同的记录,达到更新目的,没有相同的则添加,以使用keyPath做键为例
function updateDataByKey(db, storeName, value) {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const request = store.get(value);
request.onsuccess = function (e) {
const student = e.target.result;
student.age = 35;
store.put(student);
};
}
删除数据及object store
调用object store
的delete
方法根据键值删除记录
function deleteDataByKey(db, storeName, value) {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
store.delete(value);
}
调用object store
的clear
方法可以清空object store
function clearObjectStore(db, storeName) {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
store.clear();
}
调用数据库实例的deleteObjectStore
方法可以删除一个object store
,这个就得在onupgradeneeded
里面调用了
if (db.objectStoreNames.contains("students")) {
db.deleteObjectStore("students");
}
创建索引
可以在创建object store
的时候指明索引,使用object store
的createIndex
创建索引,方法有三个参数:
- 索引名称
- 索引属性字段名
- 索引属性值是否唯一
function openDB(name, version) {
const version = version || 1;
const request = window.indexedDB.open(name, version);
request.onerror = function (e) {
console.log(e.currentTarget.error.message);
};
request.onsuccess = function (e) {
myDB.db = e.target.result;
};
request.onupgradeneeded = function (e) {
const db = e.target.result;
if (!db.objectStoreNames.contains("students")) {
const store = db.createObjectStore("students", { keyPath: "id" });
store.createIndex("nameIndex", "name", { unique: true });
store.createIndex("ageIndex", "age", { unique: false });
}
console.log("DB version changed to " + version);
};
}
利用索引获取数据
function getDataByIndex(db, storeName) {
const transaction = db.transaction(storeName);
const store = transaction.objectStore(storeName);
const index = store.index("nameIndex");
index.get("Byron").onsuccess = function (e) {
const student = e.target.result;
console.log(student.id);
};
}
这样可以利用索引快速获取数据,name
的索引是唯一的没问题,但是对于age
索引只会取到第一个匹配值,要想得到所有age符合条件的值就需要使用游标了
游标
在indexedDB
中使用索引和游标是分不开的,对数据库熟悉的同学很好理解游标是什么东东,有了数据库object store
的游标,就可以利用游标遍历object store了。
使用object store的openCursor()
方法打开游标
function fetchStoreByCursor(db, storeName) {
const transaction = db.transaction(storeName);
const store = transaction.objectStore(storeName);
const request = store.openCursor();
request.onsuccess = function (e) {
const cursor = e.target.result;
if (cursor) {
console.log(cursor.key);
const currentStudent = cursor.value;
console.log(currentStudent.name);
cursor.continue();
}
};
}
curson.contine()
会使游标下移,直到没有数据返回undefined
index与游标结合
要想获取age
为26
的student
,可以结合游标使用索引
function getMultipleData(db, storeName) {
const transaction = db.transaction(storeName);
const store = transaction.objectStore(storeName);
const index = store.index("ageIndex");
const request = index.openCursor(IDBKeyRange.only(26));
request.onsuccess = function (e) {
const cursor = e.target.result;
if (cursor) {
const student = cursor.value;
console.log(student.id);
cursor.continue();
}
};
}
这样可是使用索引打开一个游标,参数下面会讲到,在成功的句柄内获得游标便利age
为26的student
,也可以通过index.openKeyCursor()
方法只获取每个对象的key
值。
指定游标范围
index.openCursor()
/index.openKeyCursor()
方法在不传递参数的时候会获取object store
所有记录,像上面例子一样我们可以对搜索进行筛选。
可以使用key range
限制游标中值的范围,把它作为第一个参数传给 openCursor()
或是 openKeyCursor()
。
IDBKeyRange.only(value)
:只获取指定数据IDBKeyRange.lowerBound(value,isOpen)
:获取最小是value
的数据,第二个参数用来指示是否排除value
值本身,也就是数学中的是否是开区间IDBKeyRange.upperBound(value,isOpen)
:和上面类似,用于获取最大值是value的数据IDBKeyRange.bound(value1,value2,isOpen1,isOpen2)
:不用解释了吧
获取名字首字母在B-E的student
function getMultipleData(db, storeName) {
const transaction = db.transaction(storeName);
const store = transaction.objectStore(storeName);
const index = store.index("nameIndex");
const request = index.openCursor(IDBKeyRange.bound("B", "F", false, true));
request.onsuccess = function (e) {
const cursor = e.target.result;
if (cursor) {
const student = cursor.value;
console.log(student.name);
cursor.continue();
}
};
}