jQuery + ThinkPHP 实现大文件切割上传

作者: 分类: php 发布时间: 2021-08-28 14:48 浏览人数:4
 

为什么服务器不能直接上传大文件呢?跟php.ini的几个配置有关:

upload_max_filesize = 2M //PHP最大能接受的文件大小
post_max_size = 8M //PHP能收到的最大POST值'
memory_limit = 128M //内存上限
max_execution_time = 30 //最大执行时间

不过也不能简单粗暴的把上面这几个值调大,否则服务器的内存资源被吃光是早晚的问题。
所以,就得从技术方面下手解决了。


解决思路

JS思路

1、监听文件上传onchange事件;
2、获取到上传的文件File对象;
3、把文件的File对象的文件二进制内容进行切割,并且附加到FormData对象中;
4、把FormData对象通过Ajax发送到服务器;
5、重复【步骤3】【步骤4】,直到文件发送完。

PHP思路

1、建立上传文件夹;
2、把接收到的文件从上传临时目录移动到上传文件夹;
3、所有的分片文件上传完成以后,对分片文件按照顺序进行合并;
4、删除文件夹和(或)分片文件;
5、返回合并后的文件路径。


DEMO代码

前端部分代码

<!DOCTYPE HTML>
<html>

<head>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script>
</head>

<body>
    <form id="uploadForm" method="post">
        <input type="file" name="file" id="file">
        <input type="submit">
    </form>
    <script>
        $("#uploadForm").on("submit", function (e) {
            const LENGTH = 1024 * 1024; // 每次上传的文件大小,即把文件分割成指定大小的分块文件。单位为Bytes
            
            var file = $("#file")[0].files[0]; // 获取要上传的文件
            
            var start = 0; // 当前文件的分割起始位置
            
            var end = start + LENGTH; // 当前文件的分割结束位置
            
            var blob; // 当前的分割文件块。用来存储文件块的内容
            
            var blob_num = 1; // 当前的分割文件对应的块数
            
            var total_blob_num = Math.ceil(file.size / LENGTH); // 总的文件分割块数

            /**
             * 切割文件
             * @param file
             * @return blob
             */
            function cutFile (file) {
                // 按照指定的起始和结束位置,获取指定部分的文件内容
                var file_blob = file.slice(
                    start, 
                    end
                );

                // 分割完之后,结束位置更改为下一次分割的起始位置
                start = end;

                // 下一次分割的起始位置加上分割长度作为下一次分割的结束位置
                end = start + LENGTH;

                // 返回被分割出来的文件内容
                return file_blob;
            }

            /**
             * 发送文件
             * @param blob
             * @param file
             */
            function sendFile (blob, file) {
                var formData = new FormData();

                formData.append(
                    'file',
                    blob
                );

                formData.append(
                    'blob_num',
                    blob_num
                );

                formData.append(
                    'total_blob_num',
                    total_blob_num
                );

                formData.append(
                    'file_name',
                    file.name
                );

                // 计算当前上传文件的MD5内容
                calculate(file, function (md5) {
                    formData.append(
                        'md5', 
                        md5
                    );

                    $.ajax({
                        url: "/index/upload.html",
                        type: "post",
                        cache: false,
                        processData: false,
                        contentType: false,
                        data: formData,
                        success: function (res) {
                            // 此处延时1000毫秒,防止程序执行混乱
                            setTimeout(function () {
                                // 如果当前的分割块数未超过总的分割块数,则继续分割并上传
                                if (blob_num <= total_blob_num) {
                                    blob = cutFile(file);

                                    sendFile(
                                        blob,
                                        file
                                    );

                                    blob_num += 1;
                                } else {
                                    alert("上传完成!");
                                }
                            }, 1000);
                        },
                    });
                });
            }

            /**
             * @param file
             * @param callback
             */
            function calculate (file, callBack) {
                function loadNext () {
                    var start = currentChunk * chunkSize,
                        end = start + chunkSize >= file.size ? file.size : start + chunkSize;

                    fileReader.readAsBinaryString(blobSlice.call(
                        file,
                        start,
                        end
                    ));
                };

                var fileReader = new FileReader(),
                    blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
                    chunkSize = 2097152,
                    // read in chunks of 2MB 
                    chunks = Math.ceil(file.size / chunkSize),
                    currentChunk = 0,
                    spark = new SparkMD5();

                fileReader.onload = function (e) {
                    spark.appendBinary(e.target.result); // append binary string 

                    currentChunk++;

                    if (currentChunk < chunks) {
                        loadNext();
                    } else {
                        callBack(spark.end());
                    }
                };

                loadNext();
            }

            blob = cutFile(file);

            sendFile(
                blob,
                file
            );

            blob_num += 1;

            return false;
        });
    </script>
</body>

</html>


PHP代码

<?php
declare (strict_types = 1);

namespace app\index\controller;

use think\facade\View;
use think\Request;

class Index
{
    /**
     * 显示资源列表
     *
     * @return \think\Response
     */
    public function index()
    {
        //
        return View::fetch('index');
    }
	
	public function upload()
    {
        // 获取当前的分割的块数
        $blobNum = (integer)request()->param(
            'blob_num'
        );
        
        // 获取总的分割块数
        $totalBlobNum = (integer)request()->param(
            'total_blob_num'
        );
        
        // 获取上传的文件的真实文件名(不包含路径)
        $fileName = request()->param(
            'file_name'
        );
        
        // 获取文件内容的MD5格式,防止与其它上传文件冲突
        $md5 = request()->param(
            'md5'
        );
        
        // 获取当前上传的分片文件
        $file = request()->file(
            'file'
        );

        // 获取文件最终被存储的路径和文件名(路径仅相对于当前项目根目录位置)
        $realFileName = 'storage/index/material/' . (new \think\File('/' . $fileName, false))->hashName();
        
        // 获取分片文件的临时存储位置
        $storagePath = root_path() . 'runtime/storage/' . $md5 . '/';

        // 把每一个分片文件按照块数命名,并且存储到临时存储的位置
        \think\facade\Filesystem::putFileAs(
            $md5, 
            $file, 
            $blobNum . ''
        );

        // 如果已经上传了最后一个分片文件,则合并并存储最终文件,并删除临时文件
        if ($blobNum >= $totalBlobNum) {
            // 遍历每一个分片文件
            for ($i = 1; $i <= $totalBlobNum; $i++) {
                // 获取当前遍历到的分片文件
                $tmpFile = $storagePath . $i . '';

                // 判断当前的分片文件是否存在,不存在则跳过
                if (!is_file($tmpFile)) {
                    continue;
                }

                // 读取当前的分片文件的内容
                $tmpFileContent = file_get_contents(
                    $tmpFile
                );

                // 判断文件最终的存储路径是否存在,不存在则创建
                if (!is_dir(pathinfo(public_path() . $realFileName)['dirname'])) {
                    mkdir(
                        pathinfo(public_path() . $realFileName)['dirname'], 
                        0777, 
                        true
                    );
                }

                // 把当前的分片文件追加到最终的文件里面
                file_put_contents(
                    public_path() . $realFileName, 
                    $tmpFileContent, 
                    FILE_APPEND
                );

                // 当前的分片文件已经处理完,没有用了,删除掉
                unlink(
                    $tmpFile
                );
            }

            // 删除分片文件的临时存储位置(文件夹)
            rmdir($storagePath);
        } else { // 如果上传的分片文件还不是最后一个分片文件
			// to do something...
        }

        return json([
            'code' => 0, 
            'msg' => 'Success', 
            'data' => '/' . $realFileName, 
        ]);
    }
}

好了,上传完成

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!