JavaScript 文件上传详解

更新时间:2017-03-02 09:23:00 点击次数:1912次

本教程包含7个 Demo,它们循序渐进、由浅入深地讲解文件上传。每个 Demo 都被精心设计,都是可执行的。因为我刚做完并上线了一个真实的文件上传程序,所以有些 Demo 对实际生产有指导意义。

除了前端的上传部分,后端的接收部分也由我们一手操办,并且没有用现成的包而是亲自去解析数据,因为我想让你更清晰的看到 HTTP 协议。

在运行 Demo 的时候,请将网络速度调低,这样,我们就可以清楚地看到 http 的交互过程。

调低网络速度的方法之一,是用 Chrome 的 Debugger 工具,下文会有详细的图示。

下载zip文件,然后解压到c盘 c:\> cd javascript-file-upload-master c:\> node demo1\server.js linux or mac
$ git clone https://github.com/ktont/javascript-file-upload
$ cd javascript-file-upload
$ sudo node demo1/server.js 类推,运行demo2的时候,去执行demo2下的server.js。
$ sudo node demo2/server.js

然后在浏览器中(建议 Chrome)打开 http://localhost

ERROR: 如果你遇到 EADDRINUSE 的错误,那是因为80端口已经被其它诸如 apache、nginx 的进程占用了。 
可以在启动的时候指定端口, 比如端口3000。

$ node demo1/server.js 3000

ERROR: 如果遇到 EACCES 的错误,请用 sudo 权限运行它。

$ sudo node demo1/server.js

demo1 form 表单,原生的文件上传方式 
demo2 plupload 的原理 
demo3 mOxie 文件选取和文件预览 
demo4 mOxie 文件上传,进度提示 
demo5 使用 plupload 实现了图片上传 
demo6 断点续传 
demo7 plupload 之 Ui Widget 的示例 
总结

1. form 表单,原生文件上传方式

首先,来看个例子。它是用原生的文件提交方法,前端只有一段 HTML 而没有 JS。我们的目的是观察 http 协议。

前端 index.html,使用一个 input 标签进行文件选择,然后使用 form 表单发送数据。 
后端 server.js,对表单发过来的数据进行解析,把协议格式打印出来。

点击“选择文件”后:


在点击 “Upload” 按钮之前,对网络进行限速,方便观察数据传输的过程。打开 Debugger“


点击后,选取一个较慢的:


服务端会打印下面的提示,注意红框中的 token,它用来表示二进制数据的边界。


你在 server.js 中可以看到解析 http 数据的 formidable 函数。

可以调试它,用来学习 http 协议。

上传完成后:


TIP: 观察,它是我们本次学习之旅的主要方法。

你一定要运行每个例子,看到它们运行,观察它们的行为。

这样,就熟悉了这个技术。

2. plupload 的原理

plupload 是一个文件上传的前端插件。

主页Github 地址

demo2 并没有使用 plupload,事实上它是自己实现了 plupload,它本身就相当于 plupload 的 v0.01 版本。

通过 v0.01,这20行代码来一窥 plupload 的原理。而不是去读 plupload 的上万行代码, 
真是,两岸猿声啼不住,轻舟已过万重山,一日千里。

plupload 的原理,就是拿到文件句柄后,自己发送(XMLHttpRequest)文件。 
尽量控制整个过程,从中加入自己实现的功能,这就是它的想法。

这些操作,都有个前提,就是要拿到文件。否则,一切无从做起。

3. mOxie文件选取和文件预览

这个例子没有服务端,请直接用浏览器打开 demo3/index.html。然后选取图片,就可以看到预览。 
这样避免你想当然的认为,预览是服务端辅助的。


文件预览一般的做法是,先上传图片,然后从图片服务器上下载 thumbnail,这么做是有缺点的,预览要先上传才能看到(可能人们更喜欢先看到再决定要不要上传)。但是这里采用的做法不同,它在本地进行预览,但这势必会增加一些 cpu 的开销,因为预览的实质是进行了图片压缩(要么服务端压缩要么客户端压缩而已)。

实际生产中,采用哪一种做法,要看需求,或者看你方便的程度。如果需求中要求节省流量,或有上传前删除功能,那就采用本地预览(也就是本例的做法)。如果服务器能存储压缩后的 thumbnail,且压力不大,速度够快,那就用服务端预览。

另外,当你看到 mOxie 的时候,可能会觉得莫名其妙。是这样的:

打开 http://www.plupload.com/docs/

文档的后一段话如下:

  • Low-level pollyfills (mOxie)
  • Plupload API
  • UI Widget
  • Queue Widget

其实我写本文的初衷,是为了解释这四句话。我跟你一样,一开始读不懂。这四句话的意思是: plupload 有四个安装等级 —— 初级,中级,高级,长级。

那么回过头,再来看这个例子。这个例子只是演示文件选择,它没有上传的功能。 
只有文件选择功能的 mOxie 插件的大小为77k,比正常功能要小30%。为什么呢?

因为 mOxie 是一个可以自定义的前端库,如果有些功能不需要,比如 silverlight,那么就可以不把它们编到目标中。 参见 编译 mOxie

那么 mOxie 都做了什么呢,为甚么有77k这么大(大吗?)的体积。它提供文件预览功能、图片压缩功能、国际化支持(就是 i18n )等。同时,上面也提到,它解决浏览器的兼容性问题。

4、mOxie文件上传,进度提示

这个例子只使用 mOxie 提供的功能,实现了文件上传

$ ls -l demo[3-4]/moxie.min.js -rw-r--r-- ktont  staff 73499 13:53 demo3/moxie.min.js -rw-r--r-- ktont  staff 77782 13:58 demo4/moxie.min.js

您会发现,本例中的 mOxie 库比上一例多了4k,那是因为在编译的时候加入了 XMLHttpRequest 的支持。

所以 demo4 中的 moxie.min.js 就是 plupload 库能投入生产的精简版本。参见 编译 mOxie

您可以在这个 demo 的基础上实现自己的文件上传。相比 Plupload API,它更灵活,您可能更喜欢在这个层次上编写应用。当然,灵活性的对立面是复杂度,它们之间的平衡点因人而异。

5、使用plupload实现了图片上传

这个例子,比较实际一点,使用 Plupload APIPlupload API 主要在 mOxie 上实现一套事件驱动的机制。

同时,顺带演习上传的暂停和重传。为甚么在这里演习暂停和重传呢?

为了区分下个例子 – 断点续传。断点续传是指,重启了电脑后断点续传

断点续传在上传大文件的场景下,很有用。 
比如我上传一个电影,中间关闭了电脑,然后睡个觉。醒来后可以继续传。

下一个例子演示断点续传。

而本例的重传是说,不重启浏览器的前提下,重新传文件。它会从头再来,之前传的会丢弃。 
实际场景中,用来重传图片这种小文件。 
因为小文件一个封包或几个封包就发送完了,没必要断点续传,也没法儿断了。 
大炮不适合打蚊子,因为蚊子小(我怎么这么啰嗦——)

6、断点续传

是时候请出你的硬盘女神啦!运行本程序需要一个大文件,而电影文件再合适不过了。


选取文件后,并没有立即上传。而是去服务器询问上次传输的断点。 
在本例中,服务端会返回一个50到100的随机值,它表示百分比,用来模拟实际情况中的上次的断点

例如,下面图片中,上次的断点是94%:


你可能会误认为服务器会从94%的地方把数据存起来,不是的, 
它的意思是告诉客户端,请从文件94%的地方把剩下的数据发送过来

服务端的情况:


本例中使用的大小是1兆字节,这个配置在 index.html 的19行

 chunk_size: '1mb' 

上图中,两个绿色框之间是一次独立的 http 交互过程,它用来发送一个

本例中的文件一共4G多,会切成4千多个块。产生4千多次 http 交互来发送它们。 
相比不分块而一次 http 发送完所有数据,这么做会有些网络性能损耗。但是不分块的缺点是非常明显的。

如果真的不分块,单 http 发送所有数据。假设网络异常,服务端 hanging,客户端此时开启另一个链接 retry。retry 首先询问服务端上次的断点,然后从该断点处继续发送。之前 hanging 的链接可能已经 hang up,也可能没有,这取决于服务端的超时时间。

此时,服务端就会面临一个尴尬的选择,必须关闭之前 hanging 的链接。因为如果不关闭,网络中残留的数据可能继续写入文件,导致数据错乱。服务端一般请求间是无法操作的,一个请求不能操作其它请求。


虽然,实际上几乎不会出现上面的情况,但是它不严谨。并且, 
http 协议是一个应用层协议。http 协议在 application 和 network transfer 更靠近 application。大多数 http 服务器都会帮你做封包的拼解工作,而让你从网络层传输层解放出来。如果达不到这一点,http 的处理还是和 tcp 一样麻烦,那 http 就不应该存在。参考 http协议

然而,如果分块来传输,就不会遇到这个问题。如果链接 hang up。那么整个请求的数据统统丢弃,偏移仍然在当前

话说回来,所以要把文件数据拆分成一个较小的单元来用 http 传输,并且 
* 用块发送可以降低 token 冲突的概率。上传文件是使用一个随机 token 来标记数据边界(个绿框)的。 
当文件大的时候,会有可能遇到和 token 一样的字符串。但是,分块传,会每次都换一个 token。 
* 适当的大小,有助于浏览器读取文件。比如本例中 chrome 用的是 slice 读取文件,我们不能指望它很智能,塞给它一个很大的文件,让它很好的处理。有些浏览器对文件大小有限制,甚至在传大文件的时候会卡死。

上图中,红色的框表示当前传输的是第几块数据。因为服务端给了随机值94%,所以这里是4261的尾部 – 4005。

黄色的框表示一共有多少块数据。当红色和黄色相等的时候,表示文件传输完成。

灰色的框表示传输的二进制数据,数据的边界由个绿框定义。这个时候,这次 http 交互就完成了,链接会被关闭。紧接着会是下一块数据,一个全新的 http 交互,token 也会是一个新的。

断点续传的关键在于 --从文件的指定偏移处读取 (ZHUANGBI: c语言中 fseek)

但是浏览器提供给前端的功能都是受限的,没有 fseek,而是提供了一个 slice 功能。 
比如,slice(off, off+1024) 用来读取 off 处的1024字节数据。 
还能凑合着用吧,那我们每次读一块数据,然后发送,再读下一块,再发送。。。

突然发现,这不就是失传已久的 socket 编程 吗?搞一个 缓冲,撸一串数据然后发出去,再撸一串数据再发出去。

好吧!幸亏不是让我们写这种恶心的数据解析工作,plupload 已经给我们写好了,我刚撸起的袖管赶紧放了下去。

7、plupload ui widget的示例

这个例子,用来展示 plupload 的 UI Widget


在 index.html 中,ui 部分只需安置一个 div

plupload 会在这个 div 中,自动安插一个 ui 组件,就是图片中展示的那个。

这样极大的方便了开发,你可能一句 js 都没写,就实现了复杂的图片上传。 
当然,你可以定制这个组件,那样需要一些学习成本,并且挺高的。所以,如果你想要一个轻量,自定义的 ui 组件的时候,就需要自己设计 ui 了。 
比如下面这样的


在上面这个组件中,要求

总结

在生活中,图片上传的应用越来越广泛。特别是在智能手机普及以后,获得图片很便捷,图片的质量也很高。

比如,在一个突发事件中,人们能很及时的从各个角度拍到它,然后分享到朋友圈或者上传到网上。

在移动端的开发中,因为手机的特点,它处理能力弱、展示空间小,导致图片上传技术有些困难。另外, 
站在运营商角度,从长远来看,手机流量会是主要的营收服务。虽然流量成本不高,但它不会便宜。这样,用户就会在乎自己的流量。 
还有,在实际应用中,需要后端配合搭建图片服务器、图片数据库,前端还要解决跨域的问题,虽然本文没讲这些,但实际也要开发者解决。所以,编写一个省流量又好用的图片上传程序是一个挑战。

另一方面,随着浏览器和web技术的持续推进。传统的大文件上传,势必会转到 web 上来实现,而不是一些桌面 app。这里面甚至还蕴含一些商机。 
这还需要一些时间,因为我知道很多人还在用 xp,但趋势是明确的。

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!