文件/大文件上傳功能實現(JS+PHP)全過程

来源:https://www.cnblogs.com/morningclock/archive/2019/10/14/11670969.html
-Advertisement-
Play Games

文件/大文件上傳功能實現(JS+PHP) 參考博文:掘金-橙紅年代 前端大文件上傳 路漫漫 其修遠 PHP + JS 實現大文件分割上傳 本文是學習文件上傳後的學習總結文章,從無到有實現文件上傳功能,前端小白寫的代碼不是最優,如果有錯誤的地方請多多指教,如果本文對你有所幫助,深感榮幸。 近期公司的項 ...


文件/大文件上傳功能實現(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的預設行為導致數據格式或請求頭信息出現問題。

      這裡的contentTypeprocessData為必須項。

       $('#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標識數據段的位置。

    通過初步整理,我們就需要以下的參數

    1. 文件唯一標識符

    2. 分割後數據段

    3. 分割數據段的順序索引值

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

    1. file_getchunk.php

      功能:將分割chunk數據,整理並保存,此處我們用文件形式實現。

    2. file_integration.php

      功能:接收到整合通知,將數據段拼接,並生成文件。

    整體流程大致如圖:

  • PHP.ini配置

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

     post_max_size = 50M
     upload_max_filesize = 50M

     

  • 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 fileId = getFileIdentifier(myfile);
         // 數據切片
         var chunks = fileSlice(myfile);
         // 發送分割數據段
       sendChunk(fileId, chunks);
       })
     
  • 生成文件唯一標識getFileIdentifier()

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

       function getFileIdentifier(file){
         // 獲取文件標識符
         return file.size + file.name;
      }

     

    • 分割方法fileSlice()

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

        function fileSlice(file, chunkSize = 1024*1024*0.2){
          // 1.初始化數據
          var totalSize = file.size;
        var start = 0;
          var end = start + chunkSize;
          var chunks = [];
          // 2.使用bolb提供的slice方法切片
          while(start < totalSize){
            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.斷點續存。

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

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

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

    • 思路整理

      我們要在上傳前,請求服務端查詢出中斷時的位置,利用位置信息,篩選上傳的數據段切片。

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

      1. offset中斷位置信息

      2. 查詢中斷位置介面: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.總結

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首發於微信公眾號《前端成長記》,寫於 2019.10.12 導讀 有句老話說的好,好記性不如爛筆頭。人生中,總有那麼些東西你願去執筆寫下。 本文旨在把整個搭建的過程和遇到的問題及解決方案記錄下來,希望能夠給你帶來些許幫助。 本文涉及的主要技術: "Vue3.0 Composition API" "G ...
  • 看了知乎上的話題 如何才能通俗易懂的解釋javascript裡面的‘閉包’?,受到一些啟發,因此結合實例將回答中幾個精要的答案做一個簡單的分析以便加深理解。 1. "閉包就是跨作用域訪問變數。" 【示例一】 在 getName 函數中獲取 name,首先在 getName 函數的作用域中查找 nam ...
  • 前面,跟大家簡單地介紹了負載均衡和Nginx的一些基礎配置( "Nginx負載均衡配置實例" ),接下來,跟大家介紹一下Nginx的常用命令,便於日常的運維。 "查看原文" 停止Nginx的方法 通過之前的學習,大家知道瞭如何配置並啟動Nginx,但如果想停止Nginx服務,該如何操作呢?下麵介紹停 ...
  • npm multer 文件上傳 Express app 範本就不寫了,僅記錄一下上傳部分的代碼。 const fs = require('fs'); const express = require('express'); const multer = require('multer'); const ...
  • webpack 插件 ProvidePlugin:自動載入模塊,而不必到處 import 或 require 。 ...
  • 一.a標簽完成 二.js實現下載 三.js中ajax實現音頻或者視頻不跳轉進行文件下載 寫代碼的思路 四.fetch實現 ...
  • 參考自網站:https://segmentfault.com/a/1190000011779959 插件安裝完成之後,還要對一些插件進行配置,例如: vetur預設配置, 配置的過程: 打開 文件 > 首選項 > 用戶設置(U) > 點擊右上角 打開設置(json) // 執行文字相關的導航或操作時 ...
  • 本資源是我在源代碼網站上發現的,內附幾十種背景動態特效,我單獨提取出來精品背景特效在此分享,文件里有20多種精品動態效果,本人覺得可用作於個人博客主頁背景,登陸頁面背景等,有20多個背景特效,非常漂亮。 附文件下載地址: https://github.com/chengpu2/web2 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...