四个小时的SWFUpload debug之旅
由于项目需要,这两天研究了一下SWFUpload,一个JS和AS 结合实现的上传“功能包”,简单来说原理就是利用flash的FileReference类在客户端完成文件信息的过滤,包括类型、大小,以及上传的实时 进度信息,然后利用externalInterface来跟客户端的JS通信,提供了一系列属性、方法和事件触发时的回调函数接口,将UI的主动权完全由 FLASH转交给了JS来操作。(PS:目前版本还没有Release,有些功能和文档还不全,疑惑之处可直接翻代码看看。等我项目结束时希望 SWFUpload放出正式版和官方确定的接口文档了,到时再针对传统文件上传的各种方式和SWFUpload功能包唠叨几句。)这里只唠叨一下目前这个 版本存在的一个BUG,在IE6内核的多标签浏览器中(例如GreenBrowser和Maxthon),一旦页面刷新过后,SWFUpload的功能就不正常了,或许你也正为这个发愁了,可以先跳到这里察看临时解决方案。
最初用SWFUpload做demo的时候就发现了目前存在一个小BUG:我机器上的浏览器环境是GreenBrowser(IE6内核),打开上传页 面,一切功能都正常,此时如果刷新或者强刷一下页面,那么当点击文件浏览按钮时(触发selectFile事件),debug信息中就会提示“Could not find Flash element”,而在FF、IE7下面功能很正常,更奇怪的是在我随后的debug过程中我发现在单独的IE6,也就是系统提供的那个单窗口的IE6下 居然功能也很正常。后来又在ie6内核下测试了maxthon同样存在和GreenBrowser一样的问题,难道这个跟多标签浏览器也有关系?第一反应 是我的demo有问题。于是去SWFUpload的官方demo验证了下,发现居然也存在这个问题。哎~又是IE惹的祸。开始Debug之旅吧。
一、首先用DOM察看器在IE下看看刷新前后DOM元素中flash元素的变化
有了FireBug以后,让IE下的几个调试工具显得太简陋了,不过至少我还可以直接察看到DOM元素的变化。奇怪的是刷新前后,FLASH元素都完整地出现在DOM中,那为何会提示“Could not find Flash element”呢?
二、 跟踪文件浏览的点击事件selectFile
到SWFUpload库中能看到扩展了一个selectFile的方法。
SWFUpload.prototype.selectFile = function () {
var movie_element = this.getMovieElement();
if (movie_element !== null && typeof(movie_element.SelectFile) === "function") {
try {
movie_element.SelectFile();
}
catch (ex) {
this.debug("Could not call SelectFile: " + ex);
}
} else {
this.debug("Could not find Flash element");
}
};
难道“Could not find Flash element”是这里输出来的?于是将这个语句给注释掉再测试,果然没有看到“没有找到flash元素”的debug信息了,这就确定了是这个方法里出 现了异常,仔细看了下这个if判断,这里不但对flash元素的存在性做了判断,还需要判断flash元素里是否存在SelectFile方法。个人觉得 这个双保险检测是没有错,但这个debug信息未免就太笼统了,typeof(movie_element.SelectFile) === "function" 如果是false那也不能说明是flash元素不存在。之前我总结flash和js通信的时候也做过externalInterface的demo, 猜测一下这里的实现原理:SelectFile方法是externalInterface类用addCallback方法在 flash中将一个名为SelectFile的方法注册到容器中可供JS调用,也就是现在看到的这种形式 movie_element.SelectFile()。那么理论上如果flash元素存在,那么 typeof(movie_element.SelectFile) === "function"肯定是true,可是在IE6这个总是很另类但又是用户占有率最高的浏览器里这个理论还真就出问题了,下面可以来证明一下。
三、观察浏览器刷新前后movie_element.SelectFile的变化
重新打开一个新页面测试SWFUpload的官方demo,测试文件浏览功能,正常。于是在地址栏输入
javascript:alert(document.getElementById("SWFUpload_0").SelectFile);void 0;
(PS:IE下的调试方法实在是太简陋了,还好广大开发者的智慧是无穷的。)
这
里的SWFUpload_0是SWFUpload生成的flash元素时指定的id,这个可以从dom节点中看到。如果你想更详细地确认其中细节可以看
SWFUpload中的initSWFUpload方法,事实上这里会有一个实例队列,因为这个demo中就是一个实例,所以这个flash的id在队列
中是SWFUpload_0。回车执行上面的JS,正常情况下你会看到如下信息:
function () { return eval(instance.CallFunction("<invoke name=\""+name+"\" returntype=\"javascript\">" + __flash__argumentsToXML(arguments,0) + "</invoke>")); }
从这段代码的特征上也验证我之前的猜测,SelectFile确实是flash中注册的一个方法。
然
后刷新此页面,再到地址栏输入回车,HOHO你会得到undefined信息。SWFUpload的开发者可能也没有想到理论上很保险的做法到IE6下会
有如此怪异的行为。Flash还存在,为什么其中addCallback注册的方法会“消失”呢?难道是externalInterface类本身有问
题?于是又把自己以前的一个demo拿出来测试,发现刷新前后功能依然很正常,匪夷所思。仔细对比了下两个demo的差异,最后将疑点锁定在了flash的写入方式,SWFUpload是用JS将flash写入到dom中的,我自己的demo中flash元素是直接写死在HTML代码里的。
四、测试不同的flash写入方式对addCallback方法的影响
AW说SWFUpload中写入flash的方法是从SWFObject库里抽出来的,这个SWFObject的官方说明也证实了这点。打开SWFUpload库确实也可以看到SWFObject的几个核心方法。那这就确定了SWFUpload写入flash和SWFObject原理上一样的,于是采用SWFobject对我之前的demo做测试。
1、首先用最原始的方法将flash静态写入到页面中(察看实例)
关于静态插入flash到页面的不同方式的细节以及他们对JS造成的影响随后再写个详细的东东,这里先不做具体探讨,暂且使用最常用的object和embed混合的方式。
第一个按钮是js调用as中注册的say方法;
第二个按钮是测试flash元素是否在页面中,如果在页面中,那么打印出他的innerHTML属性;
第三个按钮是测试flash元素中addCallBack注册的方法是否还存在,如果存在那么将其打印出来;
当页面第一次打开的时候分别点击三个按钮,callback注册的say方法呼叫正常,flash元素存在,其中注册的say方法也存在。
下面将页面刷新一次再测试,测试结论和刚才一样,都很正常。这就确保了我目前采用的flash中addCallBack注册的say方法功能是正常的。
2、利用SWFObject写入flash到页面中(察看实例)
同样测试刷新前后的变化,刷新前功能正常,而将页面刷新一次再测试,就跟我之前描述的bug一样,怪异现象出现了,callback注册的say方法呼叫失败了,flash元素是存在的,但其中注册的say方法也不见了。
到
这步为止已经找到办法解决项目中遇到的SWFUpload的刷新BUG了,将SWFUpload中写入flash的方法变成静态写入即可,事实上当时考虑
项目进度我确实是这么做的,嘿嘿,尽管方法太笨。由于SWFUpload实例有很多设置需要配置到FlashVars中,所以做这个静态写入时最好先将
SWFupload的getFlashHTML方法返回值打印出来(PS:需要针对IE和之外的浏览器,实际上就是Object和embed两种不同
flash的插入方式做一个判断输出,切忌不能将他们同时写入页面,否则会造成SWFUpload无法正确找到实例的flash元素。)
上面的解决方案只是曲线救国,考虑到以后项目中的灵活使用和维护,还是需要把其中的问题找出来,再回到我们的Debug进程来。现在已经证明问题出在 flash的写入方式上。刨开SWFObject的代码能够很清晰看出它的原理:根据实例化时的最基本参数配置和属性、flashvars设置方法来针对 object和embed两种不同的插入方法构造出两个不同的flashDom节点。当write方法调用时,将此节点写入到目标容器的 innerHTML中。难道是innerHTML在作怪?
五、跟踪innerHTML写入flash节点
还是针对上面的demo做测试,将SWFObject调用write前构造出来的flash节点字符串和write之后浏览器里flash节点的html代码分别打印出来比较。
IE6:
Write调用前:
<object
id="demo" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
width="300" height="100" style="undefined"><param name="movie"
value="demo.swf" /><param name="bgcolor" value="#fff"
/><param name="quality" value="high" /></object>
Write调用后:
<OBJECT
id=demo height=100 width=300
classid=clsid:D27CDB6E-AE6D-11cf-96B8-444553540000><PARAM
NAME="_cx" VALUE="7938"><PARAM NAME="_cy"
VALUE="2646"><PARAM NAME="FlashVars" VALUE=""><PARAM
NAME="Movie" VALUE="demo.swf"><PARAM NAME="Src"
VALUE="demo.swf"><PARAM NAME="WMode" VALUE="Window"><PARAM
NAME="Play" VALUE="0"><PARAM NAME="Loop" VALUE="-1"><PARAM
NAME="Quality" VALUE="High"><PARAM NAME="SAlign"
VALUE=""><PARAM NAME="Menu" VALUE="-1"><PARAM NAME="Base"
VALUE=""><PARAM NAME="AllowScriptAccess" VALUE=""><PARAM
NAME="Scale" VALUE="ShowAll"><PARAM NAME="DeviceFont"
VALUE="0"><PARAM NAME="EmbedMovie" VALUE="0"><PARAM
NAME="BGColor" VALUE=""><PARAM NAME="SWRemote"
VALUE=""><PARAM NAME="MovieData" VALUE=""><PARAM
NAME="SeamlessTabbing" VALUE="1"><PARAM NAME="Profile"
VALUE="0"><PARAM NAME="ProfileAddress" VALUE=""><PARAM
NAME="ProfilePort" VALUE="0"><PARAM NAME="AllowNetworking"
VALUE="all"><PARAM NAME="AllowFullScreen"
VALUE="false"></OBJECT>
显然,IE浏览器对object便签做了很多处理,innerHTML强大
功能的背后到底隐藏了什么?起初我怀疑是innerHTML有缓存机制,因此刷新的时候造成了bug,为了验证对着这个猜测,我给写入的flash字符串
中添加了一个时间戳来“刷新缓存”,BUG依然存在,排除了这个缓存猜测。
如果换作其他的动态写入是否也有这个BUG呢?于是想到了document.write,在这个实例中,
针对object和embed我做了单独输出,测试发现document.write写入前后的flash节点改变跟innerHTML前后的改变是一致
的,但刷新BUG没有出现。难道这个BUG真的是innerHTML的内部实现造成的,如果真是这样那麻烦就大了,总不能指望微软现在来改IE6吧。
六、只有google一下innerHTML的内部机制了
首先google了一下innerHTML和externalInterface,想看看是否也有人遇到这个问题了。直接G到一篇SWFObject论坛上的一个讨论,
对于Geoff在这里提到了几种造成externalInterface通信失败的可能性,我的实例中都没有出现,可是BUG依然存在。不过这则讨论中的
一个细节引起了我注意,Geoff说他们在开发中一直都在使用externalInterface和js通信,从来没有出现过bug,我也仔细看了他给出
的demo,这里是AS调Js的方法。难道innerHTM只对addCallBack有负作用,call是正常的?于是我又做了一个demo,这真是一个让我很无语的结论,原来call方法真的是有效的。也就是说innerHTML只对addCallBack有刷新bug。随后google到一个 flex写的externalInterface通信的例子更让我觉得无语。我把这个flex写的swf文件保存到了本地,并调用了其中addCallBack注册的方法,察看实例,
“灵异事件”发生了,无论我怎么刷新页面,此前的bug都没有出现。一下让我将怀疑对象从innerHTML转移到了AS2中的
externalInterface类本身。不过这里还有一个疑问就是为何只在IE6内核的多标签浏览器下才有此BUG呢?个人觉得可能性较大的原因是
AS2中的externalInterface类确实还不完善,对于多标签浏览器中更复杂的window对象的兼容做得还不够好。而flex中采用的
externalInterface机制可能更完善了。
下班回来从7:30到11:30,整整四个小时的Debug最终结论居然出是因为AS2的内部实现有bug,在SWFUpload官方更改自己AS内部实现前,看来暂时只有先采用曲线救国的方式了:
一、之前所提到的实例生成时,直接打印出SWFUpload中getFlashHTML方法针对object和embed的不同输出,然后将这两段代码分别针对不同浏览器做硬编码输出,切忌这两段代码不能共存。
二、禁止掉SWFUpload中loadFlash,然后扩展一个方法,利用document.write在指定dom位置调用,动态写入flash元素。目前项目中我已经这样做了,比第一种方法的扩展性要高点。
―――――――――――――――――――――――――――――――――――――――
PS:Debug是三号下班在家做的,一直没有时间整理出来,今天拼了。HOHO