本文概述
HTML 5中的文件系统API与Web Workers API是两个非常重要的API。其中文件系统API的作用是为Web应用程序提供一个具有阶层目录结构的文件系统,能够在该文件系统中创建及删除具有阶层结构的目录及文件,能够读写文件中的内容。而Web Workers API的作用是使Web应用程序具有创建后台线程的能力,能够在Web应用程序中使用多线程处理技术。然而,如果将这两者结合起来,你可以创建一个具有高级特性的Web应用程序。
本文介绍如何在后台线程中使用HTML 5中的文件系统API。本文的阅读要求读者实现掌握文件系统API与Web Workers API,如果你对这两个API还不是非常了解,可以参阅笔者所著《HTML 5与CSS 3权威指南》,或点击此处报名参加我们所办的面向企业(可赴企业现场培训)或面向个人的培训班,为了保证学员真正掌握所学知识,参加培训后一年内,凡学员上机时遇到所学课程内的各种问题,可在本站“技术论坛”栏目内提出后由本站专门回答。
同步与异步模式
异步模式的文件系统API比同步模式的文件系统API更加复杂一些,且其中的内容也更加多一些。异步模式的文件系统API的一个缺点就是出错机率较高,所以有时我们可能需要在后台线程中使用同步模式的文件系统API。
同步模式的文件系统API中的大部分属性与方法与异步模式的文件系统API中的属性与方法相同,主要区别在于:
- 同步模式的文件系统API只能被使用在后台线程中,而异步模式的文件系统API只能被使用在页面主线程中。
- 同步模式的文件系统API中不使用回调函数,各方法均直接返回值。
- 异步模式的文件系统API中的window对象的全局方法(requestFileSystem方法与resolveLocalFileSystemURL方法)在同步模式的文件系统API中分别为worker对象的requestFileSystemSync方法与resolveLocalFileSystemSyncURL方法。
除此之外,同步模式的文件系统API与异步模式的文件系统API大致相同。
请求获取文件系统
在同步模式的文件系统API中,可以在后台线程中通过使用requestFileSystemSync方法来请求获取LocalFileSystemSync对象的方法来访问文件系统,代码如下所示:
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
请注意,在requestFileSystemSync方法中,我们没有指定获取文件系统成功与失败时所执行的回调函数(在同步模式的文件系统API中,没有回调函数的概念)。
在后台线程所用脚本文件中,通过如下所示的方法来获取文件系统。
self.requestFileSystemSync = self.webkitRequestFileSystemSync || self.requestFileSystemSync;
处理磁盘限额
目前为止,不能在后台线程中请求获取永久磁盘限额。因此我们建议在主线程中请求获取永久磁盘限额,步骤如下所示。
- 在后台线程用脚本文件中,通过try/catch机制来捕捉任何使用同步模式的文件系统API的代码中抛出的错误,包括QUOTA_EXCEED_ERR(磁盘配额超限)错误。
- 在后台线程用脚本文件中,当捕捉到QUOTA_EXCEED_ERR错误后,向主线程发送“请求获取磁盘限额”消息(消息文字可自定义)。
- 在主线程中,当接收到"请求获取磁盘限额"消息后,使用window.webkitStorageInfo.requestQuota方法向用户申请磁盘配额。
- 在主线程中,当用户给予磁盘限额后,向后台线程发送“磁盘限额已获取”消息(消息文字可自定义),通知后台线程申请磁盘限额成功。可以在后台线程中继续执行之前执行失败的处理。
处理文件与目录
同步版本的getFile方法与getDirectory方法分别返回FileEntrySync对象与DirectoryEntrySync对象。
例如,在如下所示的代码中,在文件系统的根目录下创建了一个空的log.txt文件。
var fileEntry = fs.root.getFile('log.txt', {create: true});
在如下所示的代码中,在文件系统的根目录下创建了一个新的mydir目录。
var dirEntry = fs.root.getDirectory('mydir', {create: true});
处理错误
在同步模式的文件系统API中,由于缺乏引发错误时所执行的回调函数,所以增加了错误处理的难度。因此我们推荐使用try/catch机制来捕捉文件系统API中所抛出的任何错误,可以在捕捉错误后将其作为消息发送给主线程,代码如下所示:
function onError(e) { postMessage('错误: ' + e.toString()); } try { //如果log.txt文件已存在则抛出错误 var fileEntry = fs.root.getFile('log.txt', {create: true, exclusive: true}); } catch (e) { onError(e); }
传递File对象、Blob对象与ArrayBuffer对象
在HTML 5的标准Web Workers API中,只允许通过postMessage方法传递字符串消息。后来,浏览器对其进行扩展,允许传递序列化数据,即JSON对象。然而,最近,有些浏览器(例如Chrome浏览器)中开始允许通过postMessage方法传递File对象、Blob对象、ArrayBuffer对象、类型数组等二进制数据。
在如下所示的代码中,主线程向后台线程传递几个(用户通过文件选取控件选择的)文件,在后台线程中简单地将接收到的数据返还给主线程,主线程接收到后台线程返回的消息后逐个读取消息中所包含的所有用户选取文件中的内容。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>向后台线程传递FileList对象</title> <script type="javascript/worker" id="fileListWorker"> self.onmessage = function(e) { postMessage(e.data); //直接返回 }; </script> </head> <body> </body> <input type="file" id="file1" multiple></input><br/> <output id="result"></output> <script> document.getElementById("file1").addEventListener('change', function(e) { document.getElementById("result").innerHTML=""; var files = this.files; loadInlineWorker('#fileListWorker', function(worker) { //定义接收到后台线程返回消息时执行的函数 worker.onmessage = function(e) { //异步读取文件为ArrayBuffer for (var i = 0, file; file = files[i]; ++i) { var reader = new FileReader(); reader.onload = function(e) { document.getElementById("result").innerHTML+=this.result+ "<br/><br/><br/>"; }; reader.onerror = function(e) { alert('读取文件出错'); }; reader.readAsText(file); } }; worker.postMessage(files); }); }, false); function loadInlineWorker(selector, callback) { window.URL = window.URL || window.webkitURL || null; window.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.BlobBuilder; var script = document.querySelector(selector); if (script.type === 'javascript/worker') { var bb = new BlobBuilder(); bb.append(script.textContent); callback(new Worker(window.URL.createObjectURL(bb.getBlob()))); } } </script> </html>
在后台线程中读取文件
在后台线程中,可以使用同步模式的FileReader API(使用FileReaderSync对象)来读取文件。代码如下所示。
前台页面文件中的代码:
<!DOCTYPE html> <html> <head> <title>FileReaderSync对象使用示例</title> </head> <body> <input type="file" id="file1" multiple /><br/> <output id="result"></output> <script> var worker = new Worker('worker.js'); worker.onmessage = function(e) { document.getElementById("result").innerHTML+=e.data+"<br/><br/><br/>" }; worker.onerror = function(e) { document.getElementById("result").innerHTML=e.filename+"文件中抛出错误,行号为"+e.lineno+ ",错误信息为:"+e.message; }; document.getElementById("file1").addEventListener('change', function(e) { worker.postMessage(this.files); }, false); </script> </body> </html>
worker.js脚本文件中的代码:
self.addEventListener('message', function(e) { var files = e.data; var contents = []; [].forEach.call(files, function(file) { var reader = new FileReaderSync(); contents.push(reader.readAsText(file)); }); postMessage(contents); }, false);
与同步模式的文件系统API相同,在同步模式的FileReader API中,没有回调函数的概念。另外,FileReaderSync对象的几种用于读取文件的方法均直接返回被读取的文件内容(当使用readAsArrayBuffer方法时返回ArrayBuffer对象)。
代码示例:读取文件系统根目录下的所有子目录与文件
因为同步模式的文件相关API中没有回调函数的概念,所以在执行某些任务时,在后台线程中使用同步模式的文件系统API可以使代码变得更加简单易读。
在HTM 5中,主线程与后台线程永远不会共享某一个对象。当使用postMessage方法传递消息时,被传递的消息中的对象永远是当前线程中该对象的复制品。因此,不是所有类型的对象均能被传递。
不幸的是,目前为止,不能在消息中传递FileEntrySync对象与DirectoryEntrySync对象。那么,怎样将从文件系统中读取到的文件或目录传递给主线程呢?一种解决方法是传递filesystem:URLS的集合,而不是传递文件对象或目录对象的集合。filesystem:URLS的值为字符串,所以可以被传递。另外,在主线程中,可以通过resolveLocalFileSystemURL方法将filesystem:URLS字符串解析为FileEntrySync(代表文件)对象或DirectoryEntrySync(代表目录)对象。
前台页面中的代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>使用同步模式的文件系统API读取文件与目录</title> </head> <body> <output id="output"></output> <script> window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL; var worker = new Worker('worker.js'); worker.onmessage = function(e) { var urls = e.data.entries; urls.forEach(function(url, i) { window.resolveLocalFileSystemURL(url, function(fileEntry) { //显示所有文件名 document.getElementById("output").innerHTML+=fileEntry.name+"<br/>"; }); }); }; worker.postMessage({'cmd': 'list'}); </script> </body> </html>
worker.js脚本文件中的代码:
self.requestFileSystemSync = self.webkitRequestFileSystemSync || self.requestFileSystemSync; var paths = []; //filesystem:URLS字符串的集合 function getAllEntries(dirReader) { var entries = dirReader.readEntries(); for (var i = 0, entry; entry = entries[i]; ++i) { paths.push(entry.toURL()); //将所有文件或目录转换为URL字符串后保存 //如果是目录,则获取该目录下所有子目录与文件 if (entry.isDirectory) { getAllEntries(entry.createReader()); } } } function onError(e) { postMessage('错误: ' + e.toString()); //将错误传递给主线程. } self.onmessage = function(e) { var data = e.data; //只处理list命令 if (!data.cmd || data.cmd != 'list') { return; } try { var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/); fs.root.getFile('1.txt', { create: true }); fs.root.getFile('2.txt', { create: true }); getAllEntries(fs.root.createReader()); self.postMessage({entries: paths}); } catch (e) { onError(e); } };
代码示例:使用XHR2下载文件
在后台线程中可以执行的一个常见处理是使用XHR2对象下载服务器端的某个文件,并将其书写在文件系统中。
在下面这个示例中,我们下载服务器端的一个图像文件,并将其书写在文件系统中,然后读取文件系统中的该图像文件并将其转换为filesystem:URLS字符串,然后将该字符串指定为页面上某个img元素的src属性值,使该图像被显示在页面上。
前台页面代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>在后台线程中下载服务器端图片文件并将其保存在文件系统中</title> </head> <body> <output id="result"></output> <img id="img"></img> <script> var worker = new Worker('downloader.js'); worker.onmessage = function(e) { if(e.data.substring(0,10)=="filesystem") img.src=e.data; else document.getElementById("result").innerHTML+=e.data+"<br/>"; }; worker.postMessage({fileName: 'tyl',url: 'tyl.jpg', type: 'image/jpeg'}); </script> </body> </html>
downloader.js脚本文件中的代码
self.requestFileSystemSync = self.webkitRequestFileSystemSync || self.requestFileSystemSync; self.BlobBuilder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder; function makeRequest(url) { try { var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); //注意:同步模式 xhr.responseType = 'arraybuffer'; xhr.send(); return xhr.response; } catch(e) { return "XHR 错误 " + e.toString(); } } function onError(e) { postMessage('错误: ' + e.toString()); } onmessage = function(e) { var data = e.data; //确保参数正确 if (!data.fileName || !data.url || !data.type) { return; } try { var fs = requestFileSystemSync(TEMPORARY, 1024 * 1024 /*1MB*/); postMessage('获取文件系统'); var fileEntry = fs.root.getFile(data.fileName, {create: true}); postMessage('获取文件'); var bb = new BlobBuilder(); bb.append(makeRequest(data.url)); //在BlobBuilder对象中追加获取到的文件的ArrayBuffer对象 try { postMessage('开始写文件'); fileEntry.createWriter().write(bb.getBlob(data.type)); postMessage('写文件完毕'); postMessage(fileEntry.toURL()); } catch (e) { onError(e); } } catch (e) { onError(e); } };