前端开发必备:你不知道的Blob强大功能
做前端开发时,你可能遇到过这些问题:
用户上传的图片太大,想在前端压缩一下再传给后端,但不知道怎么处理
需要让用户下载文件,但不想每次都请求后端接口
大文件上传经常卡住,用户只能干等着
视频加载慢,播放不流畅
这些问题的解决,都和Blob有关。
下面是一个图片压缩的例子:5.91MB的图片,压缩后只有114KB,还可以更小。文章后面有完整的代码,可以直接复制使用。
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吧。从简单的图片压缩开始,慢慢探索更多应用场景。你会发现在前端处理文件可以如此简单高效。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!