前端开发必备:你不知道的Blob强大功能

更新日期: 2026-01-02 阅读: 215 标签: Blob

前端开发时,你可能遇到过这些问题:

  • 用户上传的图片太大,想在前端压缩一下再传给后端,但不知道怎么处理

  • 需要让用户下载文件,但不想每次都请求后端接口

  • 大文件上传经常卡住,用户只能干等着

  • 视频加载慢,播放不流畅

这些问题的解决,都和Blob有关。

下面是一个图片压缩的例子:5.91MB的图片,压缩后只有114KB,还可以更小。文章后面有完整的代码,可以直接复制使用。


Blob到底是什么?

简单说,Blob就是浏览器里的二进制数据对象。

每次你上传文件,那个File对象其实就是Blob的一种。但Blob的能力,不止上传文件这么简单。

// 创建一个简单的Blob
const simpleBlob = new Blob(['你好,世界!'], { type: 'text/plain' })

两行代码,就创建了一个文本数据的Blob对象。关键是,这个对象可以在前端直接操作,不需要经过后端。


为什么要用Blob?

场景1:图片压缩上传

用户上传5MB的照片,但后端只接受1MB以内的图片。怎么办?

传统做法是传上去让后端压缩,再告诉用户“文件太大”。体验不好。

用Blob可以这样做:

// 压缩图片函数
async function compressImage(file, maxWidth = 800, quality = 0.7) {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    
    reader.onload = (e) => {
      const img = new Image()
      img.src = e.target.result
      
      img.onload = () => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        
        // 计算等比例缩放
        let width = img.width
        let height = img.height
        if (width > maxWidth) {
          height = (maxWidth / width) * height
          width = maxWidth
        }
        
        canvas.width = width
        canvas.height = height
        
        // 绘制到canvas
        ctx.drawImage(img, 0, 0, width, height)
        
        // canvas转Blob
        canvas.toBlob(
          (blob) => resolve(blob),
          'image/jpeg',
          quality
        )
      }
    }
  })
}

// 使用
const fileInput = document.getElementById('upload')
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0]
  const compressedBlob = await compressImage(file)
  
  // 上传压缩后的blob
  const formData = new FormData()
  formData.append('image', compressedBlob, 'compressed.jpg')
  
  // 上传到服务器
  await fetch('/api/upload', {
    method: 'POST',
    body: formData
  })
})

关键点:整个过程在前端完成,用户不需要等待后端返回错误信息。

场景2:大文件分片上传

上传2GB的视频,网络一断就要重头开始?这样用户体验很差。

class BigFileUploader {
  constructor(file, chunkSize = 1024 * 1024) { // 默认1MB一片
    this.file = file
    this.chunkSize = chunkSize
    this.totalChunks = Math.ceil(file.size / chunkSize)
    this.currentChunk = 0
  }
  
  async upload() {
    while (this.currentChunk < this.totalChunks) {
      const start = this.currentChunk * this.chunkSize
      const end = Math.min(start + this.chunkSize, this.file.size)
      
      // 关键:用slice切割Blob
      const chunk = this.file.slice(start, end)
      
      const formData = new FormData()
      formData.append('chunk', chunk)
      formData.append('chunkIndex', this.currentChunk)
      formData.append('totalChunks', this.totalChunks)
      formData.append('fileName', this.file.name)
      
      try {
        await fetch('/api/upload-chunk', {
          method: 'POST',
          body: formData
        })
        
        this.currentChunk++
        
        // 更新进度条
        const progress = (this.currentChunk / this.totalChunks) * 100
        this.updateProgress(progress)
        
      } catch (error) {
        console.log('上传失败,可以从当前分片续传')
        break
      }
    }
  }
}

Blob.slice()是关键,它能让我们像切面包一样切割大文件,而且不占用太多内存。

场景3:前端生成文件并下载

用户填了表格,想导出Excel。传统做法是请求后端生成文件。

其实前端自己就能完成:

function downloadCSV(data, filename = 'data.csv') {
  // 准备CSV内容
  let csvContent = ''
  
  // 添加表头
  const headers = Object.keys(data[0])
  csvContent += headers.join(',') + '\n'
  
  // 添加数据行
  data.forEach(row => {
    const values = headers.map(header => {
      const value = row[header]
      return `"${value}"`  // 用引号包裹,避免逗号问题
    })
    csvContent += values.join(',') + '\n'
  })
  
  // 创建Blob
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
  
  // 创建下载链接
  const link = document.createElement('a')
  const url = URL.createObjectURL(blob)
  
  link.href = url
  link.download = filename
  link.style.display = 'none'
  
  document.body.appendChild(link)
  link.click()
  
  // 清理
  setTimeout(() => {
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
  }, 100)
}

URL.createObjectURL(blob)是核心,它给Blob创建了一个临时地址,让浏览器可以像访问网络资源一样访问本地Blob。


Blob的高级用法

1. 视频流处理

做视频网站时,不能让用户等太久。

// 视频流式播放
async function streamVideo(videoUrl, videoElement) {
  const response = await fetch(videoUrl)
  const reader = response.body.getReader()
  
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    
    // 将数据块转为Blob
    const blob = new Blob([value])
    const blobUrl = URL.createObjectURL(blob)
    
    // 动态更新视频源
    videoElement.src = blobUrl
    
    // 清理之前的URL
    if (videoElement.previousUrl) {
      URL.revokeObjectURL(videoElement.previousUrl)
    }
    videoElement.previousUrl = blobUrl
  }
}

2. 浏览器数据库存储

localStorage只能存字符串,但有了Blob,我们能存更多类型:

// 在IndexedDB中存储文件
async function saveFileToDB(file) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('myFiles', 1)
    
    request.onupgradeneeded = (e) => {
      const db = e.target.result
      if (!db.objectStoreNames.contains('files')) {
        const store = db.createObjectStore('files', { keyPath: 'id' })
        store.createIndex('name', 'name', { unique: false })
      }
    }
    
    request.onsuccess = (e) => {
      const db = e.target.result
      const transaction = db.transaction(['files'], 'readwrite')
      const store = transaction.objectStore('files')
      
      const fileData = {
        id: Date.now(),
        name: file.name,
        type: file.type,
        data: file,
        size: file.size,
        lastModified: file.lastModified
      }
      
      store.put(fileData)
      resolve()
    }
    
    request.onerror = (e) => {
      reject(e.target.error)
    }
  })
}


性能与安全

1. 内存管理

Blob占用内存,用完记得释放:

// 创建Blob URL
const blob = new Blob(['一些大数据'])
const blobUrl = URL.createObjectURL(blob)

// 使用...

// 用完一定要释放!
URL.revokeObjectURL(blobUrl)

忘记revokeObjectURL是常见的内存泄漏原因。

2. 安全限制

Blob不是万能的,浏览器有安全策略:

  • 同源策略:Blob URL遵循同源策略

  • 大小限制:不同浏览器有不同限制

  • 类型验证:浏览器会检查MIME类型是否匹配内容


完整图片压缩工具

下面是一个完整的图片压缩工具代码,可以直接使用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>图片压缩工具</title>
    <style>
        .container { max-width: 800px; margin: 0 auto; padding: 20px; }
        .upload-area { 
            border: 2px dashed #ccc; 
            padding: 40px; 
            text-align: center; 
            margin-bottom: 20px;
            cursor: pointer;
        }
        .preview { display: flex; gap: 20px; margin: 20px 0; }
        .preview-box { flex: 1; border: 1px solid #ddd; padding: 10px; }
        .preview-img { max-width: 100%; max-height: 200px; }
        .controls { margin: 20px 0; }
        .btn { 
            padding: 10px 20px; 
            background: #007bff; 
            color: white; 
            border: none; 
            cursor: pointer;
            margin-right: 10px;
        }
        .btn:disabled { background: #ccc; cursor: not-allowed; }
    </style>
</head>
<body>
    <div class="container">
        <h1>图片压缩工具</h1>
        
        <div class="upload-area" id="dropArea">
            <p>点击或拖拽上传图片</p>
            <input type="file" id="fileInput" accept="image/*" hidden>
        </div>
        
        <div class="controls">
            <div>
                <label>质量: <span id="qualityValue">80</span>%</label>
                <input type="range" id="quality" min="10" max="100" value="80">
            </div>
            <div>
                <label>宽度: <span id="widthValue">800</span>px</label>
                <input type="range" id="maxWidth" min="200" max="2000" value="800" step="50">
            </div>
        </div>
        
        <div class="preview">
            <div class="preview-box">
                <h3>原图</h3>
                <img id="originalImg" class="preview-img">
                <p id="originalInfo"></p>
            </div>
            <div class="preview-box">
                <h3>压缩后</h3>
                <img id="compressedImg" class="preview-img">
                <p id="compressedInfo"></p>
            </div>
        </div>
        
        <button id="compressBtn" class="btn" disabled>压缩</button>
        <button id="downloadBtn" class="btn" disabled>下载</button>
    </div>

    <script>
        const fileInput = document.getElementById('fileInput')
        const dropArea = document.getElementById('dropArea')
        const compressBtn = document.getElementById('compressBtn')
        const downloadBtn = document.getElementById('downloadBtn')
        const qualitySlider = document.getElementById('quality')
        const maxWidthSlider = document.getElementById('maxWidth')
        const qualityValue = document.getElementById('qualityValue')
        const widthValue = document.getElementById('widthValue')
        const originalImg = document.getElementById('originalImg')
        const compressedImg = document.getElementById('compressedImg')
        const originalInfo = document.getElementById('originalInfo')
        const compressedInfo = document.getElementById('compressedInfo')
        
        let originalFile = null
        let compressedBlob = null
        
        // 更新显示
        qualitySlider.addEventListener('input', () => {
            qualityValue.textContent = qualitySlider.value
        })
        
        maxWidthSlider.addEventListener('input', () => {
            widthValue.textContent = maxWidthSlider.value
        })
        
        // 上传区域点击
        dropArea.addEventListener('click', () => fileInput.click())
        
        // 拖拽上传
        dropArea.addEventListener('dragover', (e) => {
            e.preventDefault()
            dropArea.style.borderColor = '#007bff'
        })
        
        dropArea.addEventListener('dragleave', () => {
            dropArea.style.borderColor = '#ccc'
        })
        
        dropArea.addEventListener('drop', (e) => {
            e.preventDefault()
            dropArea.style.borderColor = '#ccc'
            if (e.dataTransfer.files.length) {
                handleFileSelect(e.dataTransfer.files[0])
            }
        })
        
        // 文件选择
        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length) {
                handleFileSelect(e.target.files[0])
            }
        })
        
        function handleFileSelect(file) {
            if (!file.type.startsWith('image/')) {
                alert('请选择图片文件')
                return
            }
            
            originalFile = file
            
            // 显示原图
            const url = URL.createObjectURL(file)
            originalImg.src = url
            originalInfo.textContent = `大小: ${formatSize(file.size)}`
            
            compressBtn.disabled = false
            downloadBtn.disabled = true
        }
        
        // 压缩图片
        compressBtn.addEventListener('click', async () => {
            if (!originalFile) return
            
            compressBtn.disabled = true
            compressBtn.textContent = '压缩中...'
            
            try {
                const quality = parseInt(qualitySlider.value) / 100
                const maxWidth = parseInt(maxWidthSlider.value)
                
                compressedBlob = await compressImage(originalFile, quality, maxWidth)
                
                // 显示压缩后的图片
                const url = URL.createObjectURL(compressedBlob)
                compressedImg.src = url
                compressedInfo.textContent = `大小: ${formatSize(compressedBlob.size)}`
                
                downloadBtn.disabled = false
                alert('压缩完成!')
            } catch (error) {
                alert('压缩失败:' + error.message)
            } finally {
                compressBtn.disabled = false
                compressBtn.textContent = '压缩'
            }
        })
        
        // 下载
        downloadBtn.addEventListener('click', () => {
            if (!compressedBlob) return
            
            const url = URL.createObjectURL(compressedBlob)
            const a = document.createElement('a')
            a.href = url
            a.download = 'compressed_' + originalFile.name
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            
            // 清理URL
            setTimeout(() => URL.revokeObjectURL(url), 100)
        })
        
        // 工具函数
        function formatSize(bytes) {
            if (bytes < 1024) return bytes + ' B'
            if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
            return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
        }
        
        async function compressImage(file, quality, maxWidth) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader()
                reader.readAsDataURL(file)
                
                reader.onload = (e) => {
                    const img = new Image()
                    img.src = e.target.result
                    
                    img.onload = () => {
                        const canvas = document.createElement('canvas')
                        const ctx = canvas.getContext('2d')
                        
                        let width = img.width
                        let height = img.height
                        
                        if (width > maxWidth) {
                            height = (maxWidth / width) * height
                            width = maxWidth
                        }
                        
                        canvas.width = width
                        canvas.height = height
                        
                        ctx.drawImage(img, 0, 0, width, height)
                        
                        canvas.toBlob(
                            (blob) => {
                                if (blob) {
                                    resolve(blob)
                                } else {
                                    reject(new Error('压缩失败'))
                                }
                            },
                            'image/jpeg',
                            quality
                        )
                    }
                    
                    img.onerror = () => reject(new Error('图片加载失败'))
                }
                
                reader.onerror = () => reject(new Error('文件读取失败'))
            })
        }
    </script>
</body>
</html>


使用建议

1. 适合的场景

  • 用户头像上传:先压缩再上传,节省服务器带宽

  • 内容管理系统:编辑文章时压缩配图

  • 移动端拍照上传:手机拍的照片通常很大,前端压缩后再传

  • 图床工具:压缩后上传到图床,提高加载速度

2. 注意事项

  • 兼容性:canvas.toBlob()在旧浏览器中可能需要polyfill

  • EXIF信息:压缩后会丢失图片的拍摄信息

  • 透明背景:转成JPEG会丢失透明信息,PNG则保留

  • 性能:大图片压缩时添加加载提示

3. 优化建议

  • 质量设置:80%是最佳平衡点,文件小且质量损失不明显

  • 宽度限制:800px适合网页展示,1920px适合高清需求

  • 内存管理:及时调用URL.revokeObjectURL()释放内存

Blob的强大之处,在于它让浏览器有了直接处理二进制数据的能力。用好Blob,可以大大提升前端应用的用户体验。开始尝试在你的项目中使用Blob吧。从简单的图片压缩开始,慢慢探索更多应用场景。你会发现在前端处理文件可以如此简单高效。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13321

相关推荐

js blob流和base64,以及file和base64的相互转换

file文件转换为base64,得到base64格式图片;base64转换为file;base64转换为blob流;blob流转换为base64;测试案例,可直接复制运行

html5中二进制对象Blob的使用——Blob与ArrayBuffer、TypeArray和String的相互转换

html5中Blob是什么?js如何创建Blob对象?Blob类型转换:String转换成Blob对象、TypeArray转换成 Blob对象 、ArrayBuffer转Blob、将Blob对象转换成String字符串,使用FileReader的readAsText方法 、将Blob对象转换成ArrayBuffer

前端js实现Blob、DataURL、canvas、image的相互转换

canvas转dataURL:canvas对象、转换格式、图像品质;DataURL转canvas;image转canvas:图片地址;dataURL转image,这个不需要转,直接给了src就能用

原生JS上传图片接收服务器端图片并且显示图片(主要描述blob类型)

图片以独立文件的形式存储在服务器的指定文件夹中,再将路径存入数据库字段中;将图片转换成blob,直接存储到数据库的 Image 类型字段中(这种方式负担很大不建议使用)

通过Blob实现文件下载

最近遇到一个需求,需要将页面中的配置信息下载下来供用户方便使用,以前这个场景的需求有时候会放到后端处理,然后给返回一个下载链接。其实并不需要这么麻烦,这样既增大了服务器的负载

ArrayBuffer 和 Blob 对象

ArrayBuffer 对象与 Blob 对象大家或许不太陌生,常见于文件上传操作处理(如处理图片上传预览等问题)。那么本文将与大家深入介绍两者。ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范

你不知道的 Blob

Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据

大文件预览选择Blob URL的原因

在网页开发中,我们经常需要让用户预览他们上传的大文件,比如高清图片、视频或文档。处理这种情况时,Blob URL是一个非常有用的工具。使用时要注意及时释放内存,避免内存泄漏。对于不同的文件类型,可以采用相应的优化策略。

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!