常见的语言比如php、shell等,是如何读取文件的呢?
实际上,大多数语言都需要先获取文件句柄,然后调用文件访问接口,打开文件句柄,读取文件!
那么,HTML5是否也是这样的呢?
答案是肯定的!
HTML5为我们提供了一种与本地文件系统交互的标准方式:File Api 。
该规范主要定义了以下数据结构:
HTML5访问本地文件系统时,需要先获取File
对象句柄,怎么获取文件引用句柄呢?
选择文件 首先检测一下当前浏览器是否支持File Api
:
1 2 3 4 5 6 function isSupportFileApi ( ) { if (window .File && window .FileList && window .FileReader && window .Blob) { return true ; } return false ; }
运行此示例:
HTML5虽然可以让我们访问本地文件系统,但是js只能被动地读取,也就是说只有用户主动触发了文件读取行为,js才能访问到File
Api
,这通常发生在表单选择文件 或者拖拽文件 。
表单输入 表单提交文件是最常见的场景,用户选择文件后,触发了文件选择框的change事件,通过访问文件选择框元素的files
属性可以拿到选定的文件列表。
如果文件选择框指定了multiple,则一个文件选择框可以同时选择多个文件,files
包含了所有选择的文件对象;如果没有指定,则只能选择一个文件,files[0]
就是所选择的文件对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 function fileSelect1 (e ) { var files = this .files; for (var i = 0 , len = files.length; i < len; i++) { var f = files[i]; html.push( '<p>' , f.name + '(' + (f.type || "n/a" ) + ')' + ' - ' + f.size + 'bytes' , '</p>' ); } document .getElementById('list1' ).innerHTML = html.join('' ); } document .getElementById('file1' ).onchange = fileSelect1;
运行此示例:
拖拽 拖拽是另一种常见的文件访问场景,这种方式通过一个叫dataTransfer
的接口来获得拖拽的文件列表,更多关于dataTransfer 。
拖拽同样支持多选,用户可以拖拽多个文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function dropHandler (e ) { e.stopPropagation(); e.preventDefault(); var files = e.dataTransfer.files; for (var i = 0 , len = files.length; i < len; i++) { var f = files[i]; html.push( '<p>' , f.name + '(' + (f.type || "n/a" ) + ')' + ' - ' + f.size + 'bytes' , '</p>' ); } document .getElementById('list2' ).innerHTML = html.join('' ); } function dragOverHandler (e ) { e.stopPropagation(); e.preventDefault(); e.dataTransfer.dragEffect = 'copy' ; } var drag = document .getElementById('drag' );drag.addEventListener('drop' , dropHandler, false ); drag.addEventListener('dragover' , dragOverHandler, false );
运行此示例:
将文件拖到此处
PS: 拖拽有个特别需要注意的事情就是,阻止事件冒泡和事件默认行为,防止浏览器自动打开文件等行为,比如拖拽一个pdf,浏览器可能会打开pdf。
至此,我们知道,我们可以通过两种方式来获得文件句柄,那么如何读取文件内容呢?
读取文件 HTML5提供了一个叫FileReader
的接口,用于异步读取文件内容,它主要定义了以下几个方法:
readAsBinaryString(File|Blob)
readAsText(File|Blob [, encoding])
readAsDataURL(File|Blob)
readAsArrayBuffer(File|Blob)
FileReader
还提供以下事件:
onloadstart
onprogress
onload
onabort
onerror
onloadend
一旦调用了以上某个方法读取文件后,我们可以监听以上任何一个事件来获得进度、结果等。
预览本地图片 这里主要用到FileReader的readAsDataURL
方法,通过将图片数据读取成Data URI的方法,将图片展示出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function fileSelect2 (e ) { e = e || window .event; var files = this .files; var p = document .getElementById('preview2' ); for (var i = 0 , f; f = files[i]; i++) { var reader = new FileReader(); reader.onload = (function (file ) { return function (e ) { var span = document .createElement('span' ); span.innerHTML = '<img style="padding: 0 10px;" width="100" src="' + this .result +'" alt="' + file.name +'" />' ; p.insertBefore(span, null ); }; })(f); reader.readAsDataURL(f); } } document .getElementById('files2' ).addEventListener('change' , fileSelect2, false );
运行此示例:
调用FileReader的readAsDataURL接口时,浏览器将异步读取文件内容,通过给FileReader实例监听一个onload事件,数据加载完毕后,在onload事件处理中,通过reader的result属性即可获得文件内容。
预览文本文件 这里主要用到FileReader的readAsText
,对于诸如mimeType为text/plain、text/html等文件均认为是文本文件,即mineType为text开头都可以用这个方法来预览。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function fileSelect3 (e ) { e = e || window .event; var files = this .files; var p = document .getElementById('preview3' ); for (var i = 0 , f; f = files[i]; i++) { var reader = new FileReader(); reader.onload = (function (file ) { return function (e ) { var div = document .createElement('div' ); div.className = "text" div.innerHTML = encodeHTML(this .result); p.insertBefore(div, null ); }; })(f); reader.readAsText(f); } } document .getElementById('files3' ).addEventListener('change' , fileSelect3, false );
运行此示例:
PS:由于需要在页面上预览文本,所以则需要对文件中的html特殊字符进行实体编码,避免浏览器解析文件中的html代码。
监控读取进度 既然FileReader是异步读取文件内容,那么就应该可以监听它的读取进度。
事实上,FileReader的onloadstart以及onprogress等事件,可以用来监听FileReader的读取进度。
在onprogress的事件处理器中,有一个ProgressEvent对象,这个事件对象实际上继承了Event对象,提供了三个只读属性:
lengthComputable
loaded
total
通过以上几个属性,即可实时显示读取进度,不过需要注意的是,此处的进度条是针对单次读取的进度,即一次readAsBinaryString
等方法的读取进度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var input4 = document .getElementById('file4' );var bar = document .getElementById('progress-bar' );var progress = document .getElementById('progress' );function startHandler (e ) { bar.style.display = 'block' ; } function progressHandler (e ) { var percentLoaded = Math .round((e.loaded / e.total) * 100 ); if (percentLoaded < 100 ) { progress.style.width = percentLoaded + '%' ; progress.textContent = percentLoaded + '%' ; } } function loadHandler (e ) { progress.textContent = '100%' ; progress.style.width = '100%' ; } function fileSelect4 (e ) { var file = this .files[0 ]; if (!file) { alert('请选择文件!' ); return false ; } if (file.size > 500 * 1024 * 1024 ) { alert('文件太大,请选择500M以下文件,防止浏览器崩溃!' ); return false ; } progress.style.width = '0%' ; progress.textContent = '0%' ; var reader = new FileReader(); reader.onloadstart = startHandler; reader.onprogress = progressHandler; reader.onload = loadHandler; reader.readAsBinaryString(this .files[0 ]); } input4.onchange = fileSelect4;
运行此示例:
分割文件 有的时候,一次性将一个大文件读入内存,并不是一个很好的选择(如果文件太大,可能直接导致浏览器崩溃),上述的监听进度示例就有可能在文件太大的情况下崩溃。
更稳健的方法是分段读取!
分段读取文件 HTML5 File Api提供了一个slice
方法,允许分片读取文件内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function readBlob (start, end ) { var files = document .getElementById('file5' ).files; if (!files.length) { alert('请选择文件' ); return false ; } var file = files[0 ], start = parseInt (start, 10 ) || 0 , end = parseInt (end, 10 ) || (file.size - 1 ); var r = document .getElementById('range' ), c = document .getElementById('content' ); var reader = new FileReader(); reader.onloadend = function (e ) { if (this .readyState == FileReader.DONE) { c.textContent = this .result; r.textContent = "Read bytes: " + (start + 1 ) + " - " + (end + 1 ) + " of " + file.size + " bytes" ; } }; var blob; if (file.webkitSlice) { blob = file.webkitSlice(start, end + 1 ); } else if (file.mozSlice) { blob = file.mozSlice(start, end + 1 ); } else if (file.slice) { blob = file.slice(start, end + 1 ); } reader.readAsBinaryString(blob); }; document .getElementById('file5' ).onchange = function ( ) { readBlob(10 , 100 ); }
运行此示例:(读取10 ~ 100字节)
本例使用了FileReader的onloadend事件来检测读取成功与否,如果用onloadend则必须检测一下FileReader的readyState,因为read abort时也会触发onloadend事件,如果我们采用onload,则可以不用检测readyState。
分段读取进度 那分段读取一个大文件时,如何监控整个文件的读取进度呢?
这种情况下,因为我们调用了多次FileReader的文件读取方法,跟上文一次性把一个文件读到内存中的情况不大相同,不能用onprogress来监控。
我们可以监听onload事件,每次onload代表每个片段读取完毕,我们只需要在onload中计算已读取的百分比就可以了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 var bar2 = document .getElementById('progress-bar2' );var progress2 = document .getElementById('progress2' );var input6 = document .getElementById('file6' );var block = 1 * 1024 * 1024 ; var file;var fileLoaded;var fileSize;function readBlob2 ( ) { var blob; if (file.webkitSlice) { blob = file.webkitSlice(fileLoaded, fileLoaded + block + 1 ); } else if (file.mozSlice) { blob = file.mozSlice(fileLoaded, fileLoaded + block + 1 ); } else if (file.slice) { blob = file.slice(fileLoaded, fileLoaded + block + 1 ); } else { alert('不支持分段读取!' ); return false ; } reader.readAsBinaryString(blob); } function loadHandler2 (e ) { fileLoaded += e.total; var percent = fileLoaded / fileSize; if (percent < 1 ) { readBlob2(); } else { percent = 1 ; } percent = Math .ceil(percent * 100 ) + '%' ; progress2.innerHTML = percent; progress2.style.width = percent; } function fileSelect6 (e ) { file = this .files[0 ]; if (!file) { alert('文件不能为空!' ); return false ; } fileLoaded = 0 ; fileSize = file.size; bar2.style.display = 'block' ; readBlob2(); } var reader = new FileReader();reader.onload = loadHandler2; input6.onchange = fileSelect6
运行此示例:(提示:请选择一个1G以上文件)
注意事项 在chrome浏览器上测试时,如果直接以file://xxx这种形式访问demo,会出现FileReader读取不到内容的情况,表现为FileReader的result为空或者FileReader根本就没有去读取文件内容,FileReader各个事件没有触发;
这种情况我想应该是类似于chrome不允许添加本地cookie那样,chrome也不允许以file://xxx这种页面上的js代码访问文件内容;
解决办法很简单,只需要将测试文件放到一个web服务器上,以http://xxx形式访问即可。
参考文章