1. 程式人生 > >XMLHttpRequest2.0的實踐之路--山寨某雲盤

XMLHttpRequest2.0的實踐之路--山寨某雲盤

  最近使用雲盤的頻率比較高,用久了就開始琢磨它相關功能的實現,碰巧最近在拜讀一本關於設計模式的書籍,上面也提到了*雲裡面對於上傳下載的一些實現,於是乎自己動手山寨了一下,然後今天寫下這篇文章記錄一下。

效果圖:
這裡寫圖片描述
使用的技術棧:
    前端:html.css,js,jquery.
    後端:nodejs(koa)
    外掛:ImageMagick-6.2.7-6-Q16-windows-dll.exe;ffmpeg

  一看圖就知道確實是山寨,不過我的目的是將知識串起來,探索通過web頁面來管理遠端檔案的解決方案,僅此而已,希望如果有內行的朋友看見了別笑話,方便的話可以留下以寶貴的見解。好了,不講這麼多虛的,開始進入正題.

目錄

  1. 山寨【*雲】的檔案上傳下載功能
    1.1 .檔案列表
    1.2 .上傳檔案
    1.3 .下載檔案
    1.4 .新建資料夾
    1.5 .刪除檔案及資料夾
  2. 圖片處理模組gm和視訊處理模組ffmpeg
    2.1 gm
    2.2 ffmpeg
  3. XMLHttpRequest2.0新功能
    3.1 HTTP請求的時限
    3.2 支援跨域請求
    3.3 支援上傳FormData物件
    3.4 支援接受二進位制檔案
    3.5 獲取檔案傳輸的進度資訊

一 山寨【*雲】的檔案上傳下載功能

  山寨*雲主要是受到各種雲盤的影響,於是乎就突然有這麼一個想法,然後就是幹,砸門程式設計師就時這麼簡單。其次就是複習一下XHR的相關知識,也只有在實際動手中才會對他的使用場景更加熟悉,同時在腦海中形成一套比較完善的解決方案,將來有需要的時候,才能即使即插即用。下面講講主要實現的功能:

  1. 檔案列表
  2. 上傳檔案
  3. 下載檔案
  4. 新建資料夾
  5. 刪除檔案及資料夾

下面介紹一下我實現的功能以及一些功能點的實現思路。

1.1 檔案列表

檔案列表
   檔案列表實現功能

  1. 按照目錄層級關係,顯示對應目錄下的檔案列表
  2. 顯示不同檔案型別的縮圖,檔案大小,以及最近一次的修改事件
  3. 檔案複選框操作(主要是為下載和刪除用的)

1.2 上傳檔案

檔案上傳
   檔案上傳主實現功能:

  1. 上傳進度條
  2. 上傳內容成功後在檔案列表的回顯
  3. 針對大檔案傳輸進行了處理,在進行大檔案傳輸的時候利用了XRH2可以對檔案物件進行slice處理;然後將分好的資料塊兒一塊兒一塊兒按順序上傳。這樣可以比較方便的實現暫停和恢復下載,儘管現在還沒下載。這樣如果再上傳過程成意外中斷,也可以即使恢復,不然要是再上傳一個1G的檔案時,上傳到95%的時候中斷了,那就sky!sky!了。
  4. 針對視訊和圖片,服務端在判斷上傳的檔案型別為圖片時使用圖片界的軍刀gm生成圖片的縮圖,如果上傳的檔案是視訊就採用ffmpeg生成視訊檔案首幀的縮圖。
    注意上面提到的這兩個外掛是需要在宿主機上安裝的,後面我會介紹一下安裝,以及在過程中可能會遇到的一些問題以及解決方式。

  程式碼只是一部分,主要是結合程式碼分析一下實現。

1.3 下載檔案

這裡寫圖片描述

  主要實現功能

  1. 下載進度條,
    這裡解釋一下,其實在web端我們很少看見有下載進度條,因為瀏覽器有預設的下載進度條,不需要我們在前端自己去實現,往往實現進度條的需求主要在客戶端應用,我主要是想嚐嚐鮮。
    實現主要用到了xhr的process事件,然後根據已經下載的的資料大小(loaded)和檔案的總大小來計算下載的進度.這需要注意在下載大檔案的時候需要將req.responseType 設定成 “blob”,不然會造成瀏覽器假死,分析原因就是大檔案佔用的記憶體將系統分配給瀏覽器的記憶體榨乾了,一般32位的瀏覽記憶體大小在512M左右,64位的有1G左右,當然這裡有一個特別的主,就是IE,IE的記憶體比一般的瀏覽器都小,這也是IE經常會出現假死的原因之一。然後回到req.responseType=”blob”;這是xhr2新增的型別,他的宣告是告訴瀏覽器接下來要接受的檔案是以檔案流的形傳來的,瀏覽器看見這個型別的時候就不會使用自己的記憶體來存放資料,而是使用系統的記憶體在存放資料,這樣只要系統記憶體是足夠的,下載大檔案的時候就不會有問題。

1.4 新建資料夾

實現功能:

  1. 新建資料夾
  2. 資料夾重新命名

    這兩個功能沒什麼特別,主要就是將需要新增的資料夾名字和在哪裡木下新增的告訴服務,然後後臺在收到請求的時候在對應的目錄下生產對應的資料夾,然後將新生成的資料夾資訊返回給前端,前端重新整理列表。

1.5 刪除檔案

實現功能:

  1. 刪除檔案
  2. 刪除資料夾
      刪除資料夾的時候後端需要使用遞迴的方式去刪除資料夾裡面的內容最後在刪除該資料夾

二 圖片處理gm模組和視訊處理模組ffmpeg

2.1 gm模組

   gm是一個nodejs模組,用它對圖片在伺服器端進行縮放裁切。使用gm模組還需要在宿主機上安裝應用程式ImageMagick,號稱圖片界的瑞士軍刀。我是在windows上跑的服務,所以安裝的是ImageMagick-6.2.7-6-Q16-windows-dll.exe,這裡建議安裝6.xxxx的版本,我安裝7.xx時,在使用nodejs呼叫gm模組的方法時會出現檔案路徑亂碼,然後切換到6.xx的版本,重啟一下機器,一定要重啟一下。然後就ok了。

 function uploadIsImage(filePath,ext){
            return new Promise((resolve,reject)=>{
                let surports = [".png",".jpg"];
                if(surports.indexOf(fileExt)!=-1){
                    //判斷是不是分段傳輸的最後一次上傳
                    if(current<total){
                        resolve({"thumbPath:":""});
                    }
                    let thumbPath =path.join(config.thumbPath,path.basename(filePath,path.extname(filePath)))+"_1.jpg";
                    gm(filePath)
                    .resizeExact(40,40)
                    .write(thumbPath,function(err){
                        if(err){
                            console.log("err:",err);
                            resolve({thumbPath:""})
                        }
                        let url = path.join(config.staticPath,path.basename(filePath,path.extname(filePath))+"_1.jpg");
                        resolve({"thumbPath:":url});
                    });

                }else{
                    resolve("next")
                }
            })

        }

  具體操作去看gm包裡面的readme.md檔案就ok了。這裡主要解決的就是檔案路徑亂碼問題,然後見按照demo來就好了。

2.2 ffmpeg模組

   這是一個nodejs的視訊處理模組,同樣需要安裝,這個和前面提到的ImageMagick不同的是,在官網上下載下來的安裝包不是一個可執行安裝檔案,需要手動的去配置環境變數
這裡寫圖片描述
然後就可以通過nodejs使用ffmpeg模組了

 //視訊
        function uploadIsVideo(url,ext){
            return new Promise((resolve,reject)=>{
                let surports = [".avi",".mp4"];
                if(surports.indexOf(ext)!=-1){
                    //判斷是不是分段傳輸的最後一次上傳
                    if(current<total){
                        resolve({"thumbPath:":""});
                    }else{
                        try {
                            let basename = path.basename(url)
                            let process = new ffmpeg(url);
                            process.then(function (video) {
                                let thumbFilePath = basename;
                                video.fnExtractFrameToJPG(config.thumbPath, {
                                    frame_rate : 1,
                                    number :1,
                                    size:"40x40",
                                    file_name : basename
                                }, function (error, files) {
                                    if (error){
                                        return;
                                    }
                                    let file = files[0].split("thumb")[1];
                                    let returnPath = path.join(config.staticPath,file)
                                    resolve({thumbPath:returnPath});
                                });
                            }, function (err) {
                                resolve({thumbPath:""});
                            });
                        } catch (e) {
                            resolve({thumbPath:""});
                        }
                    }  
                }else{
                    resolve("next")
                }
            })

  同樣可以通過ffmpeg模組下的readme.md檔案瞭解模組是使用方式。已釋出的npm模組都市有這個檔案的,該檔案介紹了模組的使用方式。

  前面主要介紹了一下我實現的一些功能點,沒有深入的分析程式碼,有需要的話可以去https://github.com/839305939wang/weiCloud看看,由於我沒有使用到資料庫儲存檔案資訊,所以所有資訊都是返回檔案列表的時候一次性全部返回的,然後前端程式碼還進行了一些處理,所以這一塊看起開會很亂。後期要是有時間,我把資料加進來,這樣可能程式碼會精簡很多。
  說了實現,最後來回憶一下XHR2.0

三 XMLHttpRequest2.0新功能

  XMLHttpRequest是前端和後端進行資料非同步互動的基礎,隨著現在低版本瀏覽器的逐步退出市場以及各種前端框架的普及,出現了大量已經將XMLHttpRequest封裝好了的外掛,前有dom操作神器jQuery,後有現在的axios,request…。這些讓我們逐步忘記了還有XMLHttpRequest這給個大哥。但是這並不代表我們沒有必要去了解他了,因為最原始的東西往往能擦出更激烈的火花,做出使用者體驗更好的的需求,我們也不用去生搬硬套那些外掛。應為外掛在設計的時候考慮的是大眾需求,不可能放方面都設計到。所以對於XMLHttpRequest我們還是很有必要了解的,似乎也是各大公司招聘必問的內容。
  這裡不詳細介紹XMLHttpRequest,如果有需要可以去W3school上去看看,這裡面介紹的很詳細的,這裡我們主要介紹一下XMLHttpRequest2.0針對XMLHttpRequest有了那些新增,這些新增的功能能幫我們實現什麼需求,講到到需求我就想起了前兩天網路上流傳的開發人員和產品經理幹架的段子,從這個例子中我們可以發現,只有不斷提高自己的技能才有可能滿足產品經理的某些變態需求。扯遠了,我們言歸正傳。下面羅列一下新增的功能:

1. 設定HTTP請求的時限;

2. 請求不同域名下的資料(跨域請求);

3. 使用FormData物件管理表單資料;

4.  獲取伺服器端的二進位制資料。

5. 獲取資料傳輸的進度資訊。

3.1 HTTP請求的時限

  http請求響應時間是不確定的,有的時候可能會需要很長的事件,如果這個請求由於某種原因一直沒有返回,如果這種請求過多,一方面可能會導致頁面的其他正常請求處於pendding狀態,另一方面,也可能導致伺服器由於tcp連結過多造成伺服器資源的大量消耗。所以給請求設定超時時間是非常有必要的,在XMLHttpRequest2.0中我們可以通過像下面這樣設定請超時間,當請求超時時,會主動觸發超時事件,我們在事件裡面可以做一些友好的處理.

xhr.timeout = 5000;//毫秒數
xhr.ontimeout = function(event){

    alert('請求超時');

}

  這裡需要注意一點,所有的事件和設定都是需要在xhr.open(…)執行之後在設定,不然設定是無效的.

3.2 支援跨域請求

  說到跨域請求,我們腦子裡面可能最先想到的是jsonp,現在XMLHttpRequest2.0也支援了,像下面這樣:

xhr.open('GET', 'http://other.server/getCors');

  這看起來和我們以前的請求沒有什麼區別,但是XMLHttpRequest2.0請求跨域資源需要伺服器端的配置,服務端在返回資源的時候需要指定支援跨域請求的URI,例如:

ctx.set({
   "Access-Control-Allow-Origin":"http://other.server/getCors"
})

  如果所有的請求都支援,則:

ctx.set({
   "Access-Control-Allow-Origin":"*"
})

3.3 支援上傳FormData物件

  通常在上傳表單物件的時候,通過form標籤來實現,例如:

  <form id="loginForm" action="/uploadForm" method="POST" enctype="application/x-www-form-urlencoded">
     <input type="text" name="usename" value="" placeholder="請輸輸入使用者名稱"/>
     <input type="password" name="passw" value="" placeholder="請輸輸入密碼"/>
     <input type="submit" value="提交" id="submitBtn"/>
  </form>

  一種是直接通過點選提交按鈕同步提交,但是這種方式一般使用的比較少;另外一種就是非同步提交,

function submit(){
    document.forms[0].submit();
}

  通過檢視http請求,我們可以看見表單資料被封裝成一個FormData物件被傳輸出去了
這裡寫圖片描述

  現在處理通過這種直接使用from標籤來上傳FromData資料外,HttpXMLRequest也可以上傳FormData資料,像下面這樣:

var form = new FormData();
          form.append("username","zxj");
          form.append("password",123456);
          var req = new XMLHttpRequest();
          req.open("post", "${pageContext.request.contextPath}/public/testupload", false);
          req.send(form);

3.4 支援接收二進位制檔案

  通過檢視HttpXMLRequest介面文件我們可以發現目前ajax請求的響應型別(ResponseType)支援的格式為:

    ""  DOMString (this is the default value)
    "arraybuffer"   ArrayBuffer
    "blob"  Blob
    "document"  Document
    "json"  JavaScript object, parsed from a JSON string returned by the server
    "text"  DOMString

  在HttpXMLRequest2.0中新增了arraybuffer,blob兩種二進位制型別

  ArrayBuffer代表接受的響應主體是二進位制陣列:

req.responseType="arraybuffer";
req.onreadystatechange = function () {
         if (req.readyState === 4 && req.status === 200) {
             var arrayBuffer = req.response;
         }
     };

  這樣我們就能接收到伺服器傳來的二進位制檔案了;這裡需要注意在指定了響應頭為二進位制資料的時候只能通過req.response來獲取響應體,訪問req.responseText會丟擲一個錯誤

這裡寫圖片描述

  blob:表示一個不可變的, 檔案物件,裡面可以儲存大量的二進位制編碼格式的資料

req.responseType = "blob";

  指定req.responseType==”blob”後請求響應型別必須是檔案流
這裡寫圖片描述
  content-Type為application/octet-stream.這種使用場景一般在進行大檔案傳輸的時候使用,服務端通過管道的形式獲取檔案流,然後再將該檔案流作為響應返回客戶端

 let fileReaderStream = fs.createReadStream(url);
            this.ctx.body = fileReaderStream.on('error',this.ctx.onerror).pipe(PassThrough());

3.5 獲取檔案傳輸的進度資訊
  雲盤之類的應用上傳或者下載檔案的時候往往都伴隨這個一個進度條,這樣可以給使用者更好的體驗,在httpXMLrequest中也實現了檔案上傳和下載的進度事件,我們可以監聽相關操作的process事件來獲取進度,上傳和下載進度事件是有區別的

上傳進度事件監聽:

req.upload.addEventListener("progress",function(evt){
 //通過evt.loaded來判斷檔案載入多少了
})

  這裡需要注意,通過測試發現,這裡的上傳進度其實不是指檔案成功上傳到伺服器的的進度,而是讀取本地檔案的進度,即使你上傳失敗,這裡的事件依然會觸發,直到檔案全部讀取.所以這可以用來實現檔案載入進度條或檔案掃描

下載進度事件監聽

req.addEventListener("progress",function(evt){
//通過evt.loaded來判斷檔案下載多少了
})

  這裡需要注意的是通過流獲取檔案的方式Content-Length的值不是檔案的總大小,我們可以再響應頭裡面返回檔案的總大小.這樣方便計算檔案下載進度.

總結

實踐出真知,只有動手操作,才能將書本上和別人分享知識變成自己的,同時提高自己的編碼和程式碼組織能力。