解决Uniapp中文件切片问题

798 阅读2分钟

背景: uniapp打包成app时,需要分片上传的场景,未提供完整api,只能自己摸索

在 UniApp 的 HTML5+ API(plus.io)  中,确实没有直接提供 readAsArrayBuffer 方法。这与标准浏览器环境中的 FileReader 不同,后者支持 readAsArrayBuffer 用于直接读取文件的二进制数据


UniApp 中的替代方案

步骤1:使用 readAsDataURL + Base64 转 ArrayBuffer

问:为什么要去除dataUrl头部?

答:在 DataURL 中,头部(Header)  描述了数据的类型和编码方式,而 主体(Body)  才是真正的 Base64 编码内容。去除头部的目的是为了获取 纯 Base64 字符串,以便后续处理(如转 ArrayBuffer)。以下是详细解释

const dataURL = 'data:image/png;base64,iVBORw0KGgo...';
const base64 = dataURL.split(',')[1]; // 提取逗号后的部分
console.log(base64); // 输出:iVBORw0KGgo...

代码如下:


// 读取文件切片
export function readFileSlice(arrayBuffer, start, end) {
  return new Promise((resolve, reject) => {
    //#ifdef H5
    resolve(arrayBuffer.slice(start, end));
    //#endif

    //#ifdef APP-PLUS
    const reader = new plus.io.FileReader();
    const sliceFile = arrayBuffer.slice(start, end);
    reader.readAsDataURL(sliceFile); // 读取为base64
    reader.onloadend = (e) => {
      const arrayBuffer = dataURLToArrayBuffer(e.target?.result);
      console.log('分片大小:', end - start + 1, '转换后字节数:', arrayBuffer.byteLength);
      resolve(arrayBuffer);
    };
    reader.onerror = (e) => {
      console.error('读取错误:', e.target.error);
      reject(false);
    };
    //#endif
  });
}

// DataURL转ArrayBuffer
function dataURLToArrayBuffer(dataURL) {
  const commaIndex = dataURL.indexOf(','); // 查找第一个逗号位置
  if (commaIndex === -1) throw new Error('Invalid DataURL');
  const base64 = dataURL.substring(commaIndex + 1); // 精确截取Base64部分
  return uni.base64ToArrayBuffer(base64);
}

2:分片上传到服务端后文件为啥多了几个字节?

切片上传后无法播放.png

有的可以播放,有的不能播放,分析原因:

使用对比工具发现,多出的字节都与前一个字节相同

多出一个字节.jpg

联想到是不是每个切片开头结尾有问题,去查看官网 HTML5+ API Reference (html5plus.org) 官网slice切片.jpg

破案了,html5+api中slice,不是开区间,包含end

解决办法:每个切换end-1,最后一个切片不减

// 获取切片end
export function getSliceEnd(start, chunkSize, fileSize, index, totalChunks) {
  //#ifdef H5
  return Math.min(start + chunkSize, fileSize);
  //#endif
  //#ifdef APP-PLUS
  return index < totalChunks - 1 ? start + chunkSize - 1 : fileSize;
  //#endif
}

完整代码如下:

// 读取文件内容(跨平台处理H5,app)
export function readFile(fileInfo) {
  return new Promise((resolve, reject) => {
    //#ifdef H5
    if (fileInfo) {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(fileInfo);
    }
    //#endif

    //#ifdef APP-PLUS
    const filepath = fileInfo.url || fileInfo.path;
    if (filepath) {
      plus.io.resolveLocalFileSystemURL(
        filepath,
        (fileEntry) => {
          fileEntry.file(
            (file) => {
              console.log('获取文件对象成功:', file);
              resolve(file);
            },
            (error) => {
              console.error('获取文件对象失败:', error);
              reject(error);
            },
          );
        },
        (error) => {
          console.error('解析路径失败:', error);
        },
      );
    }
    //#endif
  });
}

// 获取切片end
export function getSliceEnd(start, chunkSize, fileSize, index, totalChunks) {
  //#ifdef H5
  return Math.min(start + chunkSize, fileSize);
  //#endif
  //#ifdef APP-PLUS
  return index < totalChunks - 1 ? start + chunkSize - 1 : fileSize;
  //#endif
}

// 切片逻辑
const fileBuffer = await readFile(file);
// Upload each part.
const partsInfo = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * this.config.chunkSize;
const end = getSliceEnd(start, this.config.chunkSize, file.size, i, totalChunks);
// 获取到chunk 数据(arraybuffer)
  const chunk = await readFileSlice(fileBuffer, start, end);
  ...
}

3:切片后也可以写入到文件,demo如下

// 保存ArrayBuffer为临时文件
function saveArrayBufferToTempFile(arrayBuffer) {
// 将 ArrayBuffer 转换为 H5+ 字节数组
    const byteArray = new plus.io.ByteArray();
    byteArray.setBuffer(arrayBuffer); // 直接传入 ArrayBuffer

  return new Promise((resolve, reject) => {
    const tempPath = '_doc/' + Date.now() + '_chunk.tmp';
    plus.io.resolveLocalFileSystemURL('_doc', (dirEntry) => {
      dirEntry.getFile(tempPath, { create: true }, (fileEntry) => {
        fileEntry.createWriter((writer) => {
          writer.onwrite = () => resolve(tempPath);
          writer.onerror = reject;
          //const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });
          writer.write(byteArray);
        }, reject);
      }, reject);
    }, reject);
  });
}