由于项目需要,这两天研究了一下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>