转载文章,尚未验证
对于目前大文件上传,较为合理的方式就是分片上传。get方式仅可以传输几K的数据,POST 理论上是可以传输无限的数据,但会有服务器的限制,php.ini里面的upload_max_filesize会对你的文件上传数据大小加以限制,一旦超过这个数值,服务器就会拒绝接收,所以前端要进行大文件分割,分成文件块进行上传。
前端JS实现
对于分片上传,最重要的就是slice方法,此方法可以分割文件。
fileBlock = file.slice(start, end), fileBlock 即为大小为(end – start)分割好的文件块。
对于上传,不能使用json进行传输数据,而要使用formData进行传输,也就类似于直接点击form表单的submit的按钮进行上传。
因为是前端是基于vue写的,会有一些变量不在方法中,在方法中会进行解释。
我是基于axios封装的ajax进行上传
分片上传
将上传文件封装成一个方法
function uploadSlice (file, skip) {
const blockSize = 1024 * 1024 // 一个文件块的大小为1M
let totalNum = Math.ceil(file.size / blockSize) // 可分为多少文件块
let formData = new FormData() // 不用json包装数据,而使用formData
let config = {
headers = {
'Content-Type': 'multipart/form-data' // 将请求头的Content-Type改为这个,后端才能接收到上传的文件块的二进制流
}
}
let nextSize = Math.min((skip + 1) * blockSize, file.size)
let fileData = file.slice(skip * blockSize, nextSize) // fileData就是分割好的blob对象,可进行二进制流传输
formData.append('file', fileData) // append方法可以在formData对象进行数据追加
formData.append('blobNum', skip + 1) // blobNum就是目前上传到第几块,以便后端判断是否上传完成
formData.append('totalNum', totalNum)
formData.append('id', this.id) // id为后端存储记录对应的id
axios.post('upload/slice', formData, config).then({
({ code }) => {
if (code === 2) {
this.uploadSlice(file, ++skip)
} else if (code === 1) {
alert('上传成功')
}
}
})
}
断点续传
上传期间会有各种不可违逆因素导致上传中断,因此断点续传是不可或缺的。
断点续传主要分为两步:第一步,向后端获取中断的节点,此时需要验证用户再次选择的文件是否与上传中断的文件一样;第二步,在中断节点处进行分片上传。
验证文件并获取节点
我写的时候其实是带token验证的,但是基于普适性,就没将token加上去了
function compareFile () {
const blockSize = 1024 * 1024
let formData = new FormData()
let config = {
headers: {
'Content-Type': 'multipart/form-data'
}
}
// file是用户选择文件后生成的File对象,blobNum是向后端获取的中断节点
let fileData = this.file.slice((this.blobNum - 1) * blockSize, Math.min(this.blobNum * blockSize, this.file.size))
formData.append('file', fileData)
axios.post('upload/check', formData, config).then({
({ result }) => {
if (result) {
this.sliceUpload(this.file, this.blobNum) // 验证成功,基于中断节点进行分片上传
}else {
alert('选择的文件与已上传不一致')
}
}
}
})
}
后端PHP实现
后端是基于TP开发的,会使用一些TP封装的方法。
后端主要是接收前端发送的各个文件块,然后当前端上传结束后,后端要将这些文件块重新合并成原文件,并将相应信息存储到数据库中。
Upload上传类
这个类是用于上传接收与合并的核心类,封装了一些文件操作的方法。
网上合并用的是file_put_contents,这个方法很水泵内存,其实直接使用PHP的文件读写操作就可以完成。
class Upload
{
private $filePath; // 上传目录
private $tmpFile; // 临时文件
private $blobNum; // 当前文件块
private $totalBlobNum; // 总共文件块
private $fileName; // 文件名
private $file; // 文件
public function __construct($tmpFile, $blobNum, $totalBlobNum, $fileName, $where)
{
$this->filePath = $where;
!is_dir($this->filePath) && mkdir($this->filePath, 0777, true);
$this->tmpFile = $tmpFile->getInfo()['tmp_name'];
$this->blobNum = $blobNum;
$this->totalBlobNum = $totalBlobNum;
$this->fileName = $fileName;
$this->file = $this->filePath . DS . $fileName;
$this->fileMerge();
}
// 文件合并
private function fileMerge()
{
$tmpFile = fopen($this->file . '__tmp', 'a+');
fwrite($tmpFile, file_get_contents($this->tmpFile));
fclose($tmpFile);
if ($this->blobNum === $this->totalBlobNum)
rename($this->file . '__tmp', $this->file);
if (file_exists($this->file))
if (file_exists($this->file . '__tmp'))
@unlink($this->file . '__tmp');
}
public function result()
{
$data = 0;
if ($this->blobNum === $this->totalBlobNum) {
if (file_exists($this->filePath . DS . $this->fileName)) {
$data = 1;
}
} else {
if (file_exists($this->filePath . DS . $this->fileName . '__tmp')) {
$data = 2;
}
}
if ($data === 0)
// 这个方法是我封装的,用于抛出HttpException,TP会自动处理这个Exception,并返回给前端
SeverResponse::error('文件上传出错');
return $data;
}
}
断点续传的文件检验
public static function compareFile($uploadPath, $fileName, $tmp, $blobNum = 1)
{
$blockSize = 1024 * 1024;
$tmpPath = $uploadPath . DS . 'tmp';
!is_dir($tmpPath) && mkdir($tmpPath, 0777, true);
$tmpFilePath = $tmpPath . DS . $fileName . '__tmp';
$tmpFile = fopen($tmpFilePath, 'a+');
fwrite($tmpFile, file_get_contents($fileName . '__tmp', false, null, ($blobNum - 1) * $blockSize, $blockSize));
fclose($tmpFile);
// md5_file方法可以用来检验文件一致性,FileUtil::deleteFile是用于删除文件的方法
if (md5_file($tmpFilePath) === md5_file($tmp->getInfo()['tmp_name'])) {
FileUtil::deleteFile($tmpFilePath);
return true;
} else {
FileUtil::deleteFile($tmpFilePath);
return false;
}
}
接收分片上传的controller
public function upload()
{
// request中TP封装的处理HTTP请求的助手函数
$file = request()->file('file');
$data = [
'blobNum' => request()->param('blobNum'),
'totalNum' => request()->param('totalNum'),
'id' => request()->param('id')
];
// $info为通过id获取相应纪录对应的对象
$upload = new Upload($file, $info->blob_num + 1, $info->total_num, CheckAndGenerate::getFileName($info->source), 'uploadPath');
$result = $upload->result();
if ($result === 2) { // 正在上传的状态
// 分片上传中,可以将对应blobNum存入数据库,以便进行断点续传
// 此处用redis纪录blobNum进行优化更好
}
if ($result === 1) { // 上传完成的状态
// 上传完成后,对数据库进行一系列操作
}
// 此方法是我封装用于返回json对象
return SeverResponse::getSuccessMessage(['code' => $result]);
}
这篇文章是我开发中遇到的一个技术难点,将大部分的逻辑处理分享出来,其中很多细节部分我就没往上写了,希望这篇文章能帮到一些人吧。