能否执行异步跨域文件上传?

46

#可以做到!请往下看。


首先,让我用这张图解释一下如何实现异步文件上传


抱歉,我关闭了其中一个域名,现在这张图片已经不存在了。不过这是在我发现 Stack Overflow 可以通过 Imgur 上传图片之前的事情。


正如您所看到的,诀窍在于让 HTTP 响应加载到一个隐藏的 IFRAME 元素中,而不是页面本身。(这是通过在提交 FORM 表单时使用 JavaScript 设置 FORM 元素的 target 属性来实现的。)

这是行得通的。然而,我面临的问题是,服务器端脚本在不同的域上。FORM 提交是跨域 HTTP 请求。现在,服务器端脚本已启用 CORS,这使得我的网页有权读取从该脚本到我的页面发送的 HTTP 请求的响应数据——但这仅在我通过 AJAX(即 JavaScript)接收 HTTP 响应时才有效。

然而,在这种情况下,响应是指向 IFRAME 元素的。一旦 XML 响应落入 IFRAME,它的 URL 就会成为远程脚本的 URL,例如:http://remote-domain.example/script.pl

不幸的是,CORS 并不涉及这种情况(至少我认为是这样)——我无法读取 IFRAME 的内容,因为它的 URL 不匹配页面的 URL(域名不同)。我会收到以下错误:

Unsafe JavaScript attempt to access frame with URL hxxp://remote-domain.example/script.pl from frame with URL hxxp://example.com/outer.html. Domains, protocols and ports must match.

由于 IFRAME 的内容是一个 XML 文档,因此其中没有 JavaScript 代码可以利用 postMessage 或其他东西。

因此,我的问题是: 我如何从 IFRAME 中获取 XML 内容?

如我所言,我能够直接检索跨域HTTP响应(启用CORS),但似乎在它们加载到IFRAME后无法读取跨域HTTP响应。

如果这个问题还不够难解决,那么就让我排除这些解决方案

  1. easyXDM和类似技术需要远程域上的终端点。

  2. 更改XML响应(以包括SCRIPT元素)。

  3. 服务器端代理 - 我明白我可以在我的域上有一个服务器端脚本作为代理。

因此,除了这两个解决方案之外,还能做到吗?


#它是可以做到的!

事实证明,可以伪造XHR请求(Ajax请求),模拟multipart/form-data表单提交(用于将文件上传到服务器,如上图所示)。

诀窍是使用FormData构造函数 - 请阅读这篇Mozilla Hacks文章获取更多信息。

方法如下:

// STEP 1
// retrieve a reference to the file
// <input type="file"> elements have a "files" property
var file = input.files[0];

// STEP 2
// create a FormData instance, and append the file to it
var fd = new FormData();
fd.append('file', file);

// STEP 3
// send the FormData instance with the XHR object
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://remote-domain.example/script.pl', true);
xhr.onreadystatechange = responseHandler;
xhr.send(fd);

以上方法执行异步文件上传,与上述图像中描述的常规文件上传相同,并通过提交此表单实现:

<form action="http://remote-domain.example/script.pl"
        enctype="multipart/form-data" method="post">
    <input type="file" name="file">
</form>

#像个老板 :)


@Thomas 我不在乎旧浏览器 - 实际上,即使它只在一个浏览器中工作,我也很满意 :)。你能详细解释一下吗?我担心服务器脚本需要一个 <form enctype="multipart/form-data">,我不确定是否可以用 JavaScript 创建这样的东西... - Šime Vidas
@Balus 嗯,我可以联系远程域的管理员并请求一些修改,但我这个问题的明确意图是要弄清楚是否可以在不修改远程域上的任何文件或设置的情况下完成。 - Šime Vidas
1
@Šime,编辑后的解决方案是否适用于所有主流浏览器?偶尔我需要使用 AJAX 文件上传。 - 700 Software
1
很抱歉,George。FormData 构造函数在 Firefox、Chrome 和 Safari 中实现了,但是 Opera 和 IE 并没有。 - Šime Vidas
这还能用吗? - Anthony
显示剩余6条评论
4个回答

9
只需使用表单中的数据发送跨域XHR请求,而不是提交表单。CORS仅适用于前者。
如果您非要采用另一种方式,请使用postMessage与框架进行协商。
由于IFRAME的内容是XML文档,因此IFRAME内部没有JavaScript代码可以使用postMessage或其他东西。
那么这如何阻止您呢?只需在XML中的HTML或SVG命名空间下包含一个脚本元素(<script xmlns="http://www.w3.org/1999/xhtml" type="application/ecmascript" src="..."/>)即可。

1
请点击这里阅读相关编程内容:http://jquery.malsup.com/form/#file-upload。使用“XMLHttpRequest”对象无法上传文件,这就是为什么首先引入了这个隐藏的IFRAME-hack。至于在XML响应中包含SCRIPT元素的想法很好,但不幸的是,我的要求是远程域名不能以任何方式被修改(我没有提到这一点)。因此,easyXDM不可行,修改XML响应也不行。我的问题的意图是弄清楚是否可以在不修改远程域或使用代理的情况下完成此操作。 - Šime Vidas
2
不,可以通过 XHR 上传“文件”(只是“Blob”)。所有当前的浏览器和 IE10 都支持使用 W3C File API(http://www.w3.org/TR/FileAPI/) 进行文件上传 - 只需要执行 xhr.send(file_input.files[0]) 即可。 - Eli Grey
我认为我们必须等待5年,然后才能放弃iframe作为备用方案。 - BalusC
@Eli,你的评论引导我找到了我回答中描述的FormData解决方案。在这里,获得一些声望:) - Šime Vidas

0

我认为你描述的方式无法实现。通常,如果您遇到跨域问题,可以通过JSONp方法解决,但这仅适用于GET请求。使用HTML5,您可能会尝试使用GET请求发送二进制数据,但这只是有风险的。

  • 一种解决方案是通过在本地Web服务器上代理请求来使远程Web服务本地可用。这将导致本地Web服务器的额外负载,因此我可以想象这是不可行的。但如果文件很小且不频繁,这个方法还是不错的。

  • 另一种解决方案是在发送文件后开始轮询服务器。您可以发送一个令牌并使用常规JSONp轮询服务器的状态。这样,您就不需要从iframe中读取。

  • 将整个页面放入在远程服务器上运行的iframe中。这可能只是转移了问题,但如果XML输出是某个过程的最终步骤,那么这是完全可行的。

我相信您有让处理服务器位于不同域的充分理由,但如果不是这样,您就不会遇到所有这些问题。也许重新考虑一下值得吧?


#1明确被问题排除。如果另一方的代码库完全受控,则可能存在#2和#3。根据OP在Mic的答案中的评论,显然不是这种情况。 - BalusC

-1

以下方法在我的设置中有效(Firefox 3.6):

<!-- hidden target frame -->
<iframe name="load_target" id="load_target" onload="process(this);" src="#" ...>

<!-- get data from iframe after load and process them --> 
<script type="text/javascript">
    function process(iframe) {
       var data = iframe.contentWindow.document.body.innerHTML; 
       // got test data="<xml><a>b</a></xml>"
    }
</script>

它在Chrome中也可以工作,但需要在父页面加载后排除第一个onload调用。这可以通过设置一个“全局”变量并在process()中进行测试来轻松实现。

补充

该方法与表单一起使用。

<form action="URL" method="post" enctype="multipart/form-data" target="load_target">

该内容被提交到URL。这个URL需要与父页面page.html位于同一个域中。如果要下载来自REMOTE_URL的数据,则URL将是自己域上的PHP代理proxy.php

<?php echo file_get_contents("REMOTE_URL"); ?>

这是一种简单的方法 - 然而,它可能被问题的条件(2)所排除。我在这里添加它是为了使我的答案完整。

其他方法仅考虑iframes,由MahemoffGeorges Auberger讨论。


你误解了重点。使用JavaScript无法读取跨域IFRAME内容。你的代码只有在包含IFRAME的页面和IFRAME内页面的域名相同的情况下才能正常工作。将 http://google.com 加载到IFRAME中并尝试你的代码——它不会起作用。 - Šime Vidas
@Šime:感谢您的评论。我扩展了我的答案,以使其完整并适用于“远程”页面。我知道这种方法可能被您的问题排除在外。 - Jiri Kriz
代理被明确禁止在此问题中使用。 - BalusC
@BalusC:我在我的答案和评论中都提到了这一点。 - Jiri Kriz

-1
如果可以的话,返回一个 HTML 页面而不是 XML。
在该页面中,您可以在 SCRIPT 标记中使用命令: parent.postMessage

如果您必须支持旧浏览器(< IE8 主要),您可以编写并读取window.name以传递小于 2Mb 的消息。

这两种技术都可以让您在不同域的框架之间传递字符串数据。

另一种技术是使用 setInterval 定期从父页面调用远程域,使用 JSONP 来了解状态。

无论哪种情况,您都需要远程域的合作才能获取数据。


很遗憾,我无法以任何方式修改服务器端脚本。 - Šime Vidas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接