您当前的位置: 首页  >  博文日记

前端用ajax切片上传文件到php后端并保存的方法,跨域解决

作者:总管理员 时间:2023-05-27 08:57:11 阅读数:967人阅读

如果要上传大文件,可以使用切片上传的方式来提高上传效率和稳定性。具体步骤如下:

在前端页面中,使用input标签的type属性为file来创建一个文件上传控件,并添加一个上传按钮。

<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>

在JavaScript中,获取到文件上传控件的值,并使用FileReader对象来读取文件内容,并将文件内容切片。

var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
var chunkSize = 1024 * 1024; // 每个切片的大小为1MB
var chunks = Math.ceil(file.size / chunkSize); // 计算文件切片的数量
var currentChunk = 0; // 当前上传的切片编号
var fileReader = new FileReader();
fileReader.onload = function(e) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', 'upload.php', true);
  xhr.setRequestHeader('Content-Type', 'application/octet-stream');
  xhr.setRequestHeader('X-File-Name', encodeURIComponent(file.name));
  xhr.setRequestHeader('X-File-Size', file.size);
  xhr.setRequestHeader('X-File-Chunk', currentChunk);
  xhr.setRequestHeader('X-File-Chunks', chunks);
  xhr.onload = function() {
    if (xhr.status === 200) {
      console.log('切片上传成功');
      currentChunk++;
      if (currentChunk < chunks) {
        uploadChunk(currentChunk);
      } else {
        console.log('文件上传完成');
      }
    } else {
      console.log('切片上传失败');
    }
  };
  xhr.send(e.target.result);
};
function uploadChunk(chunk) {
  var start = chunk * chunkSize;
  var end = Math.min(start + chunkSize, file.size);
  fileReader.readAsArrayBuffer(file.slice(start, end));
}

在PHP后端中,使用$_FILES数组来获取上传的文件切片,并使用file_put_contents函数将文件切片保存到指定的目录中。

if (isset($_SERVER['HTTP_X_FILE_NAME']) && isset($_SERVER['HTTP_X_FILE_SIZE']) && isset($_SERVER['HTTP_X_FILE_CHUNK']) && isset($_SERVER['HTTP_X_FILE_CHUNKS'])) {
  $name = urldecode($_SERVER['HTTP_X_FILE_NAME']);
  $size = intval($_SERVER['HTTP_X_FILE_SIZE']);
  $chunk = intval($_SERVER['HTTP_X_FILE_CHUNK']);
  $chunks = intval($_SERVER['HTTP_X_FILE_CHUNKS']);
  $tmp_name = $_FILES['file']['tmp_name'];
  $chunk_name = 'uploads/' . $name . '.part' . $chunk;
  file_put_contents($chunk_name, file_get_contents($tmp_name), FILE_APPEND);
}

在PHP后端中,当所有文件切片上传完成后,使用fopen函数打开文件,并使用fread函数读取所有文件切片的内容,并使用file_put_contents函数将文件内容保存到指定的目录中。

if ($chunk === $chunks - 1) {
  $file_name = 'uploads/' . $name;
  $fp = fopen($file_name, 'wb');
  for ($i = 0; $i < $chunks; $i++) {
    $chunk_name = 'uploads/' . $name . '.part' . $i;
    $chunk_content = file_get_contents($chunk_name);
    fwrite($fp, $chunk_content);
    unlink($chunk_name);
  }
  fclose($fp);
}

注意:在PHP后端中,需要确保上传的文件大小不超过php.ini中upload_max_filesize和post_max_size的限制。同时,也需要确保上传的文件类型是允许的。


如果服务端不是同一服务器,需要注意跨域的解决,下面是解决了跨域和使用jq的代码:

// HTML部分
<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>

// JavaScript部分
var fileInput = $('#fileInput');
var uploadBtn = $('#uploadBtn');

uploadBtn.on('click', function() {
  var file = fileInput[0].files[0];
  var fileSize = file.size;
  var chunkSize = 1024 * 1024; // 切片大小为1MB
  var chunkCount = Math.ceil(fileSize / chunkSize);
  var currentChunk = 0;
  var fileReader = new FileReader();

  fileReader.onload = function(e) {
    $.ajax({
      url: 'http://localhost/upload.php',
      type: 'POST',
      data: e.target.result,
      processData: false,
      contentType: 'application/octet-stream',
      headers: {
        'X-File-Name': encodeURIComponent(file.name),
        'X-File-Size': fileSize,
        'X-File-Chunk-Size': chunkSize,
        'X-File-Current-Chunk': currentChunk
      },
      success: function() {
        currentChunk++;
        if (currentChunk < chunkCount) {
          loadNext();
        } else {
          console.log('上传完成');
        }
      }
    });
  };

  function loadNext() {
    var start = currentChunk * chunkSize;
    var end = Math.min(start + chunkSize, fileSize);
    fileReader.readAsArrayBuffer(file.slice(start, end));
  }

  loadNext();
});

php部分

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Access-Control-Allow-Origin, X-Requested-With, X-File-Name, X-File-Size, X-File-Chunk-Size, X-File-Current-Chunk, Content-Type');


$fileName = urldecode($_SERVER['HTTP_X_FILE_NAME']);
$fileSize = $_SERVER['HTTP_X_FILE_SIZE'];
$chunkSize = $_SERVER['HTTP_X_FILE_CHUNK_SIZE'];
$currentChunk = $_SERVER['HTTP_X_FILE_CURRENT_CHUNK'];
$filePath = 'uploads/' . $fileName . '.' . $currentChunk;

$fp = fopen($filePath, 'w');
fwrite($fp, file_get_contents('php://input'));
fclose($fp);

if ($currentChunk == $chunkCount - 1) {
  $fp = fopen('uploads/' . $fileName, 'w');
  for ($i = 0; $i < $chunkCount; $i++) {
    $filePath = 'uploads/' . $fileName . '.' . $i;
    fwrite($fp, file_get_contents($filePath));
    unlink($filePath);
  }
  fclose($fp);
}

注意,这里的跨域问题需要在PHP后端代码中添加header('Access-Control-Allow-Origin: *');来解决,f12调试时如果出现提示被拦截,就加入到里面:

header('Access-Control-Allow-Headers: Access-Control-Allow-Origin, X-Requested-With, X-File-Name, X-File-Size, X-File-Chunk-Size, X-File-Current-Chunk, Content-Type');

只要被拦截的头部就加到里面。


如果要实现秒传等功能,建议使用插件:https://www.dpwlkj.cn/js/upload.js文件。

完整示例代码,前端:

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-9">
            <div class="card">
                <div class="card-header">微附件-{{Auth::user()->username}}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    <h4 class="m-3 text-center">
                        <a style="text-decoration:none;" href="https://mp.weixin.qq.com/s/BW2GsUhuQtmu7-YqXGFVhQ">微附件-公众号中插入附件【使用方法】</a>
                    </h4>
                    <form method="POST" action="/wfjsave">
                        @csrf
                        
                        <input id="file" type="hidden" class="form-control" name="file">
                        
                        <div class="row mb-3">
                            <label for="name" class="col-md-3 col-form-label text-md-end star" required>文件名</label>

                            <div class="col-md-7">
                                <input id="name" type="text" class="form-control" name="name" required>
                            </div>
                        </div>

                        <div class="row mb-3 mt-4">
                            <label for="fileInput" class="col-md-3 col-form-label text-md-end star">上传文件</label>

                            <div class="col-md-7">
                                <button id="upid2" class="form-control" type="button">选择上传</button>
                                <div class="progress mt-3">
                                    <div class="progress-bar" id="jd" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
                                </div>
                            </div>
                        </div>


                        <div class="row justify-content-md-center mt-4">
                            <button type="submit" id="save" class="btn btn-primary" disabled style="width:150px;">确认保存</button>
                        </div>
                    </form>
                    <hr>
                    <h3 class="m-4 text-center">我的文件</h3>
                    <div class="m-3">
                        <table class="table">
                            <thead class="thead-dark">
                                <tr>
                                    <th>名称</th>
                                    <th>公众号链接(点击复制)</th>
                                </tr>
                            </thead>
                            @foreach($paginator as $value)
                                <tr>
                                    <td>{{$value->mc}}</td>
                                    <td><span onclick="copyContent(this);" title="点击复制">{{ '/pages/video/file?id='.$curlk::encode($value->id); }}</span></td>
                                </tr>
                            @endforeach
                        </table>
                        <input id="copy_content" type="text" value=""  style="position: absolute;top: 0;left: 0;opacity: 0;z-index: -10;"/>
                    </div>
                    
                    <div class="text-center m-4">
                        {{ $paginator->links() }}
                    </div>
                    
                    
                </div>
            </div>
        </div>
    </div>
</div>
<script src="/js/upload.js"></script>
<script type="text/javascript">
    function copyContent(ElementObj){
        var clickContent = ElementObj.innerText;         
        var inputElement =  document.getElementById("copy_content");
        inputElement.value = clickContent;
        inputElement.select();
        document.execCommand("Copy");
        alert('已复制');
     }
     
    function Progress2(value) {
        $('#jd').text(value + '%');
        $('#jd').css('width', value + '%');
    }
     
   let up2 = new fcup({
      id: "upid2", // 绑定id

      url: '{{$upload}}' + 'upload.php', // url地址
	  
	  check_url: '{{$upload}}' + "check.php", // 检查上传url地址

      type: "jpg,png,jpeg,doc,docx,xls,xlsx,pdf,ppt,pptx,zip,rar,mp3,mp4", // 限制上传类型,为空不限制
      shard_size: "1", // 每次分片大小,单位为M,默认1M
      min_size: '', // 最小文件上传M数,单位为M,默认为无
      max_size: "200", // 上传文件最大M数,单位为M,默认200M
	  // headers: {"version": "fcup-v2.0"}, // 附加的文件头,默认为null, 请注意指定header头时将不能进行跨域操作
	  // apped_data: {}, //每次上传的附加数据
      // 定义错误信息
      error_msg: {
         1000: "未找到上传id",
         1001: "类型不允许上传",
         1002: "上传文件过小",
         1003: "上传文件过大",
         1004: "上传请求超时"
      },
      // 错误提示
      error: (msg) => {
         alert(msg);
      },
      // 初始化事件                
      start: () => {
         console.log('上传已准备就绪');
         Progress2(0);
      },
      // 等待上传事件,可以用来loading
      before_send: () => {
         console.log('等待请求中');
      },

      // 上传进度事件
      progress: (num, other) => {
         Progress2(num);
        //  console.log(num);
        //  console.log('上传进度' + num);
        //  console.log("上传类型" + other.type);
        //  console.log("已经上传" + other.current);
        //  console.log("剩余上传" + other.surplus);
        //  console.log("已用时间" + other.usetime);
        //  console.log("预计时间" + other.totaltime);
      },
	  
      // 检查地址回调,用于判断文件是否存在,类型,当前上传的片数等操作
      check_success: (res) => {
         let data = res ? eval('(' + res + ')') : '';
		 let status = data.status;
         let url = data.url;
		 let msg = data.message;
		 // 错误提示
         if (status == 1 ) {
            alert(msg);
            return false;
         }
		 // 已经上传
         if (status == 2) {
            Progress2(100);
            $('#file').val(url);
            $("#save").removeAttr("disabled");
			alert('文件已存在');
            return false;
         }
		// 如果提供了这个参数,那么将进行断点上传的准备
		if(data.file_index){
           // 起始上传的切片要从1开始
		   let file_index = data.file_index ? parseInt(data.file_index) : 1;
           // 设置上传切片的起始位置		   
		   up2.set_shard(file_index);
		}
        // 如果接口没有错误,必须要返回true,才不会终止上传
         return true;
      },
	  
      // 上传成功回调,回调会根据切片循环,要终止上传循环,必须要return false,成功的情况下要始终返回true;
      success: (res) => {
         let data = res ? eval('(' + res + ')') : '';
         let url = data.url + "?" + Math.random();
		 let file_index = data.file_index ? parseInt(data.file_index) : 1;
         if (data.status == 2) {
            $('#file').val(data.url);
            $("#save").removeAttr("disabled");
            alert('上传完成');
         }
         // 如果接口没有错误,必须要返回true,才不会终止上传循环
         return true;
      }
   });
</script>

<style>
    .star::before {
        content:"*";
        color:red
    }
    .hidden{
        display: none;
    }
</style>

php后端有两个文件:

check.php

<?php

/**
 * 检查上传文件
 * author:lovefc
 */


// 设置跨域头
header('Access-Control-Allow-Origin:*');

header('Access-Control-Allow-Methods:PUT,POST,GET,DELETE,OPTIONS');

header('Access-Control-Allow-Headers:x-requested-with,content-type');

header('Content-Type:application/json; charset=utf-8');


$name  = isset($_POST['file_name']) ? $_POST['file_name']:null; // 文件名

$md5   = isset($_POST['file_md5']) ? $_POST['file_md5'] :''; //文件的md5值

$size   = isset($_POST['file_size']) ? $_POST['file_size'] :''; //文件大小

// 输出json信息
function jsonMsg($status,$message,$url='',$index=1){
   $arr['status'] = $status;
   $arr['message'] = $message;
   $arr['url'] = $url;
   $arr['file_index'] = $index;
   echo json_encode($arr);
   die();
}

//jsonMsg(0,'','',201);  


if(!$md5){
	jsonMsg(1,'没有文件');
}

// 简单的判断文件类型s
$info = pathinfo($name);

// 取得文件后缀
$ext = isset($info['extension'])?$info['extension']:'';

/* 判断文件类型 */
$imgarr = array('jpg','png','jpeg','doc','docx','xls','xlsx','pdf','ppt','pptx','zip','rar','mp3','mp4');
if(!in_array($ext,$imgarr)){
    jsonMsg(1,'文件类型出错');
}

// 在实际使用中,用md5来给文件命名,这样可以减少冲突

$file_name = $md5.'.'.$ext;

$newfile = 'files/'.$file_name;

$log_file = '.files/'.$md5.'.txt';

// 文件可访问的地址
$url = 'https://video.dpwlkj.cn:4430/files/'.$file_name;

/** 判断是否重复上传 **/

// 清除文件状态
clearstatcache($newfile);

// 文件大小一样的,说明已经上传过了
if(is_file($newfile) && ($size == filesize($newfile))){
   jsonMsg(2,'已经上传过了',$url);          
}
if(is_file($log_file)){
   // 读取当前片数的时候要向前偏移1个
   $index = file_get_contents($log_file);
   $index = $index + 1;
}else{
   $index = 1;
}
jsonMsg(0,'','',$index);   

upload.php

<?php

/**
 * 接受处理上传文件
 * author:lovefc
 */


// 设置跨域头
header('Access-Control-Allow-Origin:*');

header('Access-Control-Allow-Methods:PUT,POST,GET,DELETE,OPTIONS');

header('Access-Control-Allow-Headers:x-requested-with,content-type');

header('Content-Type:application/json; charset=utf-8');

$file = isset($_FILES['file_data']) ? $_FILES['file_data']:null; //分段的文件

$name = isset($_POST['file_name']) ? $_POST['file_name']:null; //要保存的文件名

$total = isset($_POST['file_total']) ? $_POST['file_total']:0; //总片数

$index = isset($_POST['file_index']) ? $_POST['file_index']:0; //当前片数

$md5   = isset($_POST['file_md5']) ? $_POST['file_md5'] : 0; //文件的md5值

$size  = isset($_POST['file_size']) ?  $_POST['file_size'] : null; //文件大小

$chunksize  = isset($_POST['file_chunksize']) ?  $_POST['file_chunksize'] : null; //当前切片的文件大小

$suffix  = isset($_POST['file_suffix']) ?  $_POST['file_suffix'] : null; //当前上传的文件后缀

 
//echo '总片数:'.$total.'当前片数:'.$index;

// 输出json信息
function jsonMsg($status,$message,$url='',$index=0){
   $arr['status'] = $status;
   $arr['message'] = $message;
   $arr['url'] = $url;
   $arr['file_index'] = $index;
   echo json_encode($arr);
   die();
}

// 在实际使用中,用md5来给文件命名,这样可以减少冲突
// 简单的判断文件类型s
$info = pathinfo($name);

// 取得文件后缀

$ext = isset($info['extension'])?$info['extension']:'';

$file_name = $md5.'.'.$ext;

$newfile = 'files/'.$file_name;

$log_file = 'files/'.$md5.'.txt';

// 文件可访问的地址
$url = 'https://video.dpwlkj.cn:4430/files/'.$file_name;

// 这里判断有没有上传的文件流
if ($file['error'] == 0) {
	file_put_contents($log_file,$index);	
    // 如果文件不存在,就创建
    if (!file_exists($newfile)) {
        if (!move_uploaded_file($file['tmp_name'], $newfile)) {
            jsonMsg(0,'无法移动文件');
        }
        // 片数相等,等于完成了
        if($index == $total ){  
          jsonMsg(2,'上传完成',$url,$index);
        }        
        jsonMsg(1,'正在上传','',$index);
    }     
    // 如果当前片数小于等于总片数,就在文件后继续添加
    if($index <= $total){
        $content = file_get_contents($file['tmp_name']);
        if (!file_put_contents($newfile, $content, FILE_APPEND)) {
          jsonMsg(0,'无法写入文件');
        }
        // 片数相等,等于完成了
        if($index == $total ){  
          jsonMsg(2,'上传完成',$url,$index);
        }
        jsonMsg(1,'正在上传','',$index);
    }   
} else {
    jsonMsg(0,'没有上传文件');
}

记录完毕!

本站所有文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱: 2554509967@qq.com

标签: php

需要 登录 才能发表评论
热门评论
0条评论

暂时没有评论!