You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

通过JavaScript/Ajax上传文件至PHP服务器的UTF-8编码问题

解决Ajax文件上传时二进制数据UTF-8编码错误的问题

核心问题在于你在前端到后端的传输过程中,不小心把二进制数据转换成了UTF-8字符串,导致原始字节被破坏。要实现1:1的原始数据传输与写入,需要从前端处理和后端接收两个环节同时入手,完全避开字符串编码转换的坑。

一、前端:直接传输二进制数据,避免字符串转换

不要再尝试把ArrayBuffer转成逗号分隔的数字字符串或者其他文本格式——这必然会引入编码问题。推荐两种可靠的方式:

方式1:用FormData传递Blob(最直观,支持文件上传的标准方式)

把文件分片读取成ArrayBuffer后,直接包装成Blob,通过FormData发送,浏览器会自动以二进制格式传输,不会做UTF-8转换:

// 读取文件分片为ArrayBuffer
function readFileSlice(file, start, end) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => resolve(e.target.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(file.slice(start, end));
  });
}

// 上传分片
async function uploadSlice(file, sliceIndex, chunkSize = 1024*1024) {
  const start = sliceIndex * chunkSize;
  const end = Math.min(start + chunkSize, file.size);
  const arrayBuffer = await readFileSlice(file, start, end);
  const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });

  const formData = new FormData();
  formData.append('file_slice', blob);
  formData.append('slice_index', sliceIndex);
  formData.append('total_slices', Math.ceil(file.size / chunkSize));
  formData.append('file_name', file.name);

  const res = await fetch('/upload_chunk.php', {
    method: 'POST',
    body: formData
  });

  return await res.json();
}

方式2:直接发送ArrayBuffer(更轻量化)

如果不想用FormData,也可以直接把ArrayBuffer作为请求体发送,设置正确的Content-Type:

async function uploadRawSlice(arrayBuffer) {
  const res = await fetch('/upload_raw.php', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/octet-stream'
    },
    body: arrayBuffer
  });
  return await res.json();
}

二、后端PHP:以二进制模式读写,拒绝字符串处理

你的PHP代码之前的问题在于,接收的是经过UTF-8编码的数字字符串,再通过pack还原时已经丢失了原始字节信息。正确的做法是直接接收原始二进制数据,并以二进制模式写入文件:

对应FormData方式的PHP处理(upload_chunk.php)

<?php
// 确保PHP不会自动解析为UTF-8字符串
mb_internal_encoding('UTF-8'); // 不影响二进制数据处理,只是确保系统默认编码不干扰

// 获取分片文件的临时路径
$sliceTmpPath = $_FILES['file_slice']['tmp_name'];
$sliceIndex = intval($_POST['slice_index']);
$totalSlices = intval($_POST['total_slices']);
$fileName = $_POST['file_name'];

// 临时分片存储路径
$tempDir = './temp_uploads/';
if (!is_dir($tempDir)) mkdir($tempDir, 0755, true);
$targetSlicePath = $tempDir . "{$fileName}_slice_{$sliceIndex}";

// 以二进制模式写入分片(关键:用wb模式)
$fp = fopen($targetSlicePath, 'wb');
// 直接读取临时文件的原始二进制内容,无需任何编码转换
$rawContent = file_get_contents($sliceTmpPath);
fwrite($fp, $rawContent);
fclose($fp);

// 如果是最后一个分片,合并所有分片
if ($sliceIndex === $totalSlices - 1) {
  $finalFilePath = "./uploads/{$fileName}";
  $finalFp = fopen($finalFilePath, 'wb');
  for ($i = 0; $i < $totalSlices; $i++) {
    $slicePath = $tempDir . "{$fileName}_slice_{$i}";
    fwrite($finalFp, file_get_contents($slicePath));
    unlink($slicePath); // 删除临时分片
  }
  fclose($finalFp);
  rmdir($tempDir);
}

echo json_encode([
  'success' => true,
  'message' => "分片 {$sliceIndex} 上传成功"
]);
?>

对应直接发送ArrayBuffer的PHP处理(upload_raw.php)

如果前端直接发二进制数据,PHP需要从php://input读取原始请求体:

<?php
// 读取原始二进制请求体
$rawData = file_get_contents('php://input');
// 以二进制模式写入文件
file_put_contents('./temp_slice.bin', $rawData, FILE_BINARY);

echo json_encode(['success' => true]);
?>

三、为什么你之前的方法会失败?

你把Int8Array转成逗号分隔的字符串(比如"80,75,3,4..."),这个过程本身没问题,但当这个字符串通过HTTP传输时,浏览器会把它当作UTF-8文本发送。PHP接收时,会把这个字符串按UTF-8解析,而某些负数对应的字符在UTF-8中是无效的,会被替换成乱码字符,导致explode后的数字序列已经不是原始的Int8Array值了,最终pack出来的二进制数据自然错误。

四、关于断点续传的补充

实现断点续传只需要在上传前,让前端请求后端已上传的分片索引,然后跳过这些分片即可。可以用文件的MD5作为唯一标识(前端可以通过SparkMD5等库计算文件MD5),后端记录每个文件已上传的分片列表。

内容的提问来源于stack exchange,提问作者Richard

火山引擎 最新活动