实战干货:原生PHP实现大文件分片上传,无需框架、开箱即用,支持普通大文件上传,完美解决PHP大文件上传超时、内存溢出、表单限制等核心问题,适配服务器生产环境。
在日常开发中,直接使用PHP原生上传接口传输几百MB甚至几GB的大文件,会遇到诸多致命问题:
- PHP脚本执行超时(max_execution_time 限制)
- 服务器POST数据大小、单文件上传大小限制
- 一次性读取大文件导致内存溢出
- 上传中断需要重新上传,不支持断点续传
分片上传 是解决以上问题的最优方案,本文将从零实现一套 前端分片切割 + PHP后端接收合并 的完整上传方案,代码简洁、可直接部署使用。
目录
[修改 php.ini 核心参数](#修改 php.ini 核心参数)
一、分片上传核心原理
分片上传的核心思想:化整为零,分批传输,最后合并,具体流程如下:
- 前端切片:通过JS File API 将超大文件切割为固定大小的小分片(1MB~5MB)
- 唯一标识:对原文件生成MD5唯一指纹,区分不同文件的分片,避免冲突
- 逐片上传:异步批量上传每一个分片,携带分片索引、总分片数等参数
- 后端暂存:PHP接收分片,临时存储在单独目录,按规则命名区分
- 合并文件:所有分片上传完成后,后端按索引顺序合并所有分片,生成完整文件
- 清理缓存:合并完成后自动删除临时分片文件,释放服务器存储空间
二、环境前置配置(必改)
PHP默认配置严格限制文件上传,必须修改 php.ini 配置文件,否则分片上传会失效。
修改 php.ini 核心参数
php
; 单文件最大上传大小(根据业务调整)
upload_max_filesize = 100M
; POST请求最大数据大小
post_max_size = 100M
; 脚本执行时间 0=不限制
max_execution_time = 0
; 脚本内存限制,大文件上传建议调高
memory_limit = 512M
; 关闭文件上传临时文件清理(可选)
upload_tmp_dir = /tmp
修改完成后,重启Apache/Nginx、PHP-FPM 服务生效。
三、完整代码实现
整套方案分为 前端上传页面(index.html) 和 PHP后端接收接口(upload.php),两个文件放在同一目录即可运行。
前端页面:文件切片+异步上传
实现文件选择、自动切片、进度条展示、逐片异步上传功能,原生JS无依赖。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>PHP大文件分片上传</title>
<style>
.box{margin: 50px auto;width: 400px;}
.progress{width: 100%;height: 12px;background: #eee;border-radius: 6px;margin: 15px 0;overflow: hidden;}
.progress-bar{height: 100%;background: #1677ff;width: 0;transition: width 0.3s;}
.tip{color: #666;margin-top: 10px;}
</style>
</head>
<body>
<div class="box">
<h3>大文件分片上传演示</h3>
<input type="file" id="file" />
<div class="progress"><div class="progress-bar" id="progress"></div></div>
<div class="tip" id="msg">请选择需要上传的大文件</div>
</div>
<script>
// 单个分片大小:1MB(可调整2M/5M)
const CHUNK_SIZE = 1 * 1024 * 1024;
const fileDom = document.getElementById('file');
const progressDom = document.getElementById('progress');
const msgDom = document.getElementById('msg');
// 监听文件选择
fileDom.addEventListener('change', async function (e) {
const file = e.target.files[0];
if (!file) return;
// 初始化状态
progressDom.style.width = '0%';
msgDom.innerText = '正在解析文件...';
// 生成文件唯一标识(简易MD5,生产建议使用spark-md5)
const fileMd5 = await getFileUniqueId(file);
// 计算总分片数
const totalChunk = Math.ceil(file.size / CHUNK_SIZE);
let successCount = 0;
msgDom.innerText = `开始上传,共${totalChunk}个分片`;
// 循环上传所有分片
for (let i = 0; i < totalChunk; i++) {
// 切割文件分片
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// 组装上传参数
const formData = new FormData();
formData.append('chunk', chunk); // 分片文件
formData.append('index', i); // 当前分片索引
formData.append('total', totalChunk); // 总分片数
formData.append('md5', fileMd5); // 文件唯一标识
formData.append('filename', file.name); // 原始文件名
// 上传当前分片
const res = await uploadChunk(formData);
if (res.code !== 0) {
msgDom.innerText = `第${i+1}片上传失败,请重试`;
return;
}
// 更新进度
successCount++;
const percent = (successCount / totalChunk) * 100;
progressDom.style.width = percent + '%';
msgDom.innerText = `上传中 ${successCount}/${totalChunk}`;
}
msgDom.innerText = '✅ 文件上传完成!';
});
// 分片上传请求
function uploadChunk(data) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php', true);
xhr.onload = function () {
resolve(JSON.parse(xhr.responseText));
};
xhr.onerror = function () {
resolve({code: 1, msg: '请求异常'});
};
xhr.send(data);
});
}
// 生成文件唯一标识(简易版)
function getFileUniqueId(file) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function (e) {
// 简易唯一ID,生产环境替换为完整MD5
resolve(btoa(e.target.result.slice(0, 100)));
};
});
}
</script>
</body>
</html>
后端接口:接收分片+合并文件
PHP核心接口,实现分片接收、临时存储、自动合并、缓存清理全逻辑。
php
<?php
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
// 基础配置
$config = [
'upload_path' => './uploads/', // 最终文件保存目录
'chunk_path' => './chunks/', // 分片临时存储目录
];
// 初始化目录
!is_dir($config['upload_path']) && mkdir($config['upload_path'], 0777, true);
!is_dir($config['chunk_path']) && mkdir($config['chunk_path'], 0777, true);
// 接收前端参数
$index = intval($_POST['index']); // 当前分片索引
$total = intval($_POST['total']); // 总分片数
$file_md5 = trim($_POST['md5']); // 文件唯一标识
$filename = trim($_POST['filename']); // 原始文件名
$chunk = $_FILES['chunk']; // 分片文件
// 分片保存路径:md5_索引 命名,防止文件冲突
$chunk_file = $config['chunk_path'] . $file_md5 . '_' . $index;
// 移动分片文件到临时目录
if (!move_uploaded_file($chunk['tmp_name'], $chunk_file)) {
exit(json_encode(['code' => 1, 'msg' => '分片上传失败']));
}
// 所有分片上传完毕,执行文件合并
if ($index == $total - 1) {
// 拼接最终文件路径
$final_file = $config['upload_path'] . $filename;
// 二进制方式创建文件
$handle = fopen($final_file, 'wb');
// 按顺序读取分片并写入最终文件
for ($i = 0; $i < $total; $i++) {
$temp_chunk = $config['chunk_path'] . $file_md5 . '_' . $i;
// 写入分片内容
fwrite($handle, file_get_contents($temp_chunk));
// 删除已合并的分片
unlink($temp_chunk);
}
fclose($handle);
}
// 返回成功响应
echo json_encode(['code' => 0, 'msg' => 'success']);
?>
四、项目目录结构
部署后自动生成目录,无需手动创建,结构清晰:
bash
项目根目录
├── index.html # 前端上传页面
├── upload.php # 后端接收合并接口
├── uploads/ # 最终上传文件存储目录(自动生成)
└── chunks/ # 分片临时缓存目录(自动生成)
五、核心知识点解析
分片大小选择
- 推荐区间:1MB - 5MB
- 分片过小:请求次数过多,占用服务器HTTP资源,上传变慢
- 分片过大:单请求超时概率高,失去分片意义
分片命名规则
采用 文件MD5_分片索引 命名,完美解决:
- 多文件同时上传的分片冲突问题
- 分片顺序错乱导致文件损坏问题
二进制合并原理
使用 fopen(xx,wb) 二进制覆写模式,逐片拼接文件二进制流,不改变文件编码、格式,支持视频、压缩包、图片、文档等所有文件格式。
六、生产环境进阶优化(必备)
上述代码为基础可用版本,生产环境需优化以下功能,提升稳定性和安全性:
实现断点续传
新增 查询已上传分片接口,上传前请求后端,获取当前文件已上传的分片索引,跳过已上传分片,中断后无需重传全部文件。
完善安全校验
- 限制上传文件后缀,禁止php、exe、sh等恶意文件
- 文件合并后校验整体MD5,防止文件损坏、篡改
- 增加请求token验证,防止恶意刷接口
异常容错处理
- 前端增加分片失败自动重试机制
- 后端增加分片过期清理脚本,定时删除残留缓存分片
- 增加文件大小、分片数量合法性校验
性能优化
- 前端开启并发上传(同时上传2-3个分片),提升上传速度
- 后端使用文件指针流式合并,替代file_get_contents,降低大文件内存占用
七、常见问题避坑指南
- 上传超时 :务必设置 max_execution_time=0,关闭脚本执行时间限制
- 文件合并损坏 :必须按分片索引顺序合并,不可无序拼接
- 权限报错:给项目目录设置 0777 权限,保证PHP有读写、创建、删除权限
- 分片残留:服务器定时清理 chunks 临时目录,避免磁盘占用过高
- 大文件内存溢出 :调高 memory_limit,优先使用流式读写
八、总结
PHP大文件分片上传的核心逻辑就是 前端切片、后端暂存、最终合并。本文提供的源码无框架依赖、轻量化、可直接部署,彻底解决了传统PHP单文件上传的各类限制问题。
基础版本可满足中小型项目需求,搭配断点续传、安全校验、定时清理等进阶功能后,可直接用于企业级生产环境。