一直以来客户端文件操作在浏览器中由于安全原因而很少涉及,之前主要有判断文件大小以及缩略图等应用。
基本应用:
?
1.判断文件大小
?? ie 中可以使用 activeX (Scripting.FileSystemObject) 来获取输入框选择文件的大小,但是由于受安全设置而并不实用。其他标准浏览器可以使用 html file api 提供的接口直接获取文件大小:
?
<input id='f' type='file'/> alert(document.getElementById('f').files[0].size);
?
2.缩略图预览
?
?? ie < 7 中的 img src 可以直接设置本地地址,进行预览。
?
<img src='c:/test.jpg'/>
?
? ?这时还可以在 img onload 后通过 img.fileSize 来获得图片的大小.
?
?? ie 7,8,9 提升了安全,禁掉了img的本地地址,但对于滤镜 而没有限制:
?
<DIV ID="oDiv" STYLE="position:relative; height:250px; width:250px; filter:progid:DXImageTransform.Microsoft.AlphaImageLoader( src='c:/workshop/graphics/earglobe.gif', sizingMethod='scale');" > </DIV>
?
但这时就无法通过 img 标签获取图片的大小了.
?
?
PS : ie8,9 有 fakepath 现象 : 默认在非信任域有个设置项设严格了:
?
?
导致不能直接取 file input 的值了,需要使用 range 迂回:
?
<DIV ID="oDiv" STYLE="position:relative; height:250px; width:250px; filter:progid:DXImageTransform.Microsoft.AlphaImageLoader( src='d:/pic/', sizingMethod='scale');" > </DIV> <input type='file' onchange="ok(this);"> function ok(self){ self.select(); var path=document.selection.createRange().text; document.getElementById("oDiv").style.filter= "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+path+"', sizingMethod='scale');"; }?
?
?
chrome, firefox 等标准浏览器,结合 datauri 以及 filereader api ,可以直接读取文件的 datauri 数据设置给 img 标签即可:
?
<input id='f' type='file'/> //注意:chrome本地协议不行,必须http://xx/x.html var r=new FileReader(); var file=document.getElementById('f').files[0], img=document.getElementById('img1'); r.onload=function(ev){ img.src = ev.target.result; }; r.readAsDataURL(file);
?
?
PS: chrome本地直接打开不行!wierd 。IE9 不支持 FileReader,还好是继续支持滤镜,网上流传的 ff 特有 getAsDataURL 做法已经无参考必要了 ,现在可以全平台支持无上传客户端预览了!
?
上传预览 demo @ googlecode
?
已知问题:
?
1. ie fakepath 情况下可能会出现 createRange() 获取真实地址时 ie 抛出拒绝访问(access denied)错误,目前无法解决。
?
2. safari 没有 filereader 接口,无法进行预览.
?
综合解决方案
?
和后端配合,如果 ie 下路径包含 c:\fakepath\ ?或 safari 下则先通过无刷新文件上传到服务器返回服务器代理地址预览.
?
?
更复杂的应用:
?
gmail 以及 google doc 都支持在 firefox 以及 chrome 下从桌面直接拖动照片到指定区域实现上传功能,特别是在编辑区域中可以拖动图片到任意位置,达到了和本地程序相似的用户体验,减少操作步鄹。这次也尝试下实现这个功能。
?
ps: gmail 问题:
?
gmail 和 google doc 不是同一套编辑系统,closure editor 弱了不少,比如不支持 firefox 在编辑区域内拖放上传:
?
?
?
chrome 虽然支持在编辑区域拖放上传,但是插入位置不对,并不是鼠标drop位置,下面会说道:在 chrome 下这个精确位置的确很难取到!
?
?
?
规范:
?
涉及3方面的规范
?
1. drag and drop : 可以阻止浏览器的默认行为以及获得用户从桌面拖放而来的文件句柄。
?
2. fileapi : 对用户主动选择文件的操作 api
?
3. XMLHttpRequest2 : 传输二进制数据而不是字符串。
?
?
实现:
?
1.阻止系统默认行为
?
?? 阻止拖动区域的相关拖动事件,防止浏览器直接打开拖放文件,更进一步可以设置拖放鼠标图标等:
?
Event.on(document, "dragenter dragover", function(ev) { ev.halt(); }); Event.on(document, "drop", function(ev) { ev.halt(); });
?
具体可参考 Drag Operations@MDC (非常难用的api,除了文件拖放上传其他地方还是用类库模拟 的好)
?
注意:
?
1. chrome 只要 dragover preventDefault 就可以触发drop 事件,并且阻止浏览器默认事件(打开该文件).
?
2. firefox 必须 dragover preventDefault 并且 stopPropagation 才能触发 drop 事件,而进一步 drop preventDefault 并且 stopPropagation 才能阻止浏览器默认事件(打开该文件).
?
2.获取插入点
?
? firefox 中当拖放图片到可编辑区域(iframe,而不是contentEditable=true的元素)时会自动插入生成的img标签,地址为图片的本地地址
?
<img _moz_dirty='' src='c:/xx.jpg' />?
?则上传后只要替换该元素即可,需要注意的是必须异步在 drop 事件触发后获取该元素,在 drop 事件处理函数中同步取不到刚刚自动插入的元素。
?
其实只要记下该插入元素的父亲及下一个兄弟节点即可,然后替换插入元素为 loading 图标。
?
if (UA.gecko) { S.all("img", document.body).each(function(el) { if (el[0].hasAttribute("_moz_dirty")) { archor = el[0].nextSibling; ap = el[0].parentNode; el.remove(); } }); }
updated 2010-12-26 :
?
最好使用 DOMNodeInserted 事件,在拖放中监听:
?
Event.on(document, "DOMNodeInserted", nodeInsert);
?
记录是否插入的是本地文件,避免干扰页面内拖放功能:
?
function nodeInsert(ev) { var oe = ev.originalEvent; var t = oe.target; if (S.DOM._4e_name(t) == "img" && t.src.match(/^file:\/\//)) { inserted[t.src] = t; } }
?
drop 时去除自动插入的图片,并找出插入点:
?
/** * firefox 会自动添加节点 */ if (!S.isEmptyObject(inserted)) { S.each(inserted, function(el) { if (S.DOM._4e_name(el) == "img") { archor = el.nextSibling; ap = el.parentNode; S.DOM._4e_remove(el); } }); inserted = {}; }?
?
?
chrome 中不会自动插入img 元素,并且拖放过程中编辑光标并不会随之变化,因此选择区 range 也不会随拖动而变化,那么只有利用拖动时的鼠标位置信息,利用 elementfrompoint 来获得鼠标所处元素,但是由于文本并不是元素,那么不可避免会没有 firefox 下那么精确,google doc 则是完全自主实现光标定位,难度过大。
?
//空行里拖放肯定没问题,其他在文字中间可能不准确 ap = document.elementFromPoint(ev.clientX, ev.clientY); archor = ap.lastChild;?
?
然后就可以根据定位信息来插入loading图片,下一步就是上传本地图片到服务器从而替换元素为服务器端图片地址。
?
var img = new Node("<img " + "src='" + "loading.gif" + "'" + "/>"); ap.insertBefore(img, archor);
?
?
拖放定位 demo
?
3.文件 xhr 上传
?
一般所说的文件异步上传是指通过提交 form 到 iframe 并监听状态来实现,而通过拖放上传的话由于不涉及 form,则不能使用以上方法,不过标准浏览器实现了 XMLHttpRequest2 ,可以和服务器端进行二进制传输,手工构建 multipart/form-data 格式的 post 信息,就能实现文件上传了。
?
主要用到:
?
filereader : 可以通过异步读取用户选择文件的二进制信息(raw data)
?
var reader = new FileReader(); reader.onload = function(ev) { //文件原始数据字符串,高8位为0 var fileData = ev.target.result; }; reader.readAsBinaryString(file);
?由于 javascript 字符为16位,则在表达单字节二进制的字符中,高8位为0,另外chrome不支持 addEventLister 来监听 load 事件。
?
构建 multipart/form-data 格式的提交信息:完全依据 rfc2388 (ietf ),声明任意边界字符串 boundary,区别对待文件以及普通表单数据:
?
//文件数据 var body = "\r\n--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + fileInput + "\"; filename=\"" + encodeURIComponent(fileName) + "\"\r\n"; body += "Content-Type: " + (file.type || "application/octet-stream") + "\r\n\r\n"; //文件原始数据 body += fileData + "\r\n"; //普通表单域数据 for (var p in serverParams) { if (serverParams.hasOwnProperty(p)) { body += "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + p + "\"\r\n\r\n"; body += serverParams[p] + "\r\n"; } } body += "--" + boundary + "--";?
设置请求的 content-type
?
xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary=" + boundary);?
最后通过 firefox 的私有api:sendAsBinary,直接发送二进制数据
?
xhr.sendAsBinary("Content-Type: multipart/form-data; boundary=" + boundary + "\r\nContent-Length: " + body.length + "\r\n" + body + "\r\n");?
需要注意的是 sendAsBinary 为 firefox 私有方法,不过对于 chrome ,可以通过构建 blob数据 ,通过标准的 send 来达到同样的效果:
?
if (!XMLHttpRequest.prototype.sendAsBinary) { XMLHttpRequest.prototype.sendAsBinary = function(datastr, contentType) { var bb = new BlobBuilder(); var len = datastr.length; var data = new Uint8Array(len); for (var i = 0; i < len; i++) { data[i] = datastr.charCodeAt(i); } bb.append(data.buffer); this.send(bb.getBlob(contentType)); } }?
?
当 xhr 返回时,将服务器图片地址替换掉loading图标即可:
?
xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200 || xhr.status == 304) { if (xhr.responseText != "") { var info = S.JSON.parse(xhr.responseText); img.src = info.imgUrl; } } } };
?
对于后端程序来说,该请求和直接form提交没有任何区别.
?
refer :
?
使用input来达到指定区域拖放上传:
http://www.thecssninja.com/javascript/gmail-upload
?
拖放的简明应用介绍:
http://html5doctor.com/native-drag-and-drop/
?
拖放上传的权威例子以及相关api
https://developer.mozilla.org/en/using_files_from_web_applications
?
https://developer.mozilla.org/En/DragDrop/Drag_Operations
https://developer.mozilla.org/En/DragDrop/DataTransfer
https://developer.mozilla.org/en/DOM/FileReader
?
?
一个成熟的跨平台拖放上传组件
http://uploader.rickylab.co.cc/
?
The File API has changed
Drag and drop file uploading using JavaScript
?
?
?
?
?
?
?
呵呵,就是传统的
自底向上
从实现到抽象
从实践到理论
getAsDataURL()只有ff支持吧?webkit和opera都不行吧,是不是还有要什么条件?
以下测试代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title> new document </title> <SCRIPT LANGUAGE="JavaScript"> <!-- function fn(){ var file=document.getElementById('f').files[0], img=document.getElementById('img1'); img.src = file.getAsDataURL(); } //--> </SCRIPT> </head> <body> <img id="img1"/> <input id='f' type='file'/> <input type="button" value="test" id="cc" onclick="fn()"/> </body> </html>
getAsDataURL()只有ff支持吧?webkit和opera都不行吧,是不是还有要什么条件?
以下测试代码:
不好意思,没仔细测,这个搞错了,已修正,应该是 filereader 的 api
<html> <head> <title> new document </title> <SCRIPT> function fn(){ //注意:chrome本地协议不行,必须http://xx/x.html var r=new FileReader(); var file=document.getElementById('f').files[0], img=document.getElementById('img1'); r.onload=function(ev){ img.src = ev.target.result; }; r.readAsDataURL(file); } </SCRIPT> </head> <body> <img id="img1"/> <input id='f' type='file'/> <input type="button" value="test" id="cc" onclick="fn()"/> </body> </html>