如何解决Asp.Net Ajax 1.0跨域名框架情况下javascript“访问拒绝”的问题[翻译]
How to work around the access denied cross-domain frame issue in ASP.NET Ajax 1.0
正好解决了我的问题,觉得作者分析的不错,于是以我憋足的英语水平把它翻译了,希望对学习Asp.Net Ajax的朋友用的上。
译文如下:
如何解决Asp.Net Ajax 1.0跨域名框架情况下javascript“访问拒绝”的问题
一些用户可能已经遇到将Asp.net Ajax程序放在和顶级窗口域名不一致的Frame或者Iframe中使用的情况。在IE浏览器中浏览这些网页时,任何在框架中触发的客户端DOM事件(比如鼠标单击),IE将抛出“Access denied”(访问拒绝)的异常。造成这种情况的原因是MicrosoftAjax.js中的Sys.UI.getLocation方法。其中有一段棘手的代码,来获取一个DOM元素相对于页面左上角坐标的像素值。所以Body下面的绝对位置的子元素使用这些坐标,从而可以准确的代替你测量的元素。这个方法在鼠标拖拽,弹出菜单的场景,比如自动完成。可以通过它获取鼠标相对于触发鼠标事件元素的坐标。由于我们不能动态的根据浏览器的兼容性来决定不同浏览器的各自的行为,一次在Sys.UI.getLocation中的代码正对不同浏览器做了特定的改写。你仅仅需要知道,例如某浏览器不计算一个元素的scroll位置,当这个元素是Body的直接的子元素并且是绝对位置时。这就是我们需要解决的一种问题。幸运的是,IE有两个方便的方法让我们来完全绕过一些我们不能有效解决的Bug。getClientRects:获取元素占据页面的所有矩形区域。getBoundingClientRect:返回一个包含整个元素的矩形区域。在我们使用的方法里,我们已经使用了getClientRects方法,并且获取到第一个矩形区域。因为我们需要一致化不同浏览器间的行为,即使对于一个包装元素Span。这种情况下,元素的左上角就是包含该元素的第一个矩形区域的左上角,而不同于包含该元素的全局的矩形区域。如下图:
这就是我们所犯的错误,不幸的是已经太晚了。getClientRects和getBoundingClientRect之间仅有一点点区别。区别在于,getClientRects在框架下使用时,给出的坐标包含框架在顶级窗口中的偏移值(offset 常常元素的offsetLeft offsetTop等属性来描述)。然而getBoundingClientRect 直接给出不包括偏移量的坐标。两个方法都需要包括计算Frameborder的高宽才能称得上完美和准确。纠正getClientRects我们不得不去关注框架相对于顶级窗口的坐标,并且减去它们,然而这种操作在跨域名的框架下是被禁止的。
解决办法就是使用getBoundingClientRect来代替getClientRects,虽然在使用包装元素(如span)情况下,这个办法将在不同浏览器之间带来一些不一致,但是总比彻底的失败要好得多。新版本的函数依然需要使用try/catch来修正frameboder的问题,所以跨域名框架的情况下,坐标可能会有2个像素的偏差,但是这已经是最好的结果了。
解决问题的步骤
首先,需要用外部的脚本文件来代替编译在Dll中基于资源的脚本。通过设置ScriptManager的ScriptPath可以实现。外部的脚本文件包可以在Microsoft Ajax Library ( http://ajax.asp.net/downloads/library/default.aspx?tabid=47&subtabid=471)找到,它是基于MSPL,你可以修改其中脚本文件的内容。将Microsoft Ajax Library解压缩后,拷贝文件夹System.Web.Extensions到ScriptPath指定的位置。如果你不希望所有脚本基于路径被引用,你可以只指定核心脚本文件MicrosoftAjax.js通过路径引用,其他的脚本文件继续使用Web Resources的方式使用。这样在使用其他基于资源的库时要容易些,比如使用toolkit的时候。将下列脚本加入到你的的Script Manager就可以轻松实现:
当然,不要忘记将[Your Script Directory]替换成为你Web程序中对应的路径。如果采用这种脚本引用方式,不能在Script Manager中在设置ScriptPath了。完成上述步骤后,你可以检查程序是否能继续正常工作,并且使用网络监视工具比如Fidder来从新的路径装载脚本。
第二步就是要修复原来脚本中的Bug了。我们需要修复脚本的debug版本(MicrosoftAjax.debug.js)和发布版本。
在MicrosoftAjax.debug.js找到以下代码片断
case Sys.Browser.InternetExplorer:
并且用下面的代码替换介于“case Sys.Browser.Safari:”之间的所有代码
if (element.self || element.nodeType === 9) return new Sys.UI.Point(0,0);
var clientRect = element.getBoundingClientRect();
if (!clientRect) {
return new Sys.UI.Point(0,0);
}
var ownerDocument = element.document.documentElement;
var offsetX = clientRect.left - 2 + ownerDocument.scrollLeft,
offsetY = clientRect.top - 2 + ownerDocument.scrollTop;
try {
var f = element.ownerDocument.parentWindow.frameElement || null;
if (f) {
var offset = 2 - (f.frameBorder || 1) * 2;
offsetX += offset;
offsetY += offset;
}
}
catch(ex) {
}
return new Sys.UI.Point(offsetX, offsetY);
}
break;
对于发布版本(MicrosoftAjax.js),步骤基本相同,除了文件有点难于操作以外(程序被搞成好长的几行,要选中不太容易)。找到代码片断
"switch(Sys.Browser.agent){case Sys.Browser.InternetExplorer:”。并且用下面的代码替换介于“case Sys.Browser.Safari:”之间的所有代码
switch(Sys.Browser.agent){case Sys.Browser.InternetExplorer:Sys.UI.DomElement.getLocation=function(a){if(a.self||a.nodeType===9)return new Sys.UI.Point(0,0);var b=a.getBoundingClientRect();if(!b)return new Sys.UI.Point(0,0);var c=a.document.documentElement,d=b.left-2+c.scrollLeft,e=b.top-2+c.scrollTop;try{var g=a.ownerDocument.parentWindow.frameElement||null;if(g){var f=2-(g.frameBorder||1)*2;d+=f;e+=f}}catch(h){}return new Sys.UI.Point(d,e)};break;
这个时候网站应该不会再抛出异常了
这个修正带来的已知后果
- Sys.UI.DomElement.getLocation方法返回的坐标,在不同域名框架场景下将偏移2像素
- 这个修正执行结果返回元素边界的左上角坐标,代替了第一个包含当前元素的矩形区域的左上角,这样对于包装元素(span)是不同的。在不同浏览器下返回值也不一致。
重要免责声明
这项修正意味着你要停止使用基于资源的脚本,而是用静态文件版本代替。我希望这个问题下次服务包发布时得到解决。所以当System.Web.Extensions发布新版本时,你将需要恢复到使用基于资源的脚本使用方式,从而获得其他问题的修正或者更新。