DOM 存储是一套在 Web Applications 1.0 规范中首次引入的与存储相关的特性的总称,现在已经分离出来,单独发展成为独立的 W3C Web 存储规范。 DOM 存储被设计为用来提供一个更大存储量、更安全、更便捷的存储方法,从而可以代替掉将一些不需要让服务器知道的信息存储到 cookies 里的这种传统方法。 上面一段是对 Dom Storage 存储机制的官方表述。看起来,Dom Storage 机制类似 Cookies,但有一些优势。 Dom Storage 是通过存储字符串的 Key/Value 对来提供的,并提供 5MB (不同浏览器可能不同,分 HOST)的存储空间(Cookies 才 4KB)。另外 Dom Storage 存储的数据在本地,不像 Cookies,每次请求一次页面,Cookies 都会发送给服务器。 DOM Storage 分为 sessionStorage 和 localStorage。localStorage 对象和 sessionStorage 对象使用方法基本相同,它们的区别在于作用的范围不同。sessionStorage 用来存储与页面相关的数据,它在页面关闭后无法使用。而 localStorage 则持久存在,在页面关闭后也可以使用。 Dom Storage 提供了以下的存储接口:
<script type="text/javascript"> // 当页面刷新时,从sessionStorage恢复之前输入的内容 window.onload = function(){ if (window.sessionStorage) { var name = window.sessionStorage.getItem("name"); if (name != "" || name != null){ document.getElementById("name").value = name; } } };
// 将数据保存到sessionStorage对象中 function saveToStorage() { if (window.sessionStorage) { var name = document.getElementById("name").value; window.sessionStorage.setItem("name", name); window.location.href="session_storage.html"; } } </script>
<p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p> <p><img src="img_logo.gif" width="336" height="69"></p> <p>Try opening <a href="tryhtml5_html_manifest.htm" target="_blank">this page</a>, then go offline, and reload the page. The script and the image should still work.</p>
</body> </html>
1 2 3 4 5 6 7 8 9
上面 HTML 文档,引用外部一个 JS 文件和一个 GIF 图片文件,在其 HTML 头中通过 manifest 属性引用了一个 appcache 结尾的文件。 我们在 Google Chrome 浏览器中打开这个 HTML 链接,JS 功能正常,图片也显示正常。禁用网络,关闭浏览器重新打开这个链接,发现 JS 工作正常,图片也显示正常。当然也有可能是浏览缓存起的作用,我们可以在文件的浏览器缓存过期后,禁用网络再试,发现 HTML 页面也是正常的。 通过 Google Chrome 浏览器自带的工具,我们可以查看已经缓存的 AppCache(分 HOST)。  上面截图中的缓存,就是我们刚才打开 HTML 的页面 AppCache。从截图中看,HTML 页面及 HTML 引用的 JS、GIF 图像文件都被缓存了;另外 HTML 头中 manifest 属性引用的 appcache 文件也缓存了。 AppCache 的原理有两个关键点:manifest 属性和 manifest 文件。 HTML 在头中通过 manifest 属性引用 manifest 文件。manifest 文件,就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要缓存的文件。  上面截图中的 manifest 文件,就 HTML 代码引用的 manifest 文件。文件比较简单,第一行是关键字,第二、三行就是要缓存的文件路径(相对路径)。这只是最简单的 manifest 文件,完整的还包括其他关键字与内容。引用 manifest 文件的 HTML 和 manifest 文件中列出的要缓存的文件最终都会被浏览器缓存。 完整的 manifest 文件,包括三个 Section,类型 Windows 中 ini 配置文件的 Section,不过不要中括号。
* CACHE MANIFEST - Files listed under this header will be cached after they are downloaded for the first time * NETWORK - Files listed under this header require a connection to the server, and will never be cached * FALLBACK - Files listed under this header specifies fallback pages if a page is inaccessible
document.querySelector("#getByName").addEventListener("click", getPeopleByNameIndex1, false); } //DB打开失败回调 openRequest.onerror = function(e) { console.log("Error"); console.dir(e); } }else{ alert('Sorry! Your browser doesn\'t support the IndexedDB.'); }
//添加一条记录 function addPerson(e) { var name = document.querySelector("#name").value; var email = document.querySelector("#email").value; console.log("About to add "+name+"/"+email); var transaction = db.transaction(["people"],"readwrite"); var store = transaction.objectStore("people"); //Define a person var person = { name:name, email:email, created:new Date() } //Perform the add var request = store.add(person); //var request = store.put(person, 2); request.onerror = function(e) { console.log("Error",e.target.error.name); //some type of error handler } request.onsuccess = function(e) { console.log("Woot! Did it."); } } //通过KEY查询记录 function getPerson(e) { var key = document.querySelector("#key").value; if(key === "" || isNaN(key)) return; var transaction = db.transaction(["people"],"readonly"); var store = transaction.objectStore("people"); var request = store.get(Number(key)); request.onsuccess = function(e) { var result = e.target.result; console.dir(result); if(result) { var s = "<p><h2>Key "+key+"</h2></p>"; for(var field in result) { s+= field+"="+result[field]+"<br/>"; } document.querySelector("#status").innerHTML = s; } else { document.querySelector("#status").innerHTML = "<h2>No match!</h2>"; } } } //获取所有记录 function getPeople(e) { var s = ""; db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) { var cursor = e.target.result; if(cursor) { s += "<p><h2>Key "+cursor.key+"</h2></p>"; for(var field in cursor.value) { s+= field+"="+cursor.value[field]+"<br/>"; } s+="</p>"; cursor.continue(); } document.querySelector("#status2").innerHTML = s; } } //通过索引查询记录 function getPeopleByNameIndex(e) { var name = document.querySelector("#name1").value;
var transaction = db.transaction(["people"],"readonly"); var store = transaction.objectStore("people"); var index = store.index("name");
//name is some value var request = index.get(name);
request.onsuccess = function(e) { var result = e.target.result; if(result) { var s = "<p><h2>Name "+name+"</h2><p>"; for(var field in result) { s+= field+"="+result[field]+"<br/>"; } s+="</p>"; } else { document.querySelector("#status3").innerHTML = "<h2>No match!</h2>"; } } } //通过索引查询记录 function getPeopleByNameIndex1(e) { var s = ""; var name = document.querySelector("#name1").value; var transaction = db.transaction(["people"],"readonly"); var store = transaction.objectStore("people"); var index = store.index("name"); //name is some value index.openCursor().onsuccess = function(e) { var cursor = e.target.result; if(cursor) { s += "<p><h2>Key "+cursor.key+"</h2></p>"; for(var field in cursor.value) { s+= field+"="+cursor.value[field]+"<br/>"; } s+="</p>"; cursor.continue(); } document.querySelector("#status3").innerHTML = s; } } </script> <p>添加数据<br/> <input type="text" id="name" placeholder="Name"><br/> <input type="email" id="email" placeholder="Email"><br/> <button id="addButton">Add Data</button> </p> <p>根据Key查询数据<br/> <input type="text" id="key" placeholder="Key"><br/> <button id="getButton">Get Data</button> </p> <div id="status" name="status"></div> <p>获取所有数据<br/> <button id="getAllButton">Get EveryOne</button> </p> <div id="status2" name="status2"></div> <p>根据索引:Name查询数据<br/> <input type="text" id="name1" placeholder="Name"><br/> <button id="getByName">Get ByName</button> </p> <div id="status3" name="status3"></div>
1 2 3
将上面的代码复制到 indexed_db.html 中,用 Google Chrome 浏览器打开,就可以添加、查询数据。在 Chrome 的开发者工具中,能查看创建的 DB 、存储对象(可理解成表)以及表中添加的数据。  IndexedDB 有个非常强大的功能,就是 index(索引)。它可对 Value 对象中任何属性生成索引,然后可以基于索引进行 Value 对象的快速查询。 要生成索引或支持索引查询数据,需求在首次生成存储对象时,调用接口生成属性的索引。可以同时对对象的多个不同属性创建索引。如下面代码就对name 和 email 两个属性都生成了索引。
1 2 3 4
var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true }); //first arg is name of index, second is the path (col); objectStore.createIndex("name","name", {unique:false}); objectStore.createIndex("email","email", {unique:true});
function getPeopleByNameIndex(e){ var name = document.querySelector("#name1").value; var transaction = db.transaction(["people"],"readonly"); var store = transaction.objectStore("people"); var index = store.index("name"); //name is some value var request = index.get(name); request.onsuccess = function(e) { var result = e.target.result; if(result) { var s = "<p><h2>Name "+name+"</h2><p>"; for(var field in result) { s+= field+"="+result[field]+"<br/>"; } s+="</p>"; } else { document.querySelector("#status3").innerHTML = "<h2>No match!</h2>"; } } }
1 2 3 4 5 6 7 8 9 10 11
分析:IndexedDB 是一种灵活且功能强大的数据存储机制,它集合了 Dom Storage 和 Web SQL Database 的优点,用于存储大块或复杂结构的数据,提供更大的存储空间,使用起来也比较简单。可以作为 Web SQL Database 的替代。不太适合静态文件的缓存。
* 以key-value 的方式存取对象,可以是任何类型值或对象,包括二进制。 * 可以对对象任何属性生成索引,方便查询。 * 较大的存储空间,默认推荐250MB(分 HOST),比 Dom Storage 的5MB 要大的多。 * 通过数据库的事务(tranction)机制进行数据操作,保证数据一致性。 * 异步的 API 调用,避免造成等待而影响体验。
File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。 File System API 也是一种可选的缓存机制,和前面的 SQLDatabase、IndexedDB 和 AppCache 等一样。File System API 有自己的一些特定的优势:
* 可以满足大块的二进制数据( large binary blobs)存储需求。 * 可以通过预加载资源文件来提高性能。 * 可以直接编辑文件。
浏览器给虚拟文件系统提供了两种类型的存储空间:临时的和持久性的。临时的存储空间是由浏览器自动分配的,但可能被浏览器回收;持久性的存储空间需要显示的申请,申请时浏览器会给用户一提示,需要用户进行确认。持久性的存储空间是 WebApp 自己管理,浏览器不会回收,也不会清除内容。持久性的存储空间大小是通过配额来管理的,首次申请时会一个初始的配额,配额用完需要再次申请。 虚拟的文件系统是运行在沙盒中。不同 WebApp 的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是互相隔离的。 File System API 提供了一组文件与文件夹的操作接口,有同步和异步两个版本,可满足不同的使用场景。下面通过一个文件创建、读、写的例子,演示下简单的功能与用法。
<script type="text/javascript"> window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; //请求临时文件的存储空间 if (window.requestFileSystem) { window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler); }else{ alert('Sorry! Your browser doesn\'t support the FileSystem API'); } //请求成功回调 function initFS(fs){ //在根目录下打开log.txt文件,如果不存在就创建 //fs就是成功返回的文件系统对象,fs.root代表根目录 fs.root.getFile('log.txt', {create: true}, function(fileEntry) { //fileEntry是返回的一个文件对象,代表打开的文件 //向文件写入指定内容 writeFile(fileEntry); //将写入的内容又读出来,显示在页面上 readFile(fileEntry); }, errorHandler); } //读取文件内容 function readFile(fileEntry) { console.log('readFile');
// Get a File object representing the file, // then use FileReader to read its contents. fileEntry.file(function(file) { console.log('createReader'); var reader = new FileReader(); reader.onloadend = function(e) { console.log('onloadend'); var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); }; reader.readAsText(file); }, errorHandler); }
//向文件写入指定内容 function writeFile(fileEntry) { console.log('writeFile'); // Create a FileWriter object for our FileEntry (log.txt). fileEntry.createWriter(function(fileWriter) { console.log('createWriter'); fileWriter.onwriteend = function(e) { console.log('Write completed'); }; fileWriter.onerror = function(e) { console.log('Write failed: ' + e.toString()); }; // Create a new Blob and write it to log.txt. var blob = new Blob(['Hello, World!'], {type: 'text/plain'}); fileWriter.write(blob); }, errorHandler); } function errorHandler(err){ var msg = 'An error occured: ' + err; console.log(msg); }; </script>
```
将上面代码复制到 file_system_api.html 文件中,用 Google Chrome 浏览器打开(现在 File System API 只有 Chrome 43+、Opera 32+ 以及 Chrome for Android 46+ 这三个浏览器支持)。由于 Google Chrome 禁用了本地 HTML 文件中的 File System API功能,在启动 Chrome 时,要加上”—allow-file-access-from-files“命令行参数。 上面截图,左边是 HTML 运行的结果,右边是 Chrome 开发者工具中看到的 Web 的文件系统。基本上 H5的几种缓存机制的数据都能在这个开发者工具看到,非常方便。 分析:File System API 给 Web App 带来了文件系统的功能,Native 文件系统的功能在 Web App 中都有相应的实现。任何需要通过文件来管理数据,或通过文件系统进行数据管理的场景都比较适合。 到目前,Android 系统的 Webview 还不支持 File System API。
3 移动端 Web 加载性能(缓存)优化
分析完 H5提供的各种缓存机制,回到移动端(针对 Android,可能也适用于 iOS)的场景。现在 Android App(包括手 Q 和 WX)大多嵌入了 Webview 的组件(系统 Webview 或 QQ 游览器的 X5组件),通过内嵌Webview 来加载一些H5的运营活动页面或资讯页。这样可充分发挥Web前端的优势:快速开发、发布,灵活上下线。但 Webview 也有一些不可忽视的问题,比较突出的就是加载相对较慢,会相对消耗较多流量。 通过对一些 H5页面进行调试及抓包发现,每次加载一个 H5页面,都会有较多的请求。除了 HTML 主 URL 自身的请求外,HTML外部引用的 JS、CSS、字体文件、图片都是一个独立的 HTTP 请求,每一个请求都串行的(可能有连接复用)。这么多请求串起来,再加上浏览器解析、渲染的时间,Web 整体的加载时间变得较长;请求文件越多,消耗的流量也会越多。我们可综合使用上面说到几种缓存机制,来帮助我们优化 Web 的加载性能。 结论:综合各种缓存机制比较,对于静态文件,如 JS、CSS、字体、图片等,适合通过浏览器缓存机制来进行缓存,通过缓存文件可大幅提升 Web 的加载速度,且节省流量。但也有一些不足:缓存文件需要首次加载后才会产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。要解决这些不足,可以参考手 Q 的离线包,它有效的解决了这些不足。 对于 Web 在本地或服务器获取的数据,可以通过 Dom Storage 和 IndexedDB 进行缓存。也在一定程度上减少和 Server 的交互,提高加载速度,同时节省流量。 当然 Web 的性能优化,还包括选择合适的图片大小,避免 JS 和 CSS 造成的阻塞等。这就需要 Web 前端的同事根据一些规范和一些调试工具进行优化了。