在前端开发工作中,上传文件是一个很常见的功能,尤其是后台管理方面的项目,经常需要用到。而目前单页面应用(SPA)框架的流行,使得以往上传文件的方式略显笨拙,给开发带来不便。那么今天我们就一起来实现一个非常简洁易用的上传工具。
如同你看到的标题一样,为什么说是纯js呢?因为我们的目标是:
- 不引入任何三方工具(jquery也不用)
- 不需要写任何标签和css(反感写样式的童鞋,有没有很期待?)
- 支持任何主流前端框架引入使用(这也是纯js实现的好处)
明确了目标,接下来我们需要知道为什么要做这样一个工具,能解决现实工作中的什么问题。
先回顾一下传统上传文件的方式:
- 首先需要写一个表单标签 form,指定请求方式为 post,指定 enctype 为 multipart/form-data,定义好处理上传文件的 action;
<form method="POST" action="https://your.domain.com/upload"></form>
- 在写好的form标签里添加input标签,type 为 file,用来选择要上传的文件。
<input type="file" value="请选择文件" />
- 继续添加一个按钮标签,类型为submit,用来提交表单。
<button type="submit">提交</button>
完整代码如下
<form method="POST" action="https://your.domain.com/upload">
<input type="file" value="请选择文件" />
<button type="submit">提交</button>
</form>
最终界面可能长这个样子
由于form表单的提交会导致页面重定向,当我们点击选择文件,然后点击提交按钮后,当前页面重定向到了action所指定的页面,再加上目前很多处理文件上传的action都是基于微服务架构的RESTful接口,你会发现当前页面被刷新变成了接口返回的数据!
现在我们给form的 target 属性设置 _blank,经过测试发现,当前页面虽然没有刷新,但是action指向的页面在一个新窗口中打开了,而实际需求往往是我们需要在当前页面接收返回的数据,明确告诉用户上传是否成功,很多情况下还需要把上传的文件回显出来(比如图片或者音频资源),因此,我们必须在当前页面处理上传结果。
我们在刚才的form下面写一个iframe标签,指定id和name,然后将form的target属性设置为这个iframe的id,这样做的目的是将form表单的提交目标指向这个iframe,当提交表单之后,接口返回值便会显示在iframe里面,代码如下。
......
</form>
<iframe id="uploadtarget" name="uploadtarget"></iframe>
但这样做依然没有达到我们的预期,然而事情似乎变得容易了,我们把iframe隐藏起来,然后用js读取iframe里面的内容(接口返回值)即可,那我们什么时候去读取呢?好在iframe每次加载成功后都会派发onload事件,而上传文件接口的返回值指向了iframe会触发它的onload事件,因此我们只需要为onload事件添加回调函数即可。
......
</form>
<iframe style="display:none" onload="onFrameLoad()"
id="uploadtarget" name="uploadtarget"></iframe>
<script>
function onFrameLoad(){
var frame = document.getElementById("uploadtarget");
var content = frame.contentWindow.document.body.innerHtml;
//解析content,分析上传接口返回值(略)
}
</script>
通过上面的传统方式我们已经基本实现了无刷新上传文件,并且能相对容易的获取到上传接口的返回数据了。但我们发现这种实现方式的成本还是比较大的,尤其是在如今前端工程化,模块化,组件化要求必不可少的情况下,这样的实现方式显然不能被接受。
接下来正式进入本文的正题,不写标签,不引入其他工具,用最纯粹的方式实现上面的步骤。
首先让我们分析一下刚才的示例,其中类型为file的input标签必不可少,因为它是用户访问本地磁盘文件的入口,但基于我们的目标是不让开发者写任何标签,因此这个input必须由我们的工具动态创建,现在我们就来实现这微不足道但又必不可少的第一步。
//先给我们的目标功能起一个名字 easyUpload
function easyUpload(){
var input = document.createElement("input");
input.type = "file";
}
接下来我们需要让这个input去浏览本地磁盘以便让用户去选择想要上传的文件,但是问题来了,由于我们的工具不提供任何界面,那如何让用户去点击input呢?很简单,我们只需要调用这个动态input的click方法即可,但是,浏览器出于安全的考虑,选择本地文件必须由用户行为触发,因此我们定义的easyUpload函数也必须在用户事件(如click回调函数)里去调用,这一点请务必遵守。
input.click(); //代码执行到这里,本地文件选择框便会打开
//加油,离目标似乎很近了!
现在假设用户已经选择了想要上传的文件,那我们该如何去获取这个文件呢?很简单,input标签在每次选择了文件之后都会触发onchange事件,我们在这个回调里获取文件即可。通过访问input的files属性,我们可以获取到用户选择的文件集合,这里我们只实现单文件上传,因此我们获取files下标为0的对象。这个对象包含了文件的一些常见属性,如name(文件名)、size(文件大小)、type(文件类型)等等,利用这些属性,我们可以对小工具进行功能的扩展,比如利用size和type可以控制上传文件的大小限制和文件类型要求。这个不在本文讨论的范围内。
input.onchange = function(){
var file = input.files[0];
}
拿到文件之后,我们就需要把它提交给服务端了,在上面的传统方式中,提交文件我们用到了form标签,但由于我们的工具想要尽可能的利用编程式思路去实现文件上传,如果还是像input一样去动态创建dom的话,会觉得不够优雅,虽然一样能解决我们的问题,而且原理也是完全相同的,但我们总是希望自己的代码足够清晰和简介明了。所以,在这里我们放弃动态创建form的方式,而使用FormData来实现。
让我们先了解一下FormData的定义。
FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。如果表单enctype属性设为multipart/form-data ,则会使用表单的submit()方法来发送数据,从而,发送数据具有同样形式。
相信你已经发现了它的好处,FormData完全就是form标签的对象形式,有了它,我们就可以用编程的方式去操作form了,然后通过XMLHttpRequest来提交表单,最后在它的异步回调里轻松的拿到接口数据!废话少说,秀代码!
var form = new FormData();
form.append("file", file); //第一个参数是后台读取的请求key值
form.append("fileName", file.name);
form.append("other", "666666"); //实际业务的其他请求参数
var xhr = new XMLHttpRequest();
var action = "http://localhost:8080/upload.do"; //上传服务的接口地址
xhr.open("POST", action);
xhr.send(form); //发送表单数据
xhr.onreadystatechange = function(){
if(xhr.readyState==4 && xhr.status==200){
var resultObj = JSON.parse(xhr.responseText);
//处理返回的数据......
}
}
通过这段示例代码我们不难发现,使用FormData我们可以很轻松的把文件添加到表单里,并为这个文件指定参数名,文件名,以及其他跟实际上传业务相关的附加参数。更重要的是,利用FormData我们还可以实现在线录音,在线签字,在线截图以及你能想到的更多可能,这方面的内容也不在本文讨论的范围内,如果你感兴趣,欢迎在评论区域留言,我会抽时间来写这方面的文章。
上面的代码也为我们展示了如何用XMLHttpRequest来提交表单,以及如何获取接口数据,其中有几个关键点需要注意,action变量和传统方式表单的action属性值是一样的,是用来处理上传文件的接口地址,这个变量我们可以作为参数传递给小工具,然后我们需要调用XMLHttpRequest对象的open方法,open方法的第一个参数定义了请求类型,这里我们务必要使用post类型,open方法的第二个参数就是上面定义的action,最后我们调用send方法将表单对象作为参数发送出去,别忘了在onreadystatechange回调函数里接收接口数据,像这样,一个最原始的Ajax请求就完成了,虽然原始,但它是解决问题最简单,最直接有效的方式。固然npm上有丰富的工具,几乎涵盖了我们能遇到的所有问题,但有时候解决问题可能只需要很简单的几步,在这种情况下,如果还是去安装三方依赖的话,反而把简单问题复杂化了,这个话题仁者见仁,不再过多谈论。
下面是全部示例代码:
function easyUpload(){
var input = document.createElement("input");
input.type = "file";
input.click();
input.onchange = function(){
var file = input.files[0];
var form = new FormData();
form.append("file", file); //第一个参数是后台读取的请求key值
form.append("fileName", file.name);
form.append("other", "666666"); //实际业务的其他请求参数
var xhr = new XMLHttpRequest();
var action = "http://localhost:8080/upload.do"; //上传服务的接口地址
xhr.open("POST", action);
xhr.send(form); //发送表单数据
xhr.onreadystatechange = function(){
if(xhr.readyState==4 && xhr.status==200){
var resultObj = JSON.parse(xhr.responseText);
//处理返回的数据......
}
}
}
}
本文重在讨论解决问题的思路,其中展示的示例代码仅供参考,如果需要使用这个小工具,欢迎安装我的npm包。
包名:zq-easy-uploader
在线地址:zq-easy-uploader
Github地址:https://github.com/VirgilZhang/easyuploader
如果对本文中的任何内容有异议,或者发现有错误的地方,欢迎在评论区域留言,笔者会及时回复大家。如果你喜欢笔者的文章,记得点赞哦,你们的支持是我写作的动力!
[ 原创文章,转载请注明出处 ]
我的其他文章
欢迎大家在评论区留言自己想了解的前端话题,我会继续推出更多精彩的文章!
原创不易,有钱的捧个钱场,给个打赏