JavaScript 文件上传详解

本文为 Qunar 技术沙龙投稿,版权归原作者所有,未经允许,请勿转载。
原文地址:http://mp.weixin.qq.com/s/KWFyJa06CNXrU8zhSzzQFQ
作者:梁志,2015年加入 Qunar,目前在去哪儿网大住宿 UED 中心担任前端工程师,喜欢捣鼓操作系统、系统工具,擅长分析开源程序和网络协议。
技术之路,共同进步,欢迎投稿、给文章纠错,请发送邮件至[email protected],或加微信 tree-rain-chen。

写在前面

本教程包含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,对表单发过来的数据进行解析,把协议格式打印出来。

点击“选择文件”后:

JavaScript 文件上传详解_第1张图片

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

JavaScript 文件上传详解_第2张图片

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

JavaScript 文件上传详解_第3张图片

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

JavaScript 文件上传详解_第4张图片

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

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

上传完成后:

JavaScript 文件上传详解_第5张图片

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

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

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

2. plupload 的原理

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

主页、Github 地址

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

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

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

  • 比如,图片预览,是在拿到文件以后在新的 canvas 上画出新的尺寸。
  • 比如,断点续传,是在拿到文件以后 slice 文件,从断点处开始读取。

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

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

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

JavaScript 文件上传详解_第6张图片

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

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

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

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

文档的最后一段话如下:

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

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

  • 初级,叫 moxie.min.js,插件大小77k到106k不等(神马鬼?为什么不等的原因参见 编译 mOxie 一节)。
    其中提到的 pollyfills 应为 polyfills,是帮助老浏览器跟上 h5 步伐的插件,叫 h5 垫片,用 js 提升老浏览器的 api,抹平浏览器间的差异。所以 mOxie 其实是个通用前端库。
  • 中级,plupload.full.min.js,插件大小123k
    打开它看一下,发现它其实是 moxie.min.js 和一个叫 plupload.min.js 的文件合并到一起而已。
    所以 plupload 其实是在 mOxie 的基础上,封装了一下文件上传 api,专业文件上传前端库。
  • 高级,它依赖
    jquery 137k
    jquery ui 282k
    plupload 123k
    plupload ui 30k

    一共约600k的大小。帮助你实现 UI,叫 widget - 小组件。
  • 长级,它和高级差不多,也是实现一套 UI。区别是 UI 是队列,前者的 UI 是块和列表。

那么回过头,再来看这个例子。这个例子只是演示文件选择,它没有上传的功能。
只有文件选择功能的 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、断点续传

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

JavaScript 文件上传详解_第7张图片

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

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

JavaScript 文件上传详解_第8张图片

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

服务端的情况:

JavaScript 文件上传详解_第9张图片

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

         chunk_size: '1mb'

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

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

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

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

JavaScript 文件上传详解_第10张图片

虽然,实际上几乎不会出现上面的情况,但是它不严谨。并且,
http 协议是一个应用层协议。http 协议在 applicationnetwork 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

JavaScript 文件上传详解_第11张图片

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

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

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

在上面这个组件中,要求

  • 最多只能选n张
  • 更好的用户体验
  • 可以删除节点
  • 上传失败后可以重传
  • 页面大小控制在100k以内

总结

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

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

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

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


JavaScript 文件上传详解_第12张图片

你可能感兴趣的:(网络,断点,JavaScript,文件上传)