在最近的一个项目中,我打算在页面上实现这样一个功能:
在网页上画出某种图形,上传到服务器后,返回一个src地址。这个地址可以用来分享到各种社交媒体。
这个功能看似非常简单,但要实现它还需要注意各种小的细节。
首先说下思路和技术要点:
- 用canvas来实现页面的画图功能,利用canvas的toDataURL方法可以很方便地将画布的数据保存为dataURIs。
- 用jQuery的ajax方法将图像数据异步提交到后台,在后台处理上传后,返回一个地址给前端调用。
按照这个思路马上就用jQuery.get()实现了一个测试用例。但结果非常让人失望!下面就是我遇到的各种问题:
- post OR get
首先是请求方式问题。
我们知道,http的get请求是利用url发送给服务器请求,受制于字节数限制,无法发送大的数据量。jquery中的get方法是其ajax方法的一个封装,基本原理仍是利用XMLHttpRequest对象发送了一个get请求,要发送图片这种较大的数据,用get方式显然力不从心,所以必须用post方式提交。
心想这么简单,于是把get改成post,over!
但结果失败了,因为这里遇到了另一个很严重的问题。
- 跨域提交(如果你是同域上传的话,这一段可以忽略了)
jQuery中的post方法在同域的情况下,可以顺利地以异步的方式提交form 数据,并用返回一个json结果。
所以,在同域的情况下,你可以这样
$.post("url",form1.serialize(),function(json){console.log(json)},'json');
服务器将处理后的结果用json形式返回给前端。
但在我的工作环境中,跨域是很常见的。而且我也希望能将这个功能组件化,让它在不同的子域中调用。
这样就必须要用到跨域的方式,于是很自然地想到jQuery中的jsonp。而且在jquery官方的ajax方法文档中,写明了dataType的值可以是"jsonp"。
于是又去改了一通,点了该死的按钮以后返回的是"HTTP 414 Request-URI too long"!God!
原来jQuery的post方法在跨域提交时会自动转换为GET方式,此时提交的数据已经超过了GET方式的请求字节限制!
这个时候,虽然有些气急败坏,但终于找到症结所在,就是下面要说的问题:
- 如何在跨域的情况下异步发送post请求?
这里要说明一个情况,就是为什么要用异步?
因为在我之前的许多项目中,大多都是在页面上发送一个异步请求,把服务器的结果实时地展示到页面上来,而不必刷新整个页面。当然会很自然地想到这样的实践方式。
我没有考虑到的是,之前的这些实践都是发送的get请求。当发送get请求时,如果是跨域,利用XMLHttpRequest对象发送get请求时可以用jsonp的方式跟服务器配合取回结果;如果是同域,那就更没有问题了。
但这次要上传图像数据,只能发送post请求,而且要异步实现。这可就犯难了。
好在守着互联网这个宝库,稍微搜索了一番以后,终于发现了一些有用的东西。
首先是这篇:POST跨域问题
看了这个后,知道了异步的情况下是不能跨域发送post请求的。想想也是,你在自己的网站上随便form发送了一大段数据到别人的网站,人家不搭理你就不错了,还要人家给你返回个“哥俩好”,可能么?
安全很重要,但我自己的两个子域虽然是跨域,但总归是一家子。一家人怎么才能不说两家话呢?
来看这个:跨域post请求实现方案小结
文档中列举出了目前实现跨域请求很多方法,如CORS、invisible iframe、server proxy、flash proxy。
还有一些文章列出了HTML5 WebSocket方法,但这种方式目前还不是标准,或者还没有流行起来,要想让功能以比较稳定的方式运行,最好还是用当下主流的方式。于是决定用iframe来实现。
- iframe实现跨域提交的原理
看了这个图以后,基本上思路就明晰了。
我们用异步的目的是实现无刷新,既然post不能跨域异步提交,并且不异步也可以无刷新,那我们就不异步。我把form提交到本页面的一个空iframe中,这样不会造成页面的刷新。让服务器处理完后跳转到同域下的upload_result网页里,并给这个网页的url附加处理的结果。这样原始请求页的空iframe就加载了服务器跳转的那个页面的内容。同时,虽然这个upload_result页虽然跟我的原始页面不在一个子域,但我可以人为地设置它们的document.domain为同一个父域,当这个upload_result页面在我的原始请求页加载时,就可以执行父页面的callback函数。
- 实现步骤
明白了基本的原理,剩下的就是体力活了,下面看看怎么构建这样一个机制。
- 首先在服务器端写好一个upload_result.html,里面用js解析出页面URL附带的信息。其中包括两个最重要的信息,服务器上传图片后的地址和原始页面请求的回调函数名
- 在服务器端写好upload脚本,处理前端post过来的数据,最重要的两个域对应地是文件信息(这里是二进制数据)和回调函数名称。当处理完成图片上传后,用header("Location: upload_result.html?paramString")跳转到upload_result页。其中paramString包含了url格式的图像地址和回调函数名称键值对。
需要注意的是,传给服务器端的数据是base64编码的,需要先解码才能保存。 - 前端页面需要构造的元素如下:
- 一个回调函数,比如upload_callback,此函数须能让iframe的页面中访问到。
- 一个空的iframe
- 一个隐藏的form。给这个form设置action为服务器处理地址,target为空iframe的name/id。再在这个form 中设置两个个隐藏的input:一个用来接收canvas图像数据,另一个用来接收页面的回调函数名。
这样,当用户在canvas画完图,点击upload按钮时,把canvas的信息和回调函数名赋值给form,触发form提交。结果返回给空iframe,iframe 解析自身的返回结果,并触发父页面的callback函数。于是实现了页面的无刷新跨域post提交。Good!
参考:
- JSONP跨域的原理解析
- 使用 HTML5 WebSocket 构建实时 Web 应用
- JSONP与POST方式请求
- 利用iframe进行post“跨域”无刷新提交
- js利用form+iframe解决跨域post和get提交
- ajax跨域、iframe跨域和JS跨域通信的几种解决方案
- php读取和保存base64编码的图片内容
延伸
实现了canvas二进制图像的跨域上传和返回,那么在此基础上还可以做些更好玩的:比如图像编辑。
功能很简单:用户打开本地的图片文件并加载到canvas中,在画布上进行各种编辑,完了上传到服务器,并把返回的地址分享出去。
这里要用到的一个新东西就是HTML5的FileReader对象。利用它可以实现如本地预览等功能,我们用它来把选中的本地文件加载到canvas中,编辑后上传。