本教程包含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 的示例
總結
首先,來看個例子。它是用原生的文件提交方法,前端只有一段 HTML 而沒有 JS。我們的目的是觀察 http 協議。
前端 index.html,使用一個 input 標簽進行文件選擇,然后使用 form 表單發送數據。
后端 server.js,對表單發過來的數據進行解析,把協議格式打印出來。
點擊“選擇文件”后:
在點擊 “Upload” 按鈕之前,對網絡進行限速,方便觀察數據傳輸的過程。打開 Debugger“

點擊后,選取一個較慢的:

服務端會打印下面的提示,注意紅框中的 token,它用來表示二進制數據的邊界。

你在 server.js 中可以看到解析 http 數據的 formidable 函數。
可以調試它,用來學習 http 協議。
上傳完成后:

TIP: 觀察,它是我們本次學習之旅的主要方法。
你一定要運行每個例子,看到它們運行,觀察它們的行為。
這樣,就熟悉了這個技術。
plupload 是一個文件上傳的前端插件。
主頁、Github 地址
demo2 并沒有使用 plupload,事實上它是自己實現了 plupload,它本身就相當于 plupload 的 v0.01 版本。
通過 v0.01,這20行代碼來一窺 plupload 的原理。而不是去讀 plupload 的上萬行代碼,
真是,兩岸猿聲啼不住,輕舟已過萬重山,一日千里。
plupload 的原理,就是拿到文件句柄后,自己發送(XMLHttpRequest)文件。
盡量控制整個過程,從中加入自己實現的功能,這就是它的想法。
這些操作,都有個前提,就是要拿到文件。否則,一切無從做起。
這個例子沒有服務端,請直接用瀏覽器打開 demo3/index.html。然后選取圖片,就可以看到預覽。
這樣避免你想當然的認為,預覽是服務端輔助的。

文件預覽一般的做法是,先上傳圖片,然后從圖片服務器上下載 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 一節)。 plupload 其實是在 mOxie 的基礎上,封裝了一下文件上傳 api,專業文件上傳前端庫。
jquery 137k
jquery ui 282k
plupload 123k
plupload ui 30k
那么回過頭,再來看這個例子。這個例子只是演示文件選擇,它沒有上傳的功能。
只有文件選擇功能的 mOxie 插件的大小為77k,比正常功能要小30%。為什么呢?
因為 mOxie 是一個可以自定義的前端庫,如果有些功能不需要,比如 silverlight,那么就可以不把它們編到目標中。 參見 編譯 mOxie
那么 mOxie 都做了什么呢,為甚么有77k這么大(大嗎?)的體積。它提供文件預覽功能、圖片壓縮功能、國際化支持(就是 i18n )等。同時,上面也提到,它解決瀏覽器的兼容性問題。
這個例子只使用 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,它更靈活,您可能更喜歡在這個層次上編寫應用。當然,靈活性的對立面是復雜度,它們之間的平衡點因人而異。
這個例子,比較實際一點,使用 Plupload API。Plupload API 主要在 mOxie 上實現一套事件驅動的機制。
同時,順帶演習上傳的暫停和重傳。為甚么在這里演習暫停和重傳呢?
為了區分下個例子 – 斷點續傳。斷點續傳是指,重啟了電腦后斷點續傳。
斷點續傳在上傳大文件的場景下,很有用。
比如我上傳一個電影,中間關閉了電腦,然后睡個覺。醒來后可以繼續傳。
下一個例子演示斷點續傳。
而本例的重傳是說,不重啟瀏覽器的前提下,重新傳文件。它會從頭再來,之前傳的會丟棄。
實際場景中,用來重傳圖片這種小文件。
因為小文件一個封包或幾個封包就發送完了,沒必要斷點續傳,也沒法兒斷了。
大炮不適合打蚊子,因為蚊子小(我怎么這么啰嗦——)
是時候請出你的硬盤女神啦!運行本程序需要一個大文件,而電影文件再合適不過了。

選取文件后,并沒有立即上傳。而是去服務器詢問上次傳輸的斷點。
在本例中,服務端會返回一個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 已經給我們寫好了,我剛擼起的袖管趕緊放了下去。
這個例子,用來展示 plupload 的 UI Widget。

在 index.html 中,ui 部分只需安置一個 div
plupload 會在這個 div 中,自動安插一個 ui 組件,就是圖片中展示的那個。
這樣極大的方便了開發,你可能一句 js 都沒寫,就實現了復雜的圖片上傳。
當然,你可以定制這個組件,那樣需要一些學習成本,并且挺高的。所以,如果你想要一個輕量,自定義的 ui 組件的時候,就需要自己設計 ui 了。
比如下面這樣的

在上面這個組件中,要求
在生活中,圖片上傳的應用越來越廣泛。特別是在智能手機普及以后,獲得圖片很便捷,圖片的質量也很高。
比如,在一個突發事件中,人們能很及時的從各個角度拍到它,然后分享到朋友圈或者上傳到網上。
在移動端的開發中,因為手機的特點,它處理能力弱、展示空間小,導致圖片上傳技術有些困難。另外,
站在運營商角度,從長遠來看,手機流量會是主要的營收服務。雖然流量成本不高,但它不會便宜。這樣,用戶就會在乎自己的流量。
還有,在實際應用中,需要后端配合搭建圖片服務器、圖片數據庫,前端還要解決跨域的問題,雖然本文沒講這些,但實際也要開發者解決。所以,編寫一個省流量又好用的圖片上傳程序是一個挑戰。
另一方面,隨著瀏覽器和web技術的持續推進。傳統的大文件上傳,勢必會轉到 web 上來實現,而不是一些桌面 app。這里面甚至還蘊含一些商機。
這還需要一些時間,因為我知道很多人還在用 xp,但趨勢是明確的。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。