今天解决的最有成就感的一个Web前端问题
IE诡异的行为经常让人捉摸不透。这次遇到的IE问题也很诡异,虽然没有“透”,但捉摸出了解决方法,而且是在几乎无望的情形下捉摸出来的,所以说很有成就感。那“最”从何来呢?在解决过的Web前端问题中,这次是“最”让人兴奋的。
这个问题简单来说就是一个Javascript跨域的问题。那复杂来说呢,请看下面的文字。
<script src="http://common.cnblogs.com/editor/tiny_mce/tiny_mce.js"></script>
博客园博客后台(域名是www.cnblogs.com)用的是TinyMCE编辑器,TinyMCE的js文件的引用是通过 common.cnblogs.com 这个域名(两个好处:1. 实现不同应用重用TinyMCE;2. 可以针对这个域名进行CDN加速)【注:这里虽然涉及两个域名,如果仅仅是js文件引用,不存在跨域问题】。
然后点击“上传图片”按钮,弹出上传图片的窗口(通过一个TinyMCE插件实现的),窗口打开的是 upload.cnblogs.com 这个域名【注:这时也不存在跨域问题】。
然后进行图片上传操作,上传成功之后返回图片地址,并将这个图片地址写入到编辑器文本框中(调用tinyMCEPopup.editor.execCommand),然后关闭这个窗口(调用tinyMCEPopup.close),为了调用这两个TinyMCE函数,必须在上传页面(upload.cnblogs.com)中引用 util.cnblogs.com 域名下的tiny_mce_popup.js文件。【注:跨域问题登场了】为了支持跨域操作,必须要在tiny_mce_popup.js文件开头添加如下的代码:
以前我们就是这样解决跨域问题的。
但这次使用了ajax上传组件Fine Uploader(https://github.com/valums/file-uploader)之后,Chrome, Firefox, IE10标准模式下都没问题,但在IE10的“IE9标准模式”下(估计IE9及以下版本都存在这个问题),上传图片之后,在IE10 develop tools的控制台中却出现这样的错误(图片地址也就无法写入编辑器中):
Error when attempting to access iframe during handling of upload response (Error: Access is denied.)
Error when attempting to access iframe during handling of upload response (Error: Access is denied.)
大部分时间都花在找出这个错误的来源上。。。
后来试着在tiny_mce_popup.js中将 document.domain = 'cnblogs.com'; 注释掉,错误就消失了。进一步测试,发现只要在Fine Uploader发起请求之前(注意这里的“请求之前”)有 document.domain = 'cnblogs.com'; 这样的代码,就会出现这个错误。
也就是说在IE9及IE9版本以下(未实际验证,只在IE10的“IE9标准模式”下验证过),Fine Uploader与 document.domain = 'cnblogs.com'; 水火不容。但是不在tiny_mce_popup.js中加 document.domain = 'cnblogs.com'; ,就无法将图片地址添加到TinyMCE文本框(tinyMCEPopup.editor.execCommand)中并关闭图片上传窗口(tinyMCEPopup.close)。而错误信息恰恰就是来自这两个操作【这两个操作是跨域操作,不设置document.domain就无法跨域操作】。
Fine Uploader不允许跨域,而TinyMCE又必须要跨域才能添加内容,本文开头的“几乎无望”说的就是这个时候。。。此处省略解决问题过程中最难熬的阶段
然后,突然想到:tinyMCEPopup.editor.execCommand与tinyMCEPopup.close是在图片上传完成之后fineUploader.complete回调函数中执行的,如果在这里动态加载tiny_mce_popup.js是不是可以解决问题呢?
于是,改为下面的代码(upload.cnblogs.com域名下的上传页面,关键代码是$.getScript):
$('#jquery-wrapped-fine-uploader').fineUploader({}) .on('complete', function (event, id, fileName, responseJSON) { if (responseJSON.success) { $.getScript("http://common.cnblogs.com/editor/tiny_mce/tiny_mce_popup.js", function () { tinyMCEPopup.editor.execCommand('mceInsertContent', false, responseJSON.message); tinyMCEPopup.close(); }); } });
结果得到的是不同的错误信息:
SCRIPT5: Access is denied.
SCRIPT5007: Unable to get property 'execCommand' of undefined or null
对于这个错误,当时没有进行仔细分析,只是猜测可能是$.getScript的跨域问题(有经验的朋友帮忙分析一下)。接着,将tiny_mce_popup.js文件复制到upload.cnblogs.com域名下,改为在同一个域名下加载tiny_mce_popup.js,代码如下:
$('#jquery-wrapped-fine-uploader').fineUploader({}) .on('complete', function (event, id, fileName, responseJSON) { if (responseJSON.success) { $.getScript("/scripts/tiny_mce_popup.js", function () { tinyMCEPopup.editor.execCommand('mceInsertContent', false, responseJSON.message); tinyMCEPopup.close(); }); } });
改为这个代码后,问题竟奇迹般地解决了。(当然,tiny_mce_popup.js中的 document.domain = 'cnblogs.com'; 不能少)
写了这么多,不知道没有把问题表述清楚,跨来跨去,人都被跨晕,何况IE呢?可是为什么Chrome, Firefox没有晕?
更新
写了这篇博客之后,一直在思考有没有更好的解决方法。
把问题精简一下:
1. 在tiny_mce_popup.js中,初始化tinyMCEPopup需要设置document.domain;
2. 使用fineUploader时不能设置document.domain,但在fineUploader上传完成之后的回调函数(complete)中可以设置;
3. 初始化tinyMCEPopup的目的就是为了tinyMCEPopup.editor.execCommand与tinyMCEPopup.close的执行。
目前采用的$.getScript方法,是通过异步的方式加载tiny_mce_popup.js(这样加载有成本),在tiny_mce_popup.js中设置document.domain,初始化tinyMCEPopup,这样做效率不是很高。
效率更高的做法应该是正常引用tiny_mce_popup.js(通过<script>标签),在fineUploader的回调函数(complete)中设置document.domain,初始化tinyMCEPopup。
看了一下tiny_mce_popup.js的代码,的确可以把初始化tinyMCEPopup的操作移出来。于是,有了更好的解决方法:
1. 在tiny_mce_popup.js中移除设置document.domain的代码。
2. 在tiny_mce_popup.js中移除tinyMCEPopup.init();【注:该代码在文件最后的位置】
3. 改为如下的代码:
<script src="/scripts/tiny_mce_popup.js"></script> <script src="/scripts/jquery.fineuploader-3.1.min.js"></script> <div id="jquery-wrapped-fine-uploader"></div> <div id="message">只能上传单张10M以下的 png、jpg、gif 格式的图片</div> <script> $(function () { $('#jquery-wrapped-fine-uploader').fineUploader({}). on('complete', function (event, id, fileName, responseJSON) { if (responseJSON.success) { document.domain = 'cnblogs.com'; tinyMCEPopup.init(); tinyMCEPopup.editor.execCommand('mceInsertContent', false, '<p><img src="' + responseJSON.message + '" alt=""/>'); tinyMCEPopup.close(); } }); }); </script>
这再次体现了写博客的作用:帮助自己理清思路,进行更深入的思考。