{"id":21051299,"url":"https://github.com/ktont/javascript-file-upload","last_synced_at":"2026-02-28T07:09:12.923Z","repository":{"id":143933300,"uuid":"82137978","full_name":"ktont/javascript-file-upload","owner":"ktont","description":"JavaScript 文件上传的研究，起源于一个实际应用。分析了文件上传的 http 协议，plupload 前端库的使用详解。难得的是，实现了断点续传。","archived":false,"fork":false,"pushed_at":"2019-11-18T08:50:13.000Z","size":8064,"stargazers_count":84,"open_issues_count":0,"forks_count":20,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-22T03:40:45.476Z","etag":null,"topics":["break-point-continue-transfer","image-preview","image-upload","plupload","upload"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ktont.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-02-16T04:10:27.000Z","updated_at":"2024-07-24T14:27:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"e81453d6-69be-473e-accb-48b4f03a9085","html_url":"https://github.com/ktont/javascript-file-upload","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ktont/javascript-file-upload","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktont%2Fjavascript-file-upload","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktont%2Fjavascript-file-upload/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktont%2Fjavascript-file-upload/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktont%2Fjavascript-file-upload/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ktont","download_url":"https://codeload.github.com/ktont/javascript-file-upload/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ktont%2Fjavascript-file-upload/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29927268,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T19:37:42.220Z","status":"online","status_checked_at":"2026-02-28T02:00:07.010Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["break-point-continue-transfer","image-preview","image-upload","plupload","upload"],"created_at":"2024-11-19T15:52:19.749Z","updated_at":"2026-02-28T07:09:12.905Z","avatar_url":"https://github.com/ktont.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JavaScript文件上传详解\n\n~~~sh\ngit clone https://github.com/ktont/javascript-file-upload\nnode javascript-file-upload/demo6/server.js\n~~~\n\n## 目录\n\n\u003e [demo1 form 表单，原生的文件上传方式](#demo1)   \n\u003e [demo2 plupload 的原理](#demo2)  \n\u003e [demo3 mOxie 文件选取和文件预览](#demo3)   \n\u003e [demo4 mOxie 文件上传，进度提示](#demo4)   \n\u003e [demo5 使用 plupload 实现了图片上传](#demo5)   \n\u003e [demo6 断点续传](#demo6)   \n\u003e [demo7 plupload 之 Ui Widget 的示例](#demo7)   \n\u003e [demo8 服务端限速](#demo8)   \n\u003e [demo9 FAQ 批量上传的时候，如何为每个文件添加一个id](#demo9)     \n\u003e [总结](#end)   \n\n本教程包含7个 demo，它们循序渐进、由浅入深地讲解**文件上传**。每个 demo 都被精心设计，都是可执行的。\n因为我刚做完并上线了一个真实的**文件上传**程序，所以有些 demo 对实际生产有指导意义。\n\n除了前端的**上传**部分。后端的**接收**部分也由我们一手操办，并且没有用现成的包而是亲自去解析数据，因为我想让你更清晰的看到 **http 协议**。\n\n在运行 demo 的时候，请将网络速度调低，这样，我们就可以清楚的看到 **http 的交互过程**。\n调低网络速度的方法之一，是用 chrome 的 debugger 工具，下文会有详细的图示。\n\n~~~bash\nwindows\n下载zip文件，然后解压到c盘\nc:\\\u003e cd javascript-file-upload-master\nc:\\\u003e node demo1\\server.js\n\nlinux or mac\n$ git clone https://github.com/ktont/javascript-file-upload\n$ cd javascript-file-upload\n$ sudo node demo1/server.js\n类推，运行demo2的时候，去执行demo2下的server.js。\n$ sudo node demo2/server.js\n~~~\n\n然后在浏览器中(建议 chrome)打开 [http://localhost](http://localhost)\n\n__ERROR__: 如果你遇到 EADDRINUSE 的错误，那是因为80端口已经被其它诸如 apache、nginx 的进程占用了。\n可以在启动的时候指定端口, 比如端口3000。\n\n$ node demo1/server.js 3000\n\n__ERROR__: 如果遇到 EACCES 的错误，请用 sudo 权限运行它。\n\n$ sudo node demo1/server.js\n\n## \u003ca name=\"demo1\"\u003e\u003c/a\u003e1、form表单，原生文件上传方式\n\n首先，来看第一个例子。\n它是用原生的文件提交方法，前端只有一段 html 而没有 js。我们的目的是**观察** http 协议。\n\n前端 index.html，使用一个 input 标签进行文件选择，然后使用 form 表单发送数据。\n后端 server.js，对表单发过来的数据进行解析，把协议格式打印出来。\n\n点击 选择文件 后\n\n\u003cimg src=\"img/1.1.png\" width=\"500\"\u003e\n\n在点击 Upload 按钮之前，对网络进行限速，方便观察数据传输的过程。打开 debugger\n\n\u003cimg src=\"img/1.2.png\" width=\"500\"\u003e\n\n点击后，选取一个较慢的\n\n\u003cimg src=\"img/1.22.png\" width=\"200\"\u003e\n\n服务端会打印下面的提示，注意红框中的 token，它用来表示二进制数据的边界。\n\u003cimg src=\"img/1.3.png\" width=\"400\"\u003e\n\n你在 server.js 中可以看到解析 http 数据的 formidable 函数。\n可以调试它，用来学习 http 协议。\n\n上传完成后\n\n\u003cimg src=\"img/1.4.png\" width=\"450\" style=\"border: 1px solid red\"／\u003e\n\n__TIP__: 观察，它是我们本次学习之旅的主要方法。\n你一定要运行每个例子，看到它们运行，观察它们的行为。\n这样，就熟悉了这个技术。\n\n## \u003ca name=\"demo2\"\u003e\u003c/a\u003e2、plupload的原理\n`plupload` 是一个文件上传的前端插件。\n\n[它的主页](http://www.plupload.com) [它的github地址](https://github.com/moxiecode/plupload)\n\ndemo2并没有使用 `plupload`，事实上它是自己实现了 `plupload`，它本身就相当于 `plupload` 的 v0.01 版本。\n\n通过 v0.01，这20行代码来一窥 `plupload` 的原理。而不是去读 `plupload` 的上万行代码，\n真是，两岸猿声啼不住，轻舟已过万重山，一日千里。\n\n`plupload` 的原理，就是拿到文件句柄后，自己发送(XMLHttpRequest)文件。\n尽量控制整个过程，从中加入自己实现的功能，这就是它的想法。\n\n* 比如，图片预览，是在拿到文件以后在新的 canvas 上画出新的尺寸。\n* 比如，断点续传，是在拿到文件以后 slice 文件，从断点处开始读取。\n\n这些操作，都有个前提，就是要拿到文件。否则，一切无从做起。\n\n## \u003ca name=\"demo3\"\u003e\u003c/a\u003e3、mOxie文件选取和文件预览\n\n这个例子没有服务端，请直接用浏览器打开 `demo3/index.html`。然后选取图片，就可以看到预览。\n这样避免你想当然的认为，预览是服务端辅助的。\n\nNOTE: h5 是无法直接在 img 标签中嵌入本地图片预览的。本例使用 canvas 在页面上画出缩略图。\n\n\u003cimg src=\"img/3.1.png\" width=\"500\"\u003e\n\n文件预览一般的做法是，先上传图片，然后从图片服务器上下载 thumbnail，这么做是有缺点的，预览要先上传才能看到（可能人们更喜欢先看到再决定要不要上传）。但是这里采用的做法不同，它在本地进行预览，但这势必会增加一些 cpu 的开销，因为预览的实质是进行了图片压缩（要么服务端压缩要么客户端压缩而已）。\n\n实际生产中，采用哪一种做法，要看需求，或者看你方便的程度。如果需求中要求节省流量，或有上传前删除功能，那就采用本地预览（也就是本例的做法）。如果服务器能存储压缩后的 thumbnail，且压力不大，速度够快，那就用服务端预览。\n\n另外，\n当你看到 mOxie 的时候，可能会觉得莫名其妙。是这样的\n\n打开 http://www.plupload.com/docs/\n\n文档的最后一段话如下\n\u003e * Low-level pollyfills (mOxie)\n\u003e * Plupload API   \n\u003e * UI Widget  \n\u003e * Queue Widget  \n\n其实我写本文的初衷，是为了解释这四句话。\n我跟你一样，一开始读不懂。\n\n这四句话的意思是\n `plupload` 有四个安装等级 － 初级，中级，高级，长级\n\n* 初级，叫 moxie.min.js，插件大小77k到106k不等（神马鬼？为什么不等的原因参见  [编译 mOxie](docs/compile.md) 一节）。\n    其中提到的 pollyfills 应为 **polyfills**，是帮助老浏览器跟上 h5 步伐的插件，叫 **h5 垫片**，用 js 提升老浏览器的 api，抹平浏览器间的差异。所以 mOxie 其实是个通用前端库。\n* 中级，plupload.full.min.js，插件大小123k\n      打开它看一下，发现它其实是 moxie.min.js 和一个叫 plupload.min.js 的文件合并到一起而已。\n      所以 `plupload` 其实是在 mOxie 的基础上，封装了一下文件上传 api，专业文件上传前端库。\n* 高级，它依赖\n      jquery       137k\n      jquery ui    282k\n      plupload     123k\n      plupload ui  30k\n      一共约600k的大小。帮助你实现 ui，叫 widget － 小组件。\n* 巨级，它和高级差不多，也是实现一套 ui。区别是 ui 是队列，前者的 ui 是块和列表。\n\n那么回过头，再来看这个例子。这个例子只是演示文件选择，它没有上传的功能。\n只有文件选择功能的 `mOxie` 插件的大小为77k，比正常功能要小30%。为什么呢？\n\n因为 `mOxie` 是一个可以自定义的前端库，如果有些功能不需要，比如 silverlight，那么就可以不把它们编到目标中。 参见 [编译 mOxie](docs/compile.md)\n\n那么 `mOxie` 都做了什么呢，为甚么有77k这么大（大吗？）的体积。它提供文件预览功能、图片压缩功能、国际化支持（就是 i18n ）等。同时，上面也提到，它解决浏览器的兼容性问题。\n\n## \u003ca name=\"demo4\"\u003e\u003c/a\u003e4、mOxie文件上传，进度提示\n\n这个例子只使用 `mOxie` 提供的功能，实现了**文件上传**。\n\n~~~\n$ ls -l demo[3-4]/moxie.min.js\n-rw-r--r-- ktont  staff  73499  13:53 demo3/moxie.min.js\n-rw-r--r-- ktont  staff  77782  13:58 demo4/moxie.min.js\n~~~\n您会发现，本例中的 `mOxie` 库比上一例多了4k，那是因为在编译的时候加入了 XMLHttpRequest 的支持。\n所以 demo4 中的 moxie.min.js 就是 `plupload` 库能投入生产的最精简版本。参见 [编译 mOxie](docs/compile.md)\n\n您可以在这个 demo 的基础上实现自己的文件上传。相比 `Plupload API`，它更灵活，您可能更喜欢在这个**层次**上编写应用。当然，灵活性的对立面是复杂度，它们之间的平衡点因人而异。\n\n### 怎么判断文件上传成功？\n\n- 无论上传成功或者失败，结束时都会触发 loadend 事件\n- 如果上传过程中服务器崩溃，那么 loadend 事件的附加值 info.status 为 0。也可以根据 xhr.status 为 0 \n- 如果上传过程中用户 abort，那么结果将依赖于服务器的处理。服务器有可能返回成功也可能返回失败。\n- 如果上传过程中网络中断或者超时，那么 那么 loadend 事件的附加值 info.status 为 0。也可以根据 xhr.status 为 0 \n- 如果服务器返回的值 info.status == 200，则说明上传成功。否则，失败\n\nNOTE: 关于 abort\n\n这似乎是一个bug，因为当用户主动 abort 时，并没有触发 abort 事件。\n所以你要想正确的 abort 那么就要注意了。手工 abort() 后，要标记失败了。\n最好用 Promise，abort() 后，立即返回 reject()\n\n## \u003ca name=\"demo5\"\u003e\u003c/a\u003e5、使用plupload实现了图片上传\n\n这个例子，比较实际一点，使用 `Plupload API`。`Plupload API` 主要在 `mOxie` 上实现一套事件驱动的机制。\n\n同时，顺带演习上传的暂停和重传。为甚么在这里演习暂停和重传呢？\n\n为了区分下个例子 -- 断点续传。断点续传是指，**重启了电脑后断点续传**。\n\n断点续传在上传大文件的场景下，很有用。\n比如我上传一个电影，中间关闭了电脑，然后睡个觉。醒来后可以继续传。\n\n下一个例子演示断点续传。\n\n而本例的重传是说，不重启浏览器的前提下，重新传文件。它会从头再来，之前传的会丢弃。\n实际场景中，用来重传图片这种小文件。\n因为小文件一个封包或几个封包就发送完了，没必要断点续传，也没法儿**断**了。\n\n## \u003ca name=\"demo6\"\u003e\u003c/a\u003e6、断点续传  \n\n是时候请出你的硬盘女神啦！运行本程序需要一个大文件，而电影文件再合适不过了。\n\n\u003cimg src=\"img/6.1.png\" width=\"500\" /\u003e\n\n选取文件后，并没有立即上传。而是去服务器询问上次传输的断点。\n在本例中，服务端会返回一个50到100的随机值，它表示百分比，用来模拟实际情况中的**上次的断点**。\n\n例如，下面图片中，上次的断点是94%\n\n\u003cimg src=\"img/6.2.png\" width=\"500\" /\u003e\n\n你可能会误认为服务器会从94%的地方把数据存起来，不是的，\n它的意思是告诉客户端，**请从文件94%的地方把剩下的数据发送过来**。\n\n服务端的情况\n\n\u003cimg src=\"img/6.3.png\" width=\"500\" /\u003e\n\n\n本例中使用的**块**大小是1兆字节，这个配置在 `index.html` 的19行\n\u003e              chunk_size: '1mb'\n\n上图中，两个绿色框之间是一次独立的 **http 交互过程**，它用来发送一个**块**。\n本例中的文件一共4G多，会切成4千多个块。产生4千多次 http 交互来发送它们。\n相比不分块而一次 http 发送完所有数据，这么做会有些网络性能损耗。但是不分块的缺点是非常明显的。\n\n如果真的不分块，单 http 发送所有数据。假设网络异常，服务端 hanging，客户端此时开启另一个链接 retry。\nretry 首先询问服务端**上次的断点**，然后从该断点处继续发送。\n之前 hanging 的链接可能已经 hang up，也可能没有，这取决于服务端的超时时间。\n\n此时，服务端就会面临一个尴尬的选择，必须关闭之前 hanging 的链接\n。因为如果不关闭，网络中残留的数据可能继续写入文件，导致数据错乱。 \n服务端一般请求间是无法操作的，一个请求不能操作其它请求。\n\n\u003cimg src=\"img/6.4.jpg\" width=\"500\" /\u003e\n\n虽然，实际上几乎不会出现上面的情况，但是它不严谨。并且，\nhttp 协议是一个应用层协议。http 协议在 **application** 和 **network transfer** 更靠近 **application**。大多数 http 服务器都会帮你做封包的拼解工作，而让你从网络层传输层解放出来。如果达不到这一点，http 的处理还是和 tcp 一样麻烦，那 http 就不应该存在。[参考 http协议](https://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html)\n\n然而，如果分块来传输，就不会遇到这个问题。如果链接 hang up。那么整个请求的数据统统丢弃，偏移仍然在当前**块**。\n\n话说回来，所以要把文件数据拆分成一个较小的单元来用 http 传输，并且\n* 用块发送可以降低 token 冲突的概率。上传文件是使用一个随机 token 来标记数据边界(第一个绿框)的。\n当文件大的时候，会有可能遇到和 token 一样的字符串。但是，分块传，会每次都换一个 token。\n* 适当的**块**大小，有助于浏览器读取文件。比如本例中 chrome 用的是 slice 读取文件，我们不能指望它很智能，塞给它一个很大的文件，让它很好的处理。有些浏览器对文件大小有限制，甚至在传大文件的时候会卡死。\n\n上图中，红色的框表示当前传输的是**第几块**数据。因为服务端给了随机值94%，所以这里是4261的尾部 -- 4005。\n\n黄色的框表示一共有多少块数据。当红色和黄色相等的时候，表示文件传输完成。\n\n灰色的框表示传输的二进制数据，数据的边界由第一个绿框定义。这个时候，这次 http 交互就完成了，链接会被关闭。紧接着会是下一块数据，一个全新的 http 交互，token 也会是一个新的。\n\n断点续传的关键在于 －－**从文件的指定偏移处读取** (__ZHUANGBI__: c语言中 fseek)\n\n但是浏览器提供给前端的功能都是受限的，没有 `fseek`，而是提供了一个 `slice` 功能。\n比如，`slice(off, off+1024)` 用来读取 off 处的1024字节数据。\n还能凑合着用吧，那我们每次读一块数据，然后发送，再读下一块，再发送。。。\n\n突然发现，这不就是失传已久的 **socket 编程** 吗？搞一个 **缓冲**，撸一串数据然后发出去，再撸一串数据再发出去。\n\n好吧！幸亏不是让我们写这种恶心的数据解析工作，`plupload` 已经给我们写好了，我刚撸起的袖管赶紧放了下去。\n\n\n## \u003ca name=\"demo7\"\u003e\u003c/a\u003e7、plupload ui widget的示例\n\n这个例子，用来展示 plupload 的 `UI Widget`。\n\n\u003cimg src=\"img/7.png\" width=\"600\" /\u003e\n\n在 index.html 中，ui 部分只需安置一个 div\n\u003e    \u003cdiv id=\"uploader\"\u003e\u003c/div\u003e    \n\nplupload 会在这个 div 中，自动**安插**一个 ui 组件，就是图片中展示的那个。\n\n这样极大的方便了开发，你可能一句 js 都没写，就实现了复杂的图片上传。\n当然，你可以定制这个组件，那样需要一些学习成本，并且挺高的。所以，如果你想要一个轻量，自定义的 ui 组件的时候，就需要自己设计 ui 了。\n比如下面这样的\n\n\u003cimg src=\"img/7.1.png\" width=\"300\" /\u003e\n\n在上面这个组件中，要求\n\n* 最多只能选n张\n* 更好的用户体验 \n* 可以删除节点\n* 上传失败后可以重传\n* 页面大小控制在100k以内\n\n## \u003ca name=\"demo8\"\u003e\u003c/a\u003e8、nodejs上传显示进度、服务端限速\n\n本示例研究了两个小技术\n* nodejs上传显示进度 \n* 服务端限速\n\n```\nnode demo8/client.js\n得找一个大文件才能看出效果\n```\n\n这两个小技术，用 stream 就能搞定。\n\nnodejs上传显示进度，用 readable stream 的 data 事件\n服务端限速 用 readable stream 的 once() pause() resume()，实现有些精巧，自己去看吧。它每秒限定 64k。调整 timer 可以实现任意速率限速。\n\n另外，demo8/client.js 研究了 github.com/request/request 库如何 abort 一次上传\n\n在控制台直接输入几个回车，然后输入 abort ，就会 abort 这次上传。如图：\n\n\u003cimg src=\"img/8.png\" width=\"400\"\u003e\n\n## \u003ca name=\"demo9\"\u003e\u003c/a\u003e9、FAQ 批量上传的时候，如何为每个文件添加一个id\n\n这是个真实的案例。\n\n有人问我如何实现下面两点：\n- 批量上传的时候，如何为每个文件添加一个uuid\n- 如果文件小于 10M，则没有必要分块传输\n\n这两个问题是同一个问题。答案很简单。\n\n使用 plupload 的 BeforeUpload 事件 和 setOption 方法\n\n\u003cimg src=\"img/9.png\" width=\"400\"\u003e\n\n## \u003ca name=\"end\"\u003e\u003c/a\u003e总结\n\n这篇文章好像变成了对 plupload 的讲解。\n\n其实，我的初衷是探讨 web 环境下文件上传。\n\n但是，在随后2年的开发中，我又开发了桌面端，服务端的文件上传和下载。它们\n的相关经验也记录在这里了。 demo8 就是这时候产生的。\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktont%2Fjavascript-file-upload","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fktont%2Fjavascript-file-upload","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fktont%2Fjavascript-file-upload/lists"}