一、Blob对象概述

Blob(Binary Large Object,二进制大对象)是JavaScript中用于表示不可变原始数据的类文件对象,是现代Web开发中处理二进制数据的核心工具。Blob对象不关心数据的具体格式,可以存储任意类型的二进制数据,包括文本、图片、音频、视频等。

1.1 核心特性

不可变性:一旦创建,Blob对象的内容就无法修改,这保证了数据的安全性和一致性。

数据来源灵活:可以从字符串、ArrayBuffer、Uint8Array等多种数据源构建Blob对象。

MIME类型支持:创建时可以指定type属性,标识数据的媒体类型,便于后续处理。

二、Blob对象的创建与基本操作

2.1 构造函数语法

const blob = new Blob(blobParts, options);
  • blobParts:包含数据的数组,可以是ArrayBuffer、ArrayBufferView、Blob、DOMString等

  • options:可选参数对象,常用属性为type,指定MIME类型

2.2 创建示例

// 创建文本Blob
const textBlob = new Blob(['Hello, Blob!'], { type: 'text/plain' });

// 创建JSON数据Blob
const jsonData = { name: 'Blob Example', value: 42 };
const jsonBlob = new Blob([JSON.stringify(jsonData, null, 2)], { 
  type: 'application/json' 
});

// 从ArrayBuffer创建
const buffer = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
const bufferBlob = new Blob([buffer], { type: 'text/plain' });

2.3 基本属性与方法

属性/方法

说明

示例

size

Blob对象的数据大小(字节)

blob.size

type

MIME类型字符串

blob.type

slice(start, end, contentType)

创建子Blob对象

blob.slice(0, 10)

arrayBuffer()

返回Promise,将Blob转为ArrayBuffer

blob.arrayBuffer().then(...)

text()

返回Promise,解析为文本数据

blob.text().then(...)

stream()

返回可读流,用于逐步读取

blob.stream().getReader()

三、Blob与相关数据类型的转换

3.1 Blob与ArrayBuffer互转

// ArrayBuffer转Blob
const buffer = new ArrayBuffer(16);
const blobFromBuffer = new Blob([buffer], { type: 'application/octet-stream' });

// Blob转ArrayBuffer(方法一:使用FileReader)
function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(blob);
  });
}

// Blob转ArrayBuffer(方法二:使用arrayBuffer()方法)
blob.arrayBuffer().then(buffer => {
  console.log(buffer);
});

3.2 Blob与File的关系

File对象继承自Blob,增加了文件名、最后修改时间等元数据信息:

// File转Blob(File本身就是Blob的子类)
const file = document.getElementById('fileInput').files[0];
console.log(file instanceof Blob); // true

// Blob转File
const blob = new Blob(['Hello, File!'], { type: 'text/plain' });
const file = new File([blob], 'hello.txt', { 
  type: 'text/plain', 
  lastModified: new Date() 
});

3.3 Blob与Base64互转

// Blob转Base64
function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}

// Base64转Blob
function base64ToBlob(base64, mimeType) {
  const byteCharacters = atob(base64.split(',')[1]);
  const byteNumbers = new Array(byteCharacters.length);
  for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
  }
  const byteArray = new Uint8Array(byteNumbers);
  return new Blob([byteArray], { type: mimeType });
}

四、Blob的核心应用场景

4.1 文件下载

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  
  // 清理内存
  setTimeout(() => {
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }, 100);
}

// 使用示例
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
downloadBlob(blob, 'hello.txt');

4.2 图片预览

const fileInput = document.getElementById('fileInput');
const previewImg = document.getElementById('preview');

fileInput.addEventListener('change', (event) => {
  const file = event.target.files[0];
  if (file) {
    const url = URL.createObjectURL(file);
    previewImg.src = url;
    
    // 图片加载完成后释放URL
    previewImg.onload = () => {
      URL.revokeObjectURL(url);
    };
  }
});

4.3 大文件分片上传

class ChunkUploader {
  constructor(file, options = {}) {
    this.file = file;
    this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 默认2MB
    this.uploadUrl = options.uploadUrl;
    this.onProgress = options.onProgress;
    this.uploadedChunks = new Set();
  }

  // 创建文件分片
  createChunks() {
    const chunks = [];
    let start = 0;
    while (start < this.file.size) {
      const end = Math.min(start + this.chunkSize, this.file.size);
      chunks.push({
        index: chunks.length,
        blob: this.file.slice(start, end),
        start,
        end
      });
      start = end;
    }
    return chunks;
  }

  // 上传单个分片
  async uploadChunk(chunk) {
    const formData = new FormData();
    formData.append('file', chunk.blob);
    formData.append('chunkIndex', chunk.index);
    formData.append('totalChunks', this.totalChunks);
    formData.append('fileId', this.fileId);

    await fetch(this.uploadUrl, {
      method: 'POST',
      body: formData
    });

    this.uploadedChunks.add(chunk.index);
    
    if (this.onProgress) {
      this.onProgress({
        uploaded: this.uploadedChunks.size,
        total: this.totalChunks,
        percentage: Math.round((this.uploadedChunks.size / this.totalChunks) * 100)
      });
    }
  }

  // 并发上传控制
  async uploadWithConcurrency(concurrency = 3) {
    const chunks = this.createChunks();
    this.totalChunks = chunks.length;
    this.fileId = Date.now() + '_' + Math.random().toString(36).substr(2);

    const uploadQueue = [...chunks];
    const workers = Array.from({ length: concurrency }, async () => {
      while (uploadQueue.length > 0) {
        const chunk = uploadQueue.shift();
        await this.uploadChunk(chunk);
      }
    });

    await Promise.all(workers);
    
    // 通知服务器合并文件
    await this.mergeFile();
  }

  async mergeFile() {
    const response = await fetch(`${this.uploadUrl}/merge`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileId: this.fileId,
        totalChunks: this.totalChunks,
        fileName: this.file.name
      })
    });
    return response.json();
  }
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (file) {
    const uploader = new ChunkUploader(file, {
      uploadUrl: '/api/upload',
      chunkSize: 5 * 1024 * 1024, // 5MB
      onProgress: (progress) => {
        console.log(`上传进度: ${progress.percentage}%`);
      }
    });
    await uploader.uploadWithConcurrency(3);
  }
});

4.4 断点续传实现

class ResumeUploader extends ChunkUploader {
  constructor(file, options = {}) {
    super(file, options);
    this.storageKey = `upload_${this.file.name}_${this.file.size}`;
  }

  // 检查已上传的分片
  async checkUploadedChunks() {
    try {
      const saved = localStorage.getItem(this.storageKey);
      if (saved) {
        const data = JSON.parse(saved);
        this.uploadedChunks = new Set(data.uploadedChunks);
        return data.uploadedChunks;
      }
    } catch (error) {
      console.error('读取上传记录失败:', error);
    }
    return [];
  }

  // 保存上传进度
  saveProgress() {
    const data = {
      fileId: this.fileId,
      uploadedChunks: Array.from(this.uploadedChunks),
      totalChunks: this.totalChunks
    };
    localStorage.setItem(this.storageKey, JSON.stringify(data));
  }

  // 上传单个分片(重写)
  async uploadChunk(chunk) {
    if (this.uploadedChunks.has(chunk.index)) {
      return; // 跳过已上传的分片
    }

    await super.uploadChunk(chunk);
    this.saveProgress(); // 保存进度
  }

  // 上传完成后清理
  async mergeFile() {
    const result = await super.mergeFile();
    localStorage.removeItem(this.storageKey);
    return result;
  }
}

4.5 Canvas图片处理

// 图片压缩
function compressImage(file, quality = 0.8) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      
      // 计算压缩后的尺寸
      let width = img.width;
      let height = img.height;
      const maxSize = 1024; // 最大边长
      
      if (width > height) {
        if (width > maxSize) {
          height = Math.round(height * maxSize / width);
          width = maxSize;
        }
      } else {
        if (height > maxSize) {
          width = Math.round(width * maxSize / height);
          height = maxSize;
        }
      }
      
      canvas.width = width;
      canvas.height = height;
      ctx.drawImage(img, 0, 0, width, height);
      
      // 转换为Blob
      canvas.toBlob(
        (blob) => resolve(blob),
        'image/jpeg',
        quality
      );
    };
    img.onerror = reject;
    img.src = URL.createObjectURL(file);
  });
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (file && file.type.startsWith('image/')) {
    const compressedBlob = await compressImage(file, 0.7);
    console.log(`压缩前: ${file.size} bytes`);
    console.log(`压缩后: ${compressedBlob.size} bytes`);
    console.log(`压缩率: ${((file.size - compressedBlob.size) / file.size * 100).toFixed(2)}%`);
  }
});

4.6 音频录制

class AudioRecorder {
  constructor() {
    this.mediaRecorder = null;
    this.audioChunks = [];
    this.isRecording = false;
  }

  async startRecording() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      this.mediaRecorder = new MediaRecorder(stream);
      this.audioChunks = [];
      
      this.mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          this.audioChunks.push(event.data);
        }
      };
      
      this.mediaRecorder.onstop = () => {
        const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
        this.onRecordingComplete(audioBlob);
      };
      
      this.mediaRecorder.start();
      this.isRecording = true;
    } catch (error) {
      console.error('无法访问麦克风:', error);
    }
  }

  stopRecording() {
    if (this.mediaRecorder && this.isRecording) {
      this.mediaRecorder.stop();
      this.isRecording = false;
    }
  }

  onRecordingComplete(blob) {
    const url = URL.createObjectURL(blob);
    const audio = document.createElement('audio');
    audio.src = url;
    audio.controls = true;
    document.body.appendChild(audio);
    
    // 清理内存
    audio.onload = () => {
      URL.revokeObjectURL(url);
    };
  }
}

// 使用示例
const recorder = new AudioRecorder();
document.getElementById('startBtn').addEventListener('click', () => {
  recorder.startRecording();
});
document.getElementById('stopBtn').addEventListener('click', () => {
  recorder.stopRecording();
});

五、性能优化与内存管理

5.1 内存泄漏防范

class BlobMemoryManager {
  constructor() {
    this.urlMap = new Map();
    this.autoCleanupInterval = 60000; // 1分钟
    this.maxUrlAge = 300000; // 5分钟
    this.startAutoCleanup();
    
    // 页面卸载时清理所有URL
    window.addEventListener('beforeunload', () => {
      this.cleanup();
    });
  }

  createObjectURL(blob, key) {
    const url = URL.createObjectURL(blob);
    this.urlMap.set(url, {
      key,
      createdAt: Date.now(),
      blob
    });
    return url;
  }

  revokeObjectURL(url) {
    if (this.urlMap.has(url)) {
      URL.revokeObjectURL(url);
      this.urlMap.delete(url);
    }
  }

  revokeByKey(key) {
    for (const [url, data] of this.urlMap.entries()) {
      if (data.key === key) {
        this.revokeObjectURL(url);
      }
    }
  }

  cleanup() {
    for (const url of this.urlMap.keys()) {
      URL.revokeObjectURL(url);
    }
    this.urlMap.clear();
  }

  startAutoCleanup() {
    setInterval(() => {
      const now = Date.now();
      for (const [url, data] of this.urlMap.entries()) {
        if (now - data.createdAt > this.maxUrlAge) {
          this.revokeObjectURL(url);
        }
      }
    }, this.autoCleanupInterval);
  }
}

// 使用示例
const memoryManager = new BlobMemoryManager();
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const url = memoryManager.createObjectURL(blob, 'test-blob');

// 使用完成后手动释放
memoryManager.revokeObjectURL(url);

5.2 流式处理大文件

// 流式读取大文件
async function processLargeFile(file, onProgress) {
  const chunkSize = 1024 * 1024; // 1MB
  const totalChunks = Math.ceil(file.size / chunkSize);
  let processedChunks = 0;
  
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    // 处理当前分片
    await processChunk(chunk, i);
    
    processedChunks++;
    if (onProgress) {
      onProgress({
        processed: processedChunks,
        total: totalChunks,
        percentage: Math.round((processedChunks / totalChunks) * 100)
      });
    }
  }
}

// 使用ReadableStream处理
async function streamProcessFile(file) {
  const stream = file.stream();
  const reader = stream.getReader();
  const decoder = new TextDecoder('utf-8');
  
  let done = false;
  while (!done) {
    const { value, done: streamDone } = await reader.read();
    done = streamDone;
    
    if (value) {
      const text = decoder.decode(value, { stream: true });
      // 处理文本数据
      console.log(text);
    }
  }
}

5.3 并发控制与错误重试

class ConcurrentUploader {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.queue = [];
    this.activeCount = 0;
  }

  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    while (this.queue.length > 0 && this.activeCount < this.maxConcurrent) {
      const { task, resolve, reject } = this.queue.shift();
      this.activeCount++;
      
      try {
        const result = await this.executeWithRetry(task);
        resolve(result);
      } catch (error) {
        reject(error);
      } finally {
        this.activeCount--;
        this.processQueue();
      }
    }
  }

  async executeWithRetry(task, maxRetries = 3) {
    let retries = 0;
    let lastError;
    
    while (retries <= maxRetries) {
      try {
        return await task();
      } catch (error) {
        lastError = error;
        retries++;
        
        if (retries <= maxRetries) {
          // 指数退避策略
          const delay = Math.pow(2, retries) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
    
    throw lastError;
  }
}

// 使用示例
const uploader = new ConcurrentUploader(3);
const chunks = createChunks(largeFile);

for (const chunk of chunks) {
  uploader.add(async () => {
    await uploadChunk(chunk);
  });
}

六、最佳实践与注意事项

6.1 兼容性处理

// 检查Blob支持
if (typeof Blob === 'undefined') {
  console.error('当前浏览器不支持Blob API');
  // 提供降级方案
}

// 检查slice方法(旧浏览器可能需要前缀)
const slice = Blob.prototype.slice || 
             Blob.prototype.mozSlice || 
             Blob.prototype.webkitSlice;

// 使用兼容的slice方法
const chunk = slice.call(file, start, end);

6.2 安全注意事项

文件类型验证

function isValidFileType(file, allowedTypes) {
  return allowedTypes.some(type => file.type.startsWith(type));
}

// 只允许图片文件
const file = document.getElementById('fileInput').files[0];
if (!isValidFileType(file, ['image/jpeg', 'image/png', 'image/gif'])) {
  alert('请选择有效的图片文件');
  return;
}

文件大小限制

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_FILE_SIZE) {
  alert('文件大小不能超过10MB');
  return;
}

6.3 性能优化清单

  1. 使用流式处理:对于大文件(>100MB),始终使用流式处理而非一次性加载

  2. 及时释放内存:使用URL.revokeObjectURL()释放不再需要的Blob URL

  3. 并发控制:合理控制并发上传数量(通常3-5个)

  4. 错误重试机制:实现指数退避重试策略

  5. 进度反馈:提供实时进度指示器,提升用户体验

  6. 缓存策略:使用LRU缓存管理频繁访问的Blob数据

  7. Web Workers:将CPU密集型任务(如MD5计算)放到Worker线程中执行

七、总结

Blob对象作为前端处理二进制数据的核心工具,在现代Web开发中发挥着重要作用。通过掌握Blob的创建、转换、应用场景以及性能优化技巧,开发者可以构建出高效、稳定、用户体验良好的文件处理功能。无论是大文件上传、图片处理、音频录制还是数据导出,Blob都提供了强大的能力支持。

在实际开发中,建议遵循以下原则:

  • 对于大文件操作,始终采用分片和流式处理

  • 严格管理内存,及时释放不再使用的Blob URL

  • 实现完善的错误处理和重试机制

  • 提供良好的用户反馈(进度条、错误提示等)

  • 考虑浏览器兼容性和性能优化

通过合理运用Blob API,可以显著提升Web应用的文件处理能力,为用户提供更流畅的体验。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐