(轉載)jquery分片上傳影片到php

白小二發表於2023-05-03

寫的好啊,怕沒了,先轉過來,實踐了,寫的很清晰,實踐性強,稍微做了點改動,影片圖片做不同切片大小,結尾加了小彩蛋,使用佇列控制上傳進度實現了超大檔案上傳,實測 1g 影片上傳,理論大小估計跟瀏覽器限制有關

檔案/大檔案上傳功能實現(JS+PHP)


參考博文:掘金-橙紅年代 前端大檔案上傳

路漫漫 其修遠 PHP + JS 實現大檔案分割上傳

本文是學習檔案上傳後的學習總結文章,從無到有實現檔案上傳功能,前端小白寫的程式碼不是最優,如果有錯誤的地方請多多指教,如果本文對你有所幫助,深感榮幸。

近期公司的專案中,涉及到上傳大檔案的問題,大檔案上傳用普通表單上傳時出現的問題是,無法斷點續存,一但中途中斷上傳,就要重頭開始,這很明顯不是我們想要的,所以經過一番查詢,學習了一下大檔案分割上傳的方法。並且使用簡單的php做服務端處理程式實現一個功能demo,供以後回顧使用。本人也是初出茅廬的前端小白,記錄下各種功能的實現總結,程式碼有錯誤的地方,請多多指正。

1.簡單檔案上傳
普通表單上傳
表單上傳是我們經常使用的功能,而且使用起來也是非常簡單,我們只需要宣告表單內容型別為enctype=”multipart/form-data”,表明表單上傳檔案的二進位制資料。

 <form action="index.php" method="post" enctype="multipart/form-data">
   <input type="file" name="myfile" />
   <input type="submit" value="上傳" />
 </form>

點選上傳按鈕,就可以將表單傳送到伺服器,並使用index.php接受到對應的表單資料,存入$_GET/$_POST超級全域性變數中,我們只需要使用move_uploaded_file方法,將接收到的檔案資料,儲存起來,就實現了檔案上傳功能了。

 $myfile = $_FILES['myfile'];
 //上傳路徑
 $path = "upload/" . $myfile['name']; 
 if(move_uploaded_file($myfile['tmp_name'], $path)){
   echo "上傳成功";
 } else{
   echo "上傳失敗";
 };

ajax模擬表單上傳檔案
當我們有需求,需要非同步提交表單或者需要對上傳檔案做一定修改(例如:裁剪尺寸)時,普通的表單上傳就不能滿足我們的需求,因為我們無法修改表單的file值,這時候就需要ajax出場了。這裡我們使用jQuery使用ajax更方便快捷。

我們需要做如下修改:

HTML
我們不需要配置form,只需要配置相應的ID,用於獲取DOM元素物件。

 <form id="myForm">
   <input type="file" name="myfile" id="myFile" />
   <input type="submit" value="上傳" id="submitForm"/>
 </form>
 <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>

JQuery
注意,jQuery的ajax方法,會預設配置一些請求資訊,所以我們需要重新配置放置jQuery的預設行為導致資料格式或請求頭資訊出現問題。

這裡的contentType和processData為必須項。

 $('#submitForm').on('click', function(e){
   // 阻止預設表單提交
   e.preventDefault();// 建立表單
   // 預設配置了enctype="multipart/form-data"
   var formData = new FormData();
   formData.append('myfile',$('#myFile')[0].files[0])// 提交表單
   $.ajax({
     type: "POST",
     url: 'post.php',
     data: formData,
     // 阻止jquery賦予預設屬性,使用FormData預設配置enctype="multipart/form-data"
     contentType: false,
     // 阻止jquery自動序列化資料
     processData: false,
     success: function(data){
       console.log('請求正常',data);
     }
   })
 })

2.大檔案分割上傳
簡單上傳痛點
簡單上傳,使用表單提交檔案到伺服器時,如果網路不好或者中途中斷,會使檔案上傳失敗,試想一下如果要上傳檔案很大,當你上傳到99%時,突然間中斷,又要重新上傳,那該有多崩潰,那時你可能電腦的想砸了。

實現思路
大檔案上傳,實現的方法,就是將上傳檔案的二進位制檔案透過分割的形式,逐個上傳到伺服器,在上傳完成後,伺服器再對檔案進行拼接操作。

為了能識別上傳的資料,是哪個檔案,我們必須要擁有一個檔案識別符號,用於識別接收到的檔案資料是屬於哪個檔案的,以及可以實現避免重複上傳,實現秒傳功能等。

不要忘記由於是非同步操作,而且操作的資料段大小不一,會導致整合時無法確認拼接熟悉怒,所以我們需要一個index標識資料段的位置。

透過初步整理,我們就需要以下的引數

檔案唯一識別符號

分割後資料段

分割資料段的順序索引值

經過思考,我們可以建立兩個處理程式,來分別處理接受chunk資料段和合並chunk資料段。

file_getchunk.php

功能:將分割chunk資料,整理並儲存,此處我們用檔案形式實現。

file_integration.php

功能:接收到整合通知,將資料段拼接,並生成檔案。

整體流程大致如圖:

PHP.ini配置
由於PHP預設配合中,限制了POST與上傳的大小,所以我們為了測試,需要修改php.ini中的預設配置。

post_max_size = 2048M
upload_max_filesize = 2048M

talk is cheap,show me the code
HTML

 <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
 <form id="myForm">
   <input type="file" name="myfile" id="myFile" />
   <input type="submit" value="上傳" id="submitForm"/>
 </form>

JQuery
獲取檔案物件,檔案識別符號,分割檔案,透過ajax傳送切割好的blob資料段。

    // 切片上傳
    $('#submitForm').on('click', function(e) {
        // 阻止預設表單提交
        e.preventDefault();

        // 獲取屬性
        var myfile = $('#myFile')[0].files[0];
        var ext = myfile.name.split('.').pop();
        // 判斷ext是否為影片
        var is_video = ['mp4', 'avi', 'rmvb', 'mkv'].indexOf(ext) > -1;
        var fileId = getFileIdentifier(myfile);
        // 資料切片
        var chunks = fileSlice(myfile, is_video);
        console.log(chunks)
        // 傳送分割資料段
        sendChunk(fileId, chunks);
    })

生成檔案唯一標識getFileIdentifier()

此處可以使用md5,生成檔案唯一的md5(相同檔案md5相同),作為識別符號。這裡只初略的處理了一下檔案標識。

   function getFileIdentifier(file){
     // 獲取檔案識別符號
     return file.size + file.name;
   }

分割方法fileSlice()

先將檔案使用blob檔案繼承的方法slice進行切割,生成blob字串。

    function fileSlice(file, is_video) {
     // 切片不宜過大,過大需要 nginx 以及 php 做相應配置
        var chunkSize = is_video ? 1024 * 1024 * 1 : 1024 * 1024 * 0.03;
        // 1.初始化資料
        var totalSize = file.size;
        var start = 0;
        var end = start + chunkSize;
        var chunks = [];
        // 2.使用bolb提供的slice方法切片
        while (start < totalSize) {
            if (is_video) {
                console.log('影片')
                var chunk = file.slice(start, end, 'video/mp4');
            } else {
                console.log('圖片')
                var chunk = file.slice(start, end);
            }
            chunks.push(chunk);
            start = end;
            end += chunkSize;
        }
        // 3.返回切片組chunk[]
        return chunks;
    }

傳送chunk方法sendChunk()

使用ajax依次傳送已經分割好的chunk,並提供對應的資料,請求file_getchunk.php進行處理。此處task列表,用於保證檔案分隔符全部已經完成上傳。

 function sendChunk(id, chunks){
   // 逐個提交
   // 用於保證ajax傳送完畢
   var task = [];
 ​
   chunks.forEach(function(chunk, index){
     var formData = new FormData();
     formData.append('fileId', id);
     formData.append('myFileChunk', chunk);
     formData.append('chunkIndex', index);
     $.ajax({
       type: "POST",
       url: 'file_getchunk.php',
       data: formData,
       contentType: false,
       processData: false,
       success: function(done){
         // 移除已完成任務
         task.pop();
         console.log(done,' 已完成');
         if (task.length === 0) {
           // 傳送完畢,整合檔案
           console.log('通知整合');
           makeFileIntegration(id, chunks.length);
         }
       }
     })
     task.push('file Working');
   })
 }

通知整合方法makeFileIntegration()

接收到整合通知,請求file_integration.php進行檔案的整合處理。


 function makeFileIntegration(id, size){
   // 通知已傳輸完成
   $.post(
     "file_integration.php",
     {
       id: id,
       size: size
     },
     function(data){
       console.log(data);
     }
   );
 }

PHP- file_getchunk.php

當PHP監聽到請求時,獲取對應的資料,生成資料夾,按照chunkIndex儲存資料段。

 if(!is_dir('upload')){
   mkdir('upload', 0777);
 }
 ​
 $chunk = $_FILES['myFileChunk'];
 // 檔案唯一標識
 $fileId = $_POST['fileId'];
 // 臨時資料夾名稱
 $length = strlen($fileId) - (strlen($fileId) - strpos($fileId, '.'));
 $filedir = substr($fileId, 0, $length);
 ​
 $chunkIndex = $_POST['chunkIndex'];
 ​
 $filepath = 'upload/' . $filedir;
 ​
 $filename = $filepath . '/' . $chunkIndex;if(!is_dir($filepath)){
   mkdir($filepath, 0777);
 }
 move_uploaded_file($chunk['tmp_name'], $filename);
 ​
 echo $chunkIndex;

PHP-file_integration.php

監聽到整合請求,對資料夾下面的所有檔案,進行依次拼接,並生成最終還原出來的檔案。


 $fileId = $_POST['id'];
 // 臨時資料夾名稱
 $length = strlen($fileId) - (strlen($fileId) - strpos($fileId, '.'));
 $filedir = substr($fileId, 0, $length);
 ​
 $size = $_POST['size'];
 $file = './upload/' . $fileId;// 建立最終檔案
 if(!file_exists($file)){
   // 最終檔案不存在,建立檔案
   $myfile = fopen($file, 'w+');
   fclose($myfile);
 } 
 // 用增加方式開啟最終檔案
 $myfile = fopen($file, 'a');for ($i = 0; $i < $size; $i++) {
   // 單檔案路徑
   $filePart = 'upload/' . $filedir . '/' . $i;if(file_exists($filePart)){
     $chunk = file_get_contents($filePart);
     // 寫入chunk
     fwrite($myfile, $chunk);
   } else{
     echo "缺少Part$i 檔案,請重新上傳";
     break;
   }
 }fclose($myfile);
 echo "整合完成";

3.更進一步
大檔案分割上傳功能已經基本實現,但是我們還可以擁有很多最佳化的地方

1.斷點續存。
我們需要的檔案已經可以正常的分割上傳,服務端也可以正常接收切片,完成資料段切片的合併了。此時我們就可以進一步實現斷點續存了。

斷點續存,實現方法很簡單,我們只需要獲取到上傳完成的資料段切片資訊,就可以判斷我們應該從哪個資料段開始繼續傳輸資料。

獲取已經完成資料段切片的資訊,我們可以使用前端儲存或者服務端獲取。此處我們使用服務端介面檢測,返回資料缺失位置來實現斷點續存。

思路整理
我們要在上傳前,請求服務端查詢出中斷時的位置,利用位置資訊,篩選上傳的資料段切片。

那麼我們要增加的邏輯就是:

offset中斷位置資訊

查詢中斷位置介面:file_get_breakpoint.php

實現
getFileBreakpoint()獲取檔案斷點函式

此處要保證ajax執行順序,才能正確獲取offset偏移量,實現思路有很多。此處只使用jquery提供的將ajax請求變為同步,進行處理。

注:同步請求時,success函式返回值不可以直接return,要儲存在一個變數中,在ajax請求外return才能生效。

 // 獲取檔案斷點
 function getFileBreakpoint(id, size){
   var offset = '';
   $.ajax({
     type:"post",
     url:"file_get_breakpoint.php",
     data: {
       id: id,
       size: size
     },
     async: false,
     success:function(res){
       offset = parseInt(res);
     }
   })
   return offset;
 }

在sendChunk()傳送資料前獲取offset

 // 上傳前,請求file_integration.php介面獲取資料段開始傳輸的位置
 var offset = getFileBreakpoint(id, chunks.length);

遍歷chunks傳送資料段時,增加篩選邏輯


  chunks.forEach(function(chunk, index){
    // ==============新增=================
    // 從offset開始傳輸
    if (index < offset) {
      return;
    }
    // ==============新增=================

    var formData = new FormData();
    formData.append('fileId', id);
    formData.append('myFileChunk', chunk);
    formData.append('chunkIndex', index);
    $.ajax({
      type: "POST",
      url: 'file_getchunk.php',
      data: formData,
      contentType: false,
      processData: false,
      success: function(done){
        task.pop();
        console.log(done,' 已完成');
        if (task.length === 0) {
          console.log('通知整合');
          makeFileIntegration(id, chunks.length);
        }
      }
    })
    task.push(index+' is Working');
  })

獲取中斷位置介面file_get_breakpoint.php

這裡使用的獲取中斷位置的邏輯很簡單(不是最優),只需要檢測資料夾是否存在,再依次檢測資料段是否缺失。缺失時返回缺失段的index,已存在返回chunks長度size,不存在時返回0

 // 1.檢測資料檔案是否存在(檔案標識,資料段總數)
 $fileId = $_POST['id'];
 $size = $_POST['size'];
 // 臨時資料夾名稱
 $length = strlen($fileId) - (strlen($fileId) - strpos($fileId, '.'));
 $filedir = substr($fileId, 0, $length);// 2.按順序檢測缺失的資料段的位置
 // 檢測是否存在資料夾
 if (is_dir("upload/$filedir")) {
   $offset = $size;
   // 檢測資料段缺失下標
   for ($i = 0; $i < $size; $i++) {
     $filepath = "upload/$filedir/$i";
     if(!file_exists($filepath)){
       // 缺失i部分
       $offset = $i;
       break;
     }
   }
   // 輸出偏移量
   echo $offset;
 } 
 else {
   // 是否存在已合併檔案
   if(file_exists("upload/$fileId")){
     echo $size;
   } else{
     // 檔案尚未上傳
     echo 0;
   }
 }

2.檔案秒傳
檔案秒傳的概念,按照我的理解,就是在上傳檔案請求後,伺服器端檢測資料庫中是否存在相同的檔案,如果存在相同的檔案,就可以告訴使用者上傳完成了。

此處在獲取offset後,增加一個判斷就可以實現

 var offset = getFileBreakpoint(id, chunks.length);
 // 增加判斷
 if(chunks.length === offset) {
   console.log('檔案已經上傳完成');
   return;
 }

當然,這裡僅僅是非常簡單的處理,我們還可以使用MD5來作為檔案識別符號,在在伺服器端使用這個識別符號是否存在相同檔案。

3.MD5檢測檔案完整性。
透過md5對檔案加密,傳輸到伺服器端,伺服器端實現合併後對檔案再進行一次md5加密,比對兩串md5字串是否相同,就可以知道檔案傳輸過程中是否完整。

3.上傳完成後,儲存資料段資料夾進行刪除操作。
我們最後做一步就是將臨時檔案移除操作,在整合完成後,我們只需要在file_integration.php介面中,整合完成後,移除資料夾及其下面的所有檔案。


 function deldir($path){
    //如果是目錄則繼續
   if(is_dir($path)){
       //掃描一個資料夾內的所有資料夾和檔案並返回陣列
     $p = scandir($path);
     foreach($p as $val){
       //排除目錄中的.和..
       if($val !="." && $val !=".."){
         //如果是目錄則遞迴子目錄,繼續操作
         if(is_dir($path.$val)){
           //子目錄中操作刪除資料夾和檔案
           deldir($path.$val.'/');
           //目錄清空後刪除空資料夾
           @rmdir($path.$val.'/');
         }else{
           //如果是檔案直接刪除
           unlink($path.$val);
         }
       }
     }
     // 刪除資料夾
     rmdir($path);
   }
 }

//刪除臨時資料夾

 deldir("upload/$filedir/");

4.總結
  按照上述步驟,可以跟著實現簡單上傳、大檔案分割上傳、斷點續存等知識,起碼下次遇到上傳檔案,心裡也有了點底氣。由於本人是前端小白,所以寫的程式碼比較簡陋,只是實現了功能,還有許多可以最佳化的地方,如果程式碼有誤,還望指正。

進階版,前端佇列非同步上傳1g以上的大影片

– 有點複雜,腦瓜子炸了,不想整理了

         function sendChunk(id, chunks) {
        // 逐個提交
        // 用於保證ajax傳送完畢
        // var task = chunks;
        var task = [];
        var index = 0;
        var cLength = chunks.length;
        // console.log(task.length)
        // console.log(chunks.length)
        var offset = getFileBreakpoint(id, chunks.length);
        if (offset === -1) {
            console.log('已經上傳完畢');
            return;
        }
        var chunkInterval = setInterval(function(){
            // 分片資料上傳完畢
            if (chunks.length <= 0) {
                console.log('分片上傳完畢')
                clearInterval(chunkInterval);
            }
            // 佇列還有資料,任務還沒結束
            if (task.length > 0) {
                console.log('剩餘任務')
                console.log(task)
                return;
            }
            // 裝填任務
            task = [1, 2];
            var tLength = task.length;
            for ($i = 0; $i < tLength; $i++) {
                if (chunks.length <= 0) {
                    break;
                }
                chunk = chunks.shift();
                if (index < offset) {
                    task.pop();
                    index++;
                    continue;
                }
                console.log(index);
                var formData = new FormData();
                formData.append('fileId', id);
                formData.append('video', chunk);
                formData.append('chunkIndex', index++);
                formData.append('size', cLength);
                $.ajax({
                    type: "POST",
                    url: '/native_api/main/upload_post_video',
                    data: formData,
                    // async: false,
                    // contentType: 'application/octet-stream',
                    contentType: false,
                    processData: false,
                    success: function(done) {
                        // 移除已完成任務
                        task.pop();
                        console.log(done.offset, ' 已完成');
                        if (done.status == 200) {
                            if (done.offset == '-1') {
                                console.log('已經上傳完畢');
                                console.log(done);
                                return;
                            }
                        } else {
                            console.log(done.err_code + ' 上傳失敗');
                        }
                        // if (task.length === 0) {
                        //     // 傳送完畢,整合檔案
                        //     console.log('通知整合');
                        //     makeFileIntegration(id, chunks.length);
                        // }
                    }

                })
            }
        },1000)

        return true;

    }
本作品採用《CC 協議》,轉載必須註明作者和本文連結
reading

相關文章