摘要:对象应该主要用于仅针对会话的小段数据的存储。如下代码限制与其它客户端数据存储方案类似,同样也有限制。最好一开始就调用方法为数据库指定一个版本号传入一个表示版本号的字符串。目前就浏览器,版本号方法已不再适用,另外,创建
Cookie 限制
由于浏览器存在各种限制,最好将整个cookie长度限制在4095B以内。
构成cookie由浏览器保存的以下几块信息构成:
名称: cookie的名称必须是经过URL编码后的字符串。 虽然它是不区分大小写的, 但是实际应用时建议把它当作区分大小写来使用。
值: cookie中字符串值, 也必须是经过URI编码的字符串。
域: 表示cookie对于哪个域有效。
路径: cookie是针对域中的哪个目录生效。
失效时间: 表示cookie失效时间的时间戳, 它是GMT格式的日期。 将该事件设置小于当前时, 就相当于删除了cookie。
安全标识: 指定该标识后, 只有使用SSL请求连接的时候cookie才会发送到服务器。 secure标识是cookie中唯一一个非键值对的部分, 它只包含一个secure单词。
使用分号加空格分开各个信息构成:
HTTP/1.1 200 OK Content-type: text/html Set-Cookie: CookieName=CookieValue; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com Other-header: other-header-value
或使用安全标志:
HTTP/1.1 200 OK Content-type: text/html Set-Cookie: CookieName=CookieValue; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com; path=/; secure //在这里! Other-header: other-header-valueJavaScript中的Cookie
在JavaScript中可以通过
document.cookie可以读取当前域名下的cookie, 是用分号隔开的键值对构成的字符串。 类似于name = aa;
age = 15;
注意所有的键值对名称和值都是经过encodeURIComponent() 编码的, 使用时要进行解码。
当给document.cookie赋值时, 不会直接覆盖现有的cookie, 而是会追加一个新的cookie。 例如:
document.cookie = "a=1"; //执行后会看到新增了一个cookie。
如:
document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Oliver"); console.log(document.cookie); //name=Oliver;
如果给cookie赋值则会增加一个cookie:
document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Oliver"); document.cookie = encodeURIComponent("age") + "=" + encodeURIComponent("18"); console.log(document.cookie); //name=Oliver; age=18
以下函数是常用的cookie读写删除方法:
var CookieUtil = { //根据key读取cookie get: function(name) { //注意对键编码 var cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null, cookieEnd; //找到cookie键 if (cookieStart > -1) { //键后面第一个分号位置 cookieEnd = document.cookie.indexOf(";", cookieStart); if (cookieEnd == -1) { cookieEnd = document.cookie.length; } //cookie值解码 cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd)); } return cookieValue; }, //设置cookie set: function(name, value, expires, path, domain, secure) { var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value); //失效时间,GMT时间格式 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; }, //删除cookie,保持相同的键、域、路径、安全选项,然后设置失效时间即可 unset: function(name, path, domain, secure) { this.set(name, "", new Date(0), path, domain, secure); } };
可以像下面这样使用上述方法:
设置cookie:
CookieUtil.set("name","Oliver"); CookieUtil.set("age","18"); console.log(document.cookie); //name=Oliver; age=18
读取cookie的值:
console.log(CookieUtil.get("name")); //Oliver
删除cookie:
CookieUtil.unset("name"); // console.log(document.cookie); //age=18
设置cookie路径、域等:
CookieUtil.set("name","Oliver","/","localhost",null,false);
否则使用下面的代码创建和删除cookie:
//新cookie document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Oliver"); //删除上面创建的cookie document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Oliver") + "; expires=" + new Date(0);
举例
css:
DOM:
welcome to my site!
关闭
js:
子Cookie
由于浏览器cookie数量是有限制的,为了减少cookie数量可以使用子cookie的方式。在一个cookie值中使用类似查询字符串的格式可以存储多组键值对,这样就不必每个键值对都占用一个cookie了。子cookie值举例:
name=name1=value1&name2=value2获取所有子cookie
获取所有子cookie并将它放在一个对象中返回,对象的属性名为子cookie名称,对象的属性值为子cookie的值。
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; } //取出cookie字符串值 cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd); if (cookieValue.length > 0) { //用&将cookie值分隔成数组 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; }获取单个子cookie
get()获取单个子cookie。
get: function(name, subName) { //获取所有子cookie var subCookies = this.getAll(name); if (subCookies) { //从属性中获取单个子cookie return subCookies[subName]; } else { return null; } }设置整个cookie
setAll设置整个cookie
setAll: function(name, subcookies, expires, path, domain, secure) { var cookieText = encodeURIComponent(name) + "=", subcookieParts = new Array(), subName; //遍历子cookie对象的属性 for (subName in subcookies) { //要先检测属性名 if (subName.length > 0 && subcookies.hasOwnProperty(subName)) { //属性名和属性值编码后=连接为字符串,并放到数组中 subcookieParts.push(encodeURIComponent(subName) + "=" + encodeURIComponent(subcookies[subName])); } } if (subcookieParts.length > 0) { //用&连接子cookie串 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(); } //设置整个cookie document.cookie = cookieText; }设置单个子cookie
set设置单个子cookie
set: function(name, subName, value, expires, path, domain, secure) { //获取当前cookie对象 var subcookies = this.getAll(name) || {}; //单个cookie对应的属性替换 subcookies[subName] = value; //重新设置cookie this.setAll(name, subcookies, expires, path, domain, secure); }删除cookie
删除整个cookie, 将失效时间设置为过期日期即可。
unsetAll: function(name, path, domain, secure) { this.setAll(name, null, new Date(0), path, domain, secure); }删除单个子cookie
删除单个子cookie,需要先获取所有子cookie对象,然后删除子cookie对应的属性,最后再将子cookie对象重新设置回去。
unset: function(name, subName, path, domain, secure) { //获取当前cookie对象 var subcookies = this.getAll(name); if (subcookies) { //删除子cookie对应的属性 delete subcookies[subName]; //重新设置cookie this.setAll(name, subcookies, null, path, domain, secure); } }以下函数是常用的子cookie读写删除方法:
var SubCookieUtil = { 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; } //取出cookie字符串值 cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd); if (cookieValue.length > 0) { //用&将cookie值分隔成数组 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; }, get: function(name, subName) { //获取所有子cookie var subCookies = this.getAll(name); if (subCookies) { //从属性中获取单个子cookie return subCookies[subName]; } else { return null; } }, setAll: function(name, subcookies, expires, path, domain, secure) { var cookieText = encodeURIComponent(name) + "=", subcookieParts = new Array(), subName; //遍历子cookie对象的属性 for (subName in subcookies) { //要先检测属性名 if (subName.length > 0 && subcookies.hasOwnProperty(subName)) { //属性名和属性值编码后=连接为字符串,并放到数组中 subcookieParts.push(encodeURIComponent(subName) + "=" + encodeURIComponent(subcookies[subName])); } } if (subcookieParts.length > 0) { //用&连接子cookie串 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(); } //设置整个cookie document.cookie = cookieText; }, set: function(name, subName, value, expires, path, domain, secure) { //获取当前cookie对象 var subcookies = this.getAll(name) || {}; //单个cookie对应的属性替换 subcookies[subName] = value; //重新设置cookie this.setAll(name, subcookies, expires, path, domain, secure); }, unsetAll: function(name, path, domain, secure) { this.setAll(name, null, new Date(0), path, domain, secure); }, unset: function(name, subName, path, domain, secure) { //获取当前cookie对象 var subcookies = this.getAll(name); if (subcookies) { //删除子cookie对应的属性 delete subcookies[subName]; //重新设置cookie this.setAll(name, subcookies, null, path, domain, secure); } } };
举例:
获取cookie:
//若cookie为document.cookie=data=name=Oliver&book=jsbook; document.cookie = "data" + "=" + "name" + "=" + "Oliver" + "&" + "book" + "=" + "jsbook"; //取得全部子cookie var data = SubCookieUtil.getAll("data"); console.log(data.name); //Oliver console.log(data.book); //jsbook //获取单个子cookie data = SubCookieUtil.get("data", "name"); console.log(data); //Oliver console.log(document.cookie); //data=name=Oliver&book=jsbook
设置cookie:
//若cookie为document.cookie=data=name=Oliver&book=jsbook; document.cookie = "data" + "=" + "name" + "=" + "Oliver" + "&" + "book" + "=" + "jsbook"; //设置两个cookie SubCookieUtil.set("data", "name", "Nicholas"); SubCookieUtil.set("data", "book", "HTMLreference") console.log(document.cookie); //data=name=Nicholas&book=HTMLreference //设置全部子cookie SubCookieUtil.setAll("data", { name: "Troy", book: "JSON"}); console.log(document.cookie); //data=name=Troy&book=JSON //修改部分子cookie SubCookieUtil.set("data", "name", "Oli"); console.log(document.cookie); //data=name=Oli&book=JSON
删除cookie:
//若cookie为document.cookie=data=name=Oliver&book=jsbook; document.cookie = "data" + "=" + "name" + "=" + "Oliver" + "&" + "book" + "=" + "jsbook"; //删除部分子cookie SubCookieUtil.unset("data", "name"); console.log(document.cookie); //data=book=jsbook //删除全部cookie SubCookieUtil.unsetAll("data"); console.log(document.cookie); //[This site has nos.]
举例:
多个提醒banner的“不再提醒功能”存在同一个cookie中
css部分:
{
margin: 0; padding: 0;
}
div#message {
border: 1px solid #ccc; background-color: #33CCFF; margin: 0;
}
p.content {
color: white; font-size: 2em; margin: 0.3em; font-family: monospace; display: block;
}
a.notShow {
width: 2em; display: block; float: right;
}
dom部分:
js部分:
//获取元素 var notShowBtn = document.getElementsByClassName("notShow"); //点击关闭按钮新建cookie并关闭banner var notShowBtnList = []; var hiddenElementCount = 0; for (var i = 0, len = notShowBtn.length; i < len; i++) { notShowBtnList.push(i); }; notShowBtnList.forEach(function(element, index) { notShowBtn[element].onclick = function() { event.preventDefault(); var value = "content" + element; SubCookieUtil.set("hideMessage", value, "hide"); notShowBtn[element].parentNode.style.display = "none"; }; }); //检查cookie当存在则关闭banner window.onload = function() { notShowBtnList.forEach(function(element, index) { var value = "content" + element; if (SubCookieUtil.get("hideMessage", value)) { notShowBtn[element].parentNode.style.display = "none"; } }); };IE用户数据
用以下代码:
然后利用setAttribute()方法保存数据
再调用save()方法保存到指定的数据空间
下一次页面载入后就可以使用load()方法读取数据
Web储存机制最初的Web Storage规范包含了两种对象的定义:sessionStorage和globalStorage,这两种都是在windows对象属性中存在的。
Storage类型该类型提供最大的存储空间来存储名值对,有一下方法:
clear()删除所有值;
key(index)获得index处的名字;
getItem(name)根据指定的name获取对应的值;
removeItem(name)删除name处的名值对;
setItem(name, value)为指定的name设置一个对应的值;
其中后三中可以直接调用也可以通过Storage对象间接调用。
还可以使用length属性来判断有多少名值对儿存放在Storage对象中,但无法判断对象中所有数据的大小,不过IE8提供了一个remainingSpace属性,用于获取还可以使用的存储空间的字节数。
sessionStorage对象sessionStorage对象存储特定于某个会话的数据,也就是该数据只保持到该浏览器关闭。存储在sessionStorage中的数据可以跨越页面刷新而存在,同时如果浏览器支持,浏览器崩溃并重启之后依然可用(Firefox和WebKit都支持,IE则不行)。存储在sessionStorage中的数据只能由最初给定对象存储数据的页面访问到,所以对页面应用有限制。
存储数据可以使用setItem()或者直接设置新的属性来存储数据。下面是这两种方法的例子。
sessionStorage.setItem("name", "Oliver"); //使用方法存储数据 sessionStorage.book = "JSON.com"; //使用属性存储数据
Firefox和Webkit实现了同步写入,所以添加到存储空间中的数据是立刻被提交的。而IE的实现则是异步写入数据,所以在设置数据和将数据实际写入磁盘之间可能有一些延迟。
在IE8中可以强制把数据写入磁盘:在设置新数据之前使用
begin()方法
并且所有设置完成之后调用
commit()方法
看以下例子:
sessionStorage.begin(); sessionStorage.name = "oli"; sessionStorage.commit();读取数据
getItem()
可以使用getItem()或者通过直接访问属性名来获取数据。
sessionStorage.name = "oli"; sessionStorage.age = 18; var val = sessionStorage.name; var val_1 = sessionStorage.age; console.log(val); //oli console.log(val_1); //18迭代数据
key()方法
通过结合length属性和key()方法来迭代sessionStorage中的值
for (var i = 0, len = sessionStorage.length; i < len; i++) { var key = sessionStorage.key(i); var val = sessionStorage.getItem(key); console.log(key + "=" + val); };
for-in方法
还可以使用for-in循环来迭代sessionStorage中的值:
for (key in sessionStorage) { var value = sessionStorage.getItem(key); console.log(key + "=" + value); }删除数据
removeItem()方法
要从sessionStorage中删除数据,可以使用delete操作符删除对象属性,也可调用removeItem()方法。
sessionStorage.name = "oli"; sessionStorage.age = 18; sessionStorage.removeItem("name"); delete sessionStorage.age;
sessionStorage对象应该主要用于仅针对会话的小段数据的存储。
globalStorage对象(被localStorage对象取代)Firefox 2中实现了globalStorage对象。作为最初的Web Storage规范的一部分,这个对象的目的是跨越会话存储数据,但有特定的访问限制。要使用globalStorage,首先要指定哪些域可以访问该数据。可以通过方括号标记使用属性来实现,如以下例子所示。
globalStorage["test.com"].name = "Oliver"; var name = globalStorage["test.com"].name;
其中,globalStorage不是Storage的实例,globalStorage["test.com"]才是
某些浏览器允许更加宽泛的访问限制,比如只根据顶级域名进行限制或者允许全局访问,如下面例子所示:
//存储数据,任何人都可以访问——不要这样做! globalStorage[""].name = "Nicholas"; //存储数据,可以让任何以.net结尾域名访问——不要这样做! globalStorage["net"].name = "Nicholas";
要避免使用这种可宽泛访问的数据存储
对globalStorage空间的访问,是一句发起请求的页面的域名、协议和端口来限制的。例如,如果使用HTTPS协议在w3cmm.com中存储了数据,那么通过HTTP访问的w3cmm.com的页面就不能访问该数据。
globalStorage的每个属性都是Storage的实例。因此,可以像如下代码中这样使用。
globalStorage["www.test.com"].name = "Nicholas"; globalStorage["www.test.com"].book = "Professional JavaScript"; globalStorage["www.test.com"].removeItem("name"); var book = globalStorage["www.test.com"].getItem("book");
如果你事先不能确定域名,那么使用location.host作为属性名比较安全。例如:
globalStorage[location.host].name = "Nicholas"; var book = globalStorage[location.host].getItem("book");localStorage对象
localStorage对象在修订过的HTML5规范中作为持久保存在客户端数据的方案取代了globalStorage。要访问同一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。这相当于globalStorage[location.host]。
由于localStorage是Storage的实例,所以可以像使用sessionStorage一样来使用它。下面是一些例子。
localStorage.setItem("name", "Oliver"); localStorage.book = "JSON"; var book = localStorage.book; var name = localStorage.getItem("name"); localStorage.removeItem("name"); delete localStorage.book; localStorage.clear();storage事件
对Storage对象进行任何修改,都会在文档上触发storage事件。当通过属性或setItem()方法保存数据,使用delete操作符或removeItem()删除数据,或着调用clear()方法时,都会发生该事件。这个事件的event对象有以下属性。
domain:发生变化的存储空间的域名。
key:设置或着删除的键名。
newValue:如果是设置值,则是新值;如果是删除键,则是null。
oldValue:键被更改之前的值。
如下代码:
var EventUtil = { addHandler: function(element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } } }; EventUtil.addHandler(document, "storage", function(event) { alert("Storage changed for " + event.domain); });限制
与其它客户端数据存储方案类似,Web Storage同样也有限制。这些限制因浏览器而异。一般来说,对存储空间大小的限制都是以每个来源(协议、域和端口)为单位的。
对于localStorage而言,大多数桌面浏览器会设置每个来源5MB的限制。Chrome和Safari对每个来源的限制是2.5MB。而ios版Safari和Android版Webkit的限制也是2.5MB。
对sessionStorage的限制也是因浏览器而异。有的浏览器对sessionStorage的大小没有限制,但Chrome、Safari、ios版Safari和Android版Webkit都有限制,也都是2.5MB。IE8+和Opera对sessionStorage的限制是5MB。
IndexedDB推荐一篇文章使用 IndexedDB,来自MDN,链接地址:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Using_IndexedDB
Indexed Database API 简称为IndexedDB,是在浏览器中保存结构化数据的一种数据库。
设计思想是建立一套API,方便保存和读取JavaScript对象,同时还支持查询和搜索。
该操作完全是异步进行的。
差不多每一次IndexedDB操作,都需要注册onerror或onsuccess事件处理程序,以确保适当地处理结果。
在使用时需要加上浏览器提供商前缀:
var indexDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB;打开数据库
indexDB.open()
IndexedDB就是一个数据库,IndexedDB最大的特色是使用对象保存数据,而不是使用表来保存数据。
使用IndexedDB前首先要打开它,即把要打开的数据库名传给indexDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求;
总之,调用indexedDB.open()会返回一个IDBRequest对象,在这个对象上可以添加onerror和onsuccess事件处理程序。
var request, database; request = indexedDB.open("Base"); request.onerror = function () { console.log(event.target.errorCode); }; request.onsuccess = function () { database = event.target.result; console.log(event.target.result); //IDBDatabase {} 指向数据库实例对象 console.log(event.target); //IDBOpenDBRequest {} 指向request对象 };
ABORT_ERR错误码8 : A request was aborted,
example, through a call to IDBTransaction.abort.
CONSTRAINT_ERR错误码4 : A mutation operation in the transaction failed because a constraint was not satisfied.For example, an object, such as an object store or index, already exists and a request attempted to create a new one.
DATA_ERR错误码5 : Data provided to an operation does not meet requirements.
NON_TRANSIENT_ERR错误码2 : An operation was not allowed on an object.Unless the cause of the error is corrected, retrying the same operation would result in failure.
NOT_ALLOWED_ERR错误码6 :
An operation was called on an object where it is not allowed or at a time when it is not allowed.It also occurs
if a request is made on a source object that has been deleted or removed.
More specific variants of this error includes: TRANSACTION_INACTIVE_ERR and READ_ONLY_ERR.
NOT_FOUND_ERR错误码3 : The operation failed because the requested database object could not be found;
example, an object store did not exist but was being opened.
QUOTA_ERR错误码11 : Either there "s not enough remaining storage space or the storage quota was reached and the user declined to give more space to the database.
READ_ONLY_ERR错误码9 : A mutation operation was attempted in a READ_ONLY transaction.
TIMEOUT_ERR错误码10 : A lock
the transaction could not be obtained in a reasonable time.
TRANSACTION_INACTIVE_ERR错误码7 : A request was made against a transaction that is either not currently active or is already finished.
UNKNOWN_ERR错误码1 : The operation failed
reasons unrelated to the database itself, and it is not covered by any other error code--
for example, a failure due to disk IO errors.
VER_ERR错误码12 : A request to open a database with a version lower than the one it already has.This can only happen with IDBOpenDBRequest.
数据库版本号indexedDB.setVersion()设置版本号
indexedDB.version获取版本号
默认情况下,IndexedDB数据库是没有版本号的。最好一开始就调用setVersion()方法为数据库指定一个版本号(传入一个表示版本号的字符串)。
目前就chrome浏览器,版本号方法已不再适用,另外,创建database后chrome浏览器自动设置版本号为"1":
var request, database; request = indexedDB.open("Base"); request.onerror = function () { console.log(event.target.errorCode); }; request.onsuccess = function () { database = event.target.result; console.log(database.setVersion); //undefined console.log(database.version); //1 };
要更新数据库的 schema,也就是创建或者删除对象存储空间,需要实现 onupgradeneeded 处理程序,这个处理程序将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。
request.onupgradeneeded
如:
// 该事件仅在较新的浏览器中被实现 request.onupgradeneeded = function(event) { // 更新对象存储空间和索引 .... };
如需设置数据库版本号,用下面方法:(旧)
var request, database; request = indexedDB.open("Base"); request.onerror = function() { console.log(event.target.errorCode); }; request.onsuccess = function() { database = event.target.result; if (database.version != "1.0") { request = database.setVersion("1.0"); request.onerror = function() { console.log(event.target.errorCode); }; request.onsuccess = function() { console.log("database name: " + database.name + "; version: " + database.version); }; } else { console.log("database name: " + database.name + "; version: " + database.version); //database name: Base; version: 1 }; };创建对象存储空间
建立完数据库连接以后,就要创建对象存储空间。
键的提供可以有几种不同的方法,这取决于对象存储空间是使用 key path 还是 key generator。
若要保存用户记录由用户名、密码组成,那么保存一条记录的对象应该如下所示:
var userData = [{ username: "007", firstName: "James", lastName: "Bond", password: "foo" }, { username: "005", firstName: "Oliver", lastName: "Young", password: "boo" }];
其中username为键(keyPath),这个应该是全局唯一的(代表一个user)。
下面是为了保存用户记录而创建对象存储空间的示例:
var request = indexedDB.open("Base", 2); //注意填写版本号 request.onerror = function() { console.log(event.target.errorCode); }; request.onupgradeneeded = function() { var database = event.target.result; var store = database.createObjectStore("users", { keyPath: "username" }); //根据username创建一个名为users的对象集合(表) };
获得了对象存储空间的引用之后,就可以使用
向对象存储空间添加数据add()或put()方法向其中添加数据
这两个方法都接收一个参数,即要保存的对象,然后这个对象就会被保存到存储空间中。
这两个方法的区别在于,如果空间中已经包含了键值相同的对象:add()会返回错误;put()则会重写原有对象;
可以使用下面的代码初始化(add()方法)存储空间:
request.onupgradeneeded = function() { var database = event.target.result; var store = database.createObjectStore("users", { keyPath: "username" }); for (var i in userData) { store.add(userData[i]); } };
for-in循环可用下面代码代替:
//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); }
以下为完整的创建数据库和存储空间以及初始化数据的代码例子:
// //数据在这里userData // var userData = [{ username: "007", //username唯一 firstName: "James", lastName: "Bond", password: "foo" }, { username: "005", firstName: "Oliver", lastName: "Young", password: "boo" }]; // //创建数据库Alldata // var request = indexedDB.open("allData"); request.onerror = function() { console.log(event.target.errorCode); }; request.onsuccess = function() { var database = event.target.result; console.log("数据库已经创建:" + database.name + "; 版本号:" + database.version); }; // //创建存储空间(表)users,并初始化数据 // var request = indexedDB.open("allData", 2); request.onupgradeneeded = function() { var database = event.target.result; //创建存储空间 var store = database.createObjectStore("users", { keyPath: "username" }); //添加数据 for (var i in userData) { store.add(userData[i]); } };
另外,下面是数据库结构:
indexedDB |-allData数据库 |-users存储空间(表) |-(userData数据)事务(读取、修改数据)
在数据库对象上调用transaction()方法就可以创建事务。任何时候,想要读取或修改数据,都要通过事务来组织所有的操作。
下面的代码保证只加载users存储空间中的数据,以便通过事务进行访问:
var transaction = database.transaction("users");
要访问多个对象存储空间,可以传入字符串数组:
var transaction = db.transaction(["users", "anotherStore"]);
上面的两个事务都是以只读的方式访问数据。要修改访问方式,必须在创建事务时传入第二个参数。
访问模式默认都是以只读的方式访问数据,要修改访问方式,必须传入第二个参数,这个参数表示访问模式:
READ_ONLY(0)表示只读;
READ_WRITE(1)表示读写;
VERSION_CHANGE(2)表示改变
注意: 旧版实验性的实现使用不建议使用的常量 IDBTransaction.READ_WRITE 而不是 "readwrite"。
所以现在一般表示读写应该传入字符串"readwrite"
其中,上面三个访问模式已经被改为:
"readonly"只读;
"readwrite"读写;
如下:
var request = database.transaction("users", "readonly");访问存储空间
objectStore()方法,并传入存储空间的名称,就可以访问特定的存储空间了。
就可以:
使用add()和put()方法添加数据;
使用get()可以取得值;
使用delete()可以删除对象;
使用clear()可以删除所有对象;
get()和delete()方法都接收一个对象键作为参数。所有的这5个方法都会返回一个新的请求对象。例如:
var request = database.transaction("users", "readonly").objectStore("users").get("007"); request.onsuccess = function () { var result = event.target.result; //"James" console.log(result.firstName); }; var request = database.transaction("users", "readwrite").objectStore("users").delete("005");
事务本身也有事件处理程序:onerror和oncomplete。
这两个事件可以提供事务级的状态信息。
注意:通过oncomplete事件的事件对象访问不到get()请求返回的任何数据,必须在onsuccess事件处理程序中才能访问到。
以下为实例:
var userData = [{ username: "007", //username唯一 firstName: "James", lastName: "Bond", password: "foo" }, { username: "005", firstName: "Oliver", lastName: "Young", password: "boo" }]; //创建数据库Alldata var request = indexedDB.open("allData"); request.onerror = function() { console.log(event.target.errorCode); }; request.onsuccess = function() { var database = event.target.result; console.log("数据库已经创建:" + database.name + "; 版本号:" + database.version); }; //创建存储空间(表)users并初始化数据 var request = indexedDB.open("allData", 2); var database; request.onupgradeneeded = function() { database = event.target.result; //创建存储空间 var store = database.createObjectStore("users", { keyPath: "username" }); //添加数据 for (var i in userData) { store.add(userData[i]); } }; var newPerson = { username: "001", firstName: "Troy", lastName: "Ruby", password: "hello" }; //事务 var request = indexedDB.open("allData", 2); request.onsuccess = function () { var database = event.target.result; // var request = database.transaction("users").objectStore("users").add(newPerson); //报错,因为只读,需要设置为"readwrite" var request = database.transaction("users", "readwrite").objectStore("users").add(newPerson); var request = database.transaction("users", "readonly").objectStore("users").get("007"); request.onsuccess = function () { var result = event.target.result; //"James" console.log(result.firstName); }; var request = database.transaction("users", "readwrite").objectStore("users").delete("005"); };使用游标查询
游标就是指向结果集的一个指针,在对象存储空间上调用;在需要检索多个对象的情况下,需要在事务内部创建游标。注意,必须在事务内部创建!
openCursor()方法可以创建游标。
openCursor()方法返回的也是一个请求对象,也需要为该对象指定onsuccess和onerror事件处理函数
var transaction = database.transaction("users", "readonly").objectStore("users").openCursor();
或者:
var store = db.transaction("users").objectStore("users"), request = store.openCursor(); request.onsuccess = function(event) { // 处理成功 }; request.onerror = function(event) { // 处理失败 };IDBCursor
在前面的onsuccess事件处理程序执行时,可以通过event.target.result取得存储空间中的下一个对象。
IDBCursor实例具有以下几个属性:
key: 对象的键;
value:实际的对象;
direction:数值,表示游标走动的方向。
默认是IDBCursor.NEXT(0), 表示下一项。
IDBCursor.NEXT_TO_DUPLICATE(1), 表示下一个不重复的项;
IDBCursor.PREV(2)表示前一项;
IDBCursor.PREV_NO_DUPLICATE表示前一个不重复的项。
primaryKey:游标使用的键,有可能是对象键,也有可能是索引键(后面会讨论索引)
检索结果信息要检索某个信息的结果,如下代码:
var transaction = database.transaction("users", "readonly").objectStore("users").openCursor(); transaction.onsuccess = function() { var cursor = event.target.result; if (cursor) { console.log(cursor); //IDBCursorWithValue {}direction: "next"key: "005"primaryKey: "005"source: IDBObjectStorevalue: Object__proto__: IDBCursorWithValue console.log(cursor.value); //Object {username: "005", firstName: "Oliver", lastName: "Young", password: "boo"} console.log(cursor.key); //005 console.log("Key: " + cursor.key + "; Value: " + JSON.stringify(cursor.value)); //Key: 005; Value: {"username":"005","firstName":"Oliver","lastName":"Young","password":"boo"} } };
注意:返回的cursor.value是一个对象,必要时需要用到JSON.stringify方法
使用游标更新记录update()方法更新记录
调用update()方法可以使用指定的对象更新当前游标的value:
var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(); transaction.onsuccess = function() { var cursor = event.target.result; if (cursor) { if (cursor.key == "005") { console.log(cursor.value.firstName); //游标在第一个位置”005“所以他的firstName就是"Oliver"; 刷新浏览器,显示的结果则为Oli var value = cursor.value; //更改当前游标所指的对象("005")的firstName为"Oli"; value.firstName = "Oli"; var updateRequest = cursor.update(value); //使用update方法请求保存更新 updateRequest.onerror = function () { console.log(event.target.errorCode); }; updateRequest.onsuccess = function () { console.log("success"); //success }; } } }; transaction.onerror = function() { console.log(event.target.errorCode); };使用游标删除记录
delete()方法删除记录
如:
var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(); transaction.onsuccess = function() { var cursor = event.target.result; if (cursor) { if (cursor.key == "005") { var deleteRequest = cursor.delete(); //请求删除此项 deleteRequest.onerror = function () { // body... }; deleteRequest.onsuccess = function () { // body... }; } } };
注意:如果当前的事务没有修改对象存储空间的权限,update()和delete()会抛出错误。
移动游标默认情况下每个游标只发起一次请求;要想发起另一次请求,必须调用下面的一个方法:
continue(key): 移动到结果集的下一项。参数key是可选的,不指定这个参数,游标移动到下一项;指定这个参数的话,游标会移动到指定键的位置。
advance(count): 向前移动count指定的项数。
遍历使用移动游标的方法,可以用来遍历存储空间中的所有项:
var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(); transaction.onsuccess = function() { 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键范围由IDBKeyRange的实例表示。
有四中定义键范围的方式:
only()方法:var onlyrange = IDBKeyRange.only("007"); var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(onlyrange);
将onlyrange变量传入openCursor方法中
lowerBound()方法:第二种定义键范围的方法是指定结果集的下界。下界表示游标开始的位置。
如果想要忽略键为"007"的对象本身,从它的下一个对象开始,可以传入第二个参数true:
var lowerBound = IDBKeyRange.lowerBound("003", true); //第二个参数为true表示不包括003 var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(lowerBound);upperBound()方法
第三种定义键范围的方法是指定结果集的上界,也就是指定游标不能超过哪个键。
如果不想包含键为指定值的对象,同样传入第二个参数true:
var upperBound = IDBKeyRange.upperBound("005", true); //第二个参数为true表示不包括005 var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(upperBound);bound()方法
使用bound()方法可以同时指定上下界。
这个方法可以接收四个参数:表示下界的键,表示上界的键,可选的表示是否跳过下界的布尔值和可选的表示是否跳过上界的布尔值。
var bound = IDBKeyRange.bound("003", "005", true, false); //第三和第四个参数为true和false表示不包括003包括005 var transaction = database.transaction("users", "readwrite").objectStore("users").openCursor(bound);设定游标方向
openCursor()可以接收两个参数,一个是刚才的IDBKeyRange实例,第二个是表示方向的数值常量,也就是前面讲到的IDBCursor中的常量。
正常情况下,游标都是从存储空间的第一项开始,调用continue()或advance()前进到最后一项。游标的默认方向值是
IDBCursor.NEXT或IDBCursor.NEXT_NO_DUPLICATE
IDBCursor.next或IDBCursor.nextunique
也可以创建一个游标,从最后一个对象开始,逐个迭代,直到第一个对象,这时要传入的常量是:
IDBCursor.PREV或IDBCursor.PREV_NO_DUPLICATE
IDBCursor.prev或IDBCursor.prevunique
索引对于有些数据,需要创建多个键,如把用户ID作为主键,以用户名创建索引
创建索引首先引用对象存储空间,然后调用
createIndex()方法
如下:
request.onupgradeneeded = function() { var database = event.target.result; var store = database.createObjectStore("users", { keyPath: "username" }); for (var i in userData) { store.add(userData[i]); } var index = store.createIndex("firstName", "firstName", { unique: false }); //创建名为firstName的索引,属性名为firstName,属性值非唯一 };使用索引
index()方法
如下:
var index = database.transaction("users").objectStore("users").index("firstName"); console.log(index); //IDBIndex对象索引上创建游标
openCursor()方法
如下:
var request = index.openCursor(); request.onsuccess = function () { console.log(event.target.result); //IDBCursorWithValue对象 };
遍历:
var request = index.openCursor(); request.onsuccess = function() { var cursor = event.target.result; //IDBCursorWithValue对象 if (cursor) { console.log(cursor.key + "; " + JSON.stringify(cursor.value)); cursor.continue(); } else { console.log("done"); } // Alice; { // "username": "003", // "firstName": "Alice", // "lastName": "Young", // "password": "boo" // } // (index): 87 James; { // "username": "007", // "firstName": "James", // "lastName": "Bond", // "password": "foo" // } // (index): 87 Oliver; { // "username": "005", // "firstName": "Oliver", // "lastName": "Young", // "password": "boo" // } // (index): 90 done };索引上创建主键的游标
openKeyCursor()方法
调用这个特殊的只返回每条记录主键的游标,这个方法在event.result.key中保存索引键,在event.result.value中保存的则是主键,而不是整个对象
索引中取得对象get()方法
只要传入相应的索引键即可:
var store = db.transaction("users").objectStore("users"); var index = store.index("firstName"); var request = index.get("007");根据索引键取得主键
getKey()方法
这个方法中event.result.value等于主键的值,而不是整个对象:
var store = db.transaction("users").objectStore("users"); var index = store.index("firstName"); var request = index.getKey("007");索引IDBIndex对象的属性
name:索引的名字
keyPath:createIndex方法中属性的路径
objectStore:索引的对象存储空间
unique:表示索引键是否唯一
访问所有索引indexNames属性
如:
var indexNames = store.indexNames; for (var i = 0, len = store.indexNames.length; i < len; i++) { var index = store.index(indexNames[i]); console.log(index.name); //firstName //lastName };删除索引
deleteIndex()
如:
store.deleteIndex("firstName");并发问题
每次打开数据库,都应该指定onversionchange事件处理程序:
request.onsuccess = function() { database = event.target.result; database.onversionchange = function () { database.close(); }; };
当设置version时,指定请求的onblocked事件处理程序也很重要:
var request = database.setVersion("2.0"); request.onblocked = function () { alert("轻先关闭其他标签页后再尝试"); }; request.onsuccess = function () { //处理成功,继续 };限制
同源
多个限制5MB大小
Firefox不允许本地访问IndexedDB
完整的例子var userData = [{ username: "007", firstName: "James", lastName: "Bond", password: "foo" }, { username: "005", firstName: "Oliver", lastName: "Young", password: "boo" }, { username: "003", firstName: "Alice", lastName: "Young", password: "boo" }]; var newData = { username: "001", firstName: "Ali", lastName: "Bound", password: "hello" }; var anotherNewData = { username: "001", firstName: "holyshit", lastName: "Bound", password: "hello" }; var dbName = "allData", dbVersion = 1, dbStoreName = "users"; var db; //初始化数据库 function initDb() { console.log("initDb..."); //初始化数据库... var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { db = this.result; console.log("initDb Done."); }; req.onerror = function() { console.log("initDb:", event.target.errorCode); }; req.onupgradeneeded = function() { console.log("initDb.onupgradeneeded"); var store = event.target.result.createObjectStore(dbStoreName, { keyPath: "username" }); //这里不能用db,而是event.target.result; store.createIndex("firstName", "firstName", { unique: false }); store.createIndex("lastName", "lastName", { unique: false }); }; } initDb(); //添加数据 function addData(data) { console.log("add data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readwrite"); var store = transaction.objectStore(dbStoreName); if (Object.prototype.toString.call(data).toString() === "[object Array]") { for (var i in data) { store.add(data[i]); } } else if (Object.prototype.toString.call(data).toString() === "[object Object]") { store.add(data); } else { console.log("adding data: please choose Array or Object."); } }; } addData(userData); //提取数据 function getData(key, callback) { console.log("get data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var data = store.get(key); data.onsuccess = function() { var result = event.target.result; if (result) { callback(result); console.log("get data done."); } }; }; } // getData("003", function (result) { // console.log(result); // }); //删除数据 function deleteData(key) { console.log("delete data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readwrite"); var store = transaction.objectStore(dbStoreName); var data = store.delete(key); data.onsuccess = function() { console.log("delete data done."); }; }; } // deleteData("003"); //清空数据 function clearData() { console.log("delete data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readwrite"); var store = transaction.objectStore(dbStoreName); var data = store.clear(); data.onsuccess = function() { console.log("clear data done."); }; }; } // clearData(); //游标提取数据 function cursorGetData(key, callback) { console.log("cursor get data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var cursor = store.openCursor(); cursor.onsuccess = function() { var cursor = event.target.result; if (cursor.key !== key) { cursor.continue(); } else { var result = cursor.value; callback(result); console.log("cursor get data done."); } }; }; } // cursorGetData("007", function (result) { // console.log(result); // }); //游标修改数据 function cursorUpdateData(key, property, newValue) { console.log("cursor update data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readwrite"); var store = transaction.objectStore(dbStoreName); var cursor = store.openCursor(); cursor.onsuccess = function() { var cursor = event.target.result; if (cursor.key !== key) { cursor.continue(); } else { var target = cursor.value; for (var i in target) { if (i === property) { var value = Object.defineProperty(target, property, { value: newValue }); var updateReq = cursor.update(value); console.log("cursor update data done."); } } } }; }; } // cursorUpdateData("003", "firstName", "Ali"); //游标删除数据 function cursorDeleteData(key) { console.log("cursor delete data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readwrite"); var store = transaction.objectStore(dbStoreName); var cursor = store.openCursor(); cursor.onsuccess = function() { var cursor = event.target.result; if (cursor.key !== key) { cursor.continue(); } else { var deleteReq = cursor.delete(); console.log("cursor delete data done."); } }; }; } // cursorDeleteData("003"); //游标遍历所有项 function cursorTraversalData(callback) { console.log("cursor tarversal data..."); var req = indexedDB.open(dbName, dbVersion); var result = []; req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var cursor = store.openCursor(); cursor.onsuccess = function() { var cursor = event.target.result; if (cursor) { result.push(cursor.value); cursor.continue(); } else { console.log("cursor traversal data done."); if (result.length > 0) { callback(result); } } }; }; } // cursorTraversalData(function (result) { // for (var i in result) { // console.log(JSON.stringify(result[i])); // } // }); //遍历范围内所有项 function boundTraversalData(lower, upper, boo1, boo2, callback) { console.log("bound tarversal data..."); var req = indexedDB.open(dbName, dbVersion); var result = []; req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var bound = IDBKeyRange.bound(lower, upper, boo1, boo2); var cursor = store.openCursor(bound); cursor.onsuccess = function() { var cursor = event.target.result; if (cursor) { result.push(cursor.value); cursor.continue(); } else { console.log("bound traversal data done."); if (result.length > 0) { callback(result); } } }; }; } // boundTraversalData("003", "007", true, true, function(result) { // for (var i in result) { // console.log(JSON.stringify(result[i])); // } // }); //索引中取得对象 function indexGetData(index, key, callback) { console.log("index get data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var target = store.index(index); var data = target.get(key); data.onsuccess = function() { var result = event.target.result; if (result) { callback(result); console.log("index get data done."); } else { console.log("index get data: error output."); } }; }; } // indexGetData("firstName", "Alice", function (result) { // console.log(result); // }); //索引中取得主键 function indexGetKey(index, key, callback) { console.log("index get data..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var target = store.index(index); var data = target.getKey(key); data.onsuccess = function() { var result = event.target.result; if (result) { callback(result); console.log("index get key done."); } else { console.log("index get key: error output."); } }; }; } // indexGetKey("firstName", "Alice", function (result) { // console.log(result); // }); //访问所有索引 function getAllIndex(callback) { console.log("get all index..."); var req = indexedDB.open(dbName, dbVersion); req.onsuccess = function() { var transaction = db.transaction(dbStoreName, "readonly"); var store = transaction.objectStore(dbStoreName); var indexNames = store.indexNames; if (indexNames.length) { for (var i = 0, len = store.indexNames.length; i < len; i++) { var index = store.index(indexNames[i]); callback(index); } } }; } // getAllIndex(function (index) { // console.log(index.name); // console.log(index.keyPath); // console.log(index.objectStore.name); // console.log(index.unique); // });
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/78915.html
摘要:在线离线应用缓存就是一个从浏览器的缓存中分出来的缓存去,在缓存中保存数据,可以使用一个描述文件,列出要下载和缓存的资源。 离线检测 HTML5中定义的: navigator.onLine如果为true则表示设备能够上网 注意是大写的L(onLine); 用下面代码检测属性状态: if (navigator.onLine) { // statement if online } e...
摘要:字节流这个简单的模型将数据存储为长度不透明的字节字符串变量,将任何形式的内部组织留给应用层。字节流数据存储的代表例子包括文件系统和云存储服务。使用同步存储会阻塞主线程,并为应用程序的创建冻结体验。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 16 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它...
摘要:应用缓存的应用缓存,或者简称为,是专门为开发离线应用而设计的。应用缓存还有很多相关的事件,表示其状态的改变。数据存储,通常直接叫做,最初是在客户端用于存储会话信息的。也就是使用值来存储多个名称值对儿。 所谓Web离线应用,就是在设备不能上网的情况下仍然可以运行的应用。开发离线Web应用需要几个步骤:(1)确保应用知道设备是否能上网;(2)应用还必须能访问一定的资源(图像、JavaScr...
摘要:离线检测含义设备能否上网代码注和,和最新的没问题应用缓存缓存的目的是专门为网页离线设计的,当然在在线情况也会缓存机制当用户在地址输入请求的地址去请求网页时,浏览器会先本地缓存中查看是否有对应的缓存文件,如果有然后查看新鲜度就是是否过期了,如 23.1 离线检测 含义:设备能否上网 代码: navigator.onLine 注:IE6+和safari+5,firefox3+和ope...
摘要:离线应用与客户端存储离线检测定义了属性来检测设备是在线还是离线。应用缓存还有很多相关的事件,表示其状态的改变。 离线应用与客户端存储 离线检测 HTML5定义了navigator.onLine属性来检测设备是在线还是离线。这个属性为true表示设备能上网,值为false表示设备离线。这个属性的关键是浏览器必须知道设备能否访问网络,从而返回正确的值 不同浏览器之间有小差异 IE6+...
阅读 772·2021-08-23 09:46
阅读 907·2019-08-30 15:44
阅读 2541·2019-08-30 13:53
阅读 3019·2019-08-29 12:48
阅读 3816·2019-08-26 13:46
阅读 1715·2019-08-26 13:36
阅读 3493·2019-08-26 11:46
阅读 1384·2019-08-26 10:48