是否可以执行异步跨域文件上传?

Posted

技术标签:

【中文标题】是否可以执行异步跨域文件上传?【英文标题】:Is it possible to perform an asynchronous cross-domain file-upload? 【发布时间】:2011-10-06 19:33:14 【问题描述】:

有可能!请阅读下文。


首先我用这张图来说明异步文件上传是如何实现的:


对不起。我已经关闭了我的一个域,并且图像现在消失了。这是一个非常好的图像。这是在我发现 Stack Overflow 可以通过 Imgur 上传图片之前。


如您所见,诀窍是让 HTTP 响应加载到隐藏的 IFRAME 元素中,而不是页面本身。 (这是通过在使用 javascript 提交 FORM 时设置 FORM 元素的 target 属性来完成的。)

这行得通。但是,我面临的问题是服务器端脚本位于不同的域。 FORM-submit 是一个跨域 HTTP 请求。现在,服务器端脚本启用了 CORS,这使我的网页有权读取从我的页面向该脚本发出的 HTTP 请求的响应数据 - 但只有当我通过 Ajax 接收 HTTP 响应时才有效,因此,JavaScript。

但是,在这种情况下,响应指向 IFRAME 元素。一旦 XML 响应到达 IFRAME,它的 URL 将是删除脚本 - 例如http://remote-domain.com/script.pl.

不幸的是,CORS 没有涵盖这种情况(至少我认为) - 我无法读取 IFRAME 的内容,因为它的 URL 与页面的 URL 不匹配(不同的域)。我收到此错误:

不安全的 JavaScript 尝试使用 URL 访问框架 hxxp://remote-domain.com/script.pl 来自带有 URL 的框架 hxxp://my-domain.com/outer.html。域、协议和端口必须 匹配。

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

所以我的问题是:如何从 IFRAME 获取 XML 内容?

如上所述,我可以直接检索跨域 HTTP 响应(启用 CORS),但是一旦跨域 HTTP 响应加载到 IFRAME 中,我似乎无法读取它们。

好像这个问题还不够无解,让我排除这些解决方案

    easyXDM 和需要远程域上的端点的类似技术,

    更改 XML 响应(以包含 SCRIPT 元素),

    服务器端代理 - 我知道我的域上可以有一个服务器端脚本,可以用作代理。

那么,除了这两种解决方案,还能做到吗?


可以的!!

事实证明,可以伪造一个模仿multipart/form-data FORM 提交(上图中用于将文件上传到服务器)的 XHR 请求(Ajax 请求)。

诀窍是使用FormData 构造函数 - 阅读this Mozilla Hacks article 了解更多信息。

这就是你的做法:

// 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.com/script.pl', true);
xhr.onreadystatechange = responseHandler;
xhr.send(fd);

上述方法执行异步文件上传,相当于上图描述的常规文件上传,通过提交这个表单来实现:

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

像老板一样:)

【问题讨论】:

如果你不能编辑远程服务器的响应,那就不行。如果您可以编辑上传网站的来源,则可以使用 hashchange 或 postmessage 技巧。 如果您不太关心旧版浏览器,您可以使用更现代的上传方法,通过 JS 上传文件并通过 AJAX 发布。如果这听起来是个好主意,请告诉我,我会将其作为答案发布。 @Thomas 我不关心旧的浏览器——事实上我很好,即使它只在一个浏览器中工作:)。你能详细说明一下吗?恐怕服务器脚本需要&lt;form enctype="multipart/form-data"&gt;,我不确定我是否可以用 JavaScript 创建这样的东西...... @Šime,编辑的解决方案是否适用于所有主流浏览器?我可以偶尔使用 AJAX 文件上传。 @George 不,很遗憾没有。 FormData 构造函数在 Firefox、Chrome 和 Safari 中实现,但在 Opera 和 IE 中没有实现。 【参考方案1】:

只需使用表单中的数据发送跨域 XHR 请求,而不是提交表单。 CORS 仅适用于前者。

如果您必须以其他方式进行,请使用 postMessage 与框架协商。

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

这如何阻止你?在 XML 的任意位置包含 HTML 或 SVG 命名空间 (&lt;script xmlns="http://www.w3.org/1999/xhtml" type="application/ecmascript" src="..."/&gt;) 下的脚本元素。

【讨论】:

Read here。无法使用XMLHttpRequest 对象上传文件。这就是为什么首先引入这个隐藏的 IFRAME-hack 的原因。至于在 XML 响应中包含 SCRIPT 元素,这是个好主意。不幸的是,我的要求是不以任何方式修改远程域(我没有提到这一点)。所以,easyXDM 是不可能的,而且修改 XML 响应也是如此。我的问题的目的是弄清楚这是否可以在不修改远程域或使用代理的情况下完成。 不,可以通过 XHR 上传Files(只是Blobs)。当前所有浏览器和 IE10 都支持使用 W3C 文件 API w3.org/TR/FileAPI 上传文件 - 只需 xhr.send(file_input.files[0]) 我认为我们必须等待 5 年才能放弃 iframe 解决方案作为后备方案。 @Eli 您的评论使我找到了我的回答中描述的FormData 解决方案。在这里,抓住一些代表:)【参考方案2】:

我认为不能按照您描述的方式完成。通常,如果您有跨域问题,您可以通过 JSONp 方法解决它,但这仅适用于 GET 请求。使用 HTML5,您可能会使用 GET 请求发送二进制文件,但这只是不确定的。

一种解决方案是通过代理本地 Web 服务器上的请求来使远程 Web 服务在本地可用。这会给您的本地网络服务器带来额外的负载,所以我可以想象这是不可行的。但是,如果文件很小且不常见,则效果很好。

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

将整个页面放在远程服务器上运行的 iframe 中。这可能只是解决了问题,但如果 XML 输出是某个过程的最后一步,那么它是非常可行的。

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

【讨论】:

#1 被问题明确排除。 #2 和 #3 只有在对方的代码库完全受控的情况下才有可能。根据 OP 对 Mic 回答的评论,显然情况并非如此。【参考方案3】:

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

如果你必须支持旧的浏览器(window.name 2Mb以下的消息。

这两种技术都允许您在不同域的帧之间传递字符串数据。

另一种技术是使用setInterval,它将使用JSONP 从父页面重复调用远程域以了解状态。

无论如何,您都需要远程域的合作才能获取数据。

【讨论】:

很遗憾没有。我无法以任何方式修改服务器端脚本。【参考方案4】:

以下方法适用于我的设置(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) 排除在外。我在这里添加了它以使我的答案完整。

Mahemoff 和 Georges Auberger 讨论了其他方法,仅考虑 iframe。

【讨论】:

你没有抓住重点。无法使用 JavaScript 读取跨域 IFRAME 内容。仅当 IFRAME 中的页面和包含该 IFRAME 的页面具有相同域时,您的代码才有效。将http://google.com 加载到 IFRAME 并尝试您的代码 - 它不会工作。 @Šime:感谢您的评论。我已经扩展了我的答案,只是为了使其完整并适用于“远程”页面。我知道您的问题可能排除了这种方法。 问题明确禁止代理。 @BalusC:我在回答和评论中提到了这一点。

以上是关于是否可以执行异步跨域文件上传?的主要内容,如果未能解决你的问题,请参考以下文章

(H5)FormData+AJAX+SpringMVC跨域异步上传文件

H5拖放+FormData接口+NodeJS,完整异步文件上传

七牛上传开发(三)使用NodeJS生成带数据处理的凭证

异步 JQUERY 文件上传

Java异步调用实现并发上传下载SMB共享文件

能用ajax以POST实现文件上传吗