引言:为什么需要图片压缩?
在网页开发中,图片通常都是页面加载性能的"隐形杀手",一张未经压缩的高清图片可能高达几MB,这通常导致图片加载非常缓慢,容易导致用户流失,于是,我们有了图片压缩技术。
图片压缩的目标:
- 减小文件体积:减少网络传输时间和带宽消耗。
- 保持视觉质量:在肉眼难以察觉的前提下优化图像。
- 适配不同设备:为手机、平板、桌面提供合适的分辨率。
本文将通过一个基于 Web Workers 的图片压缩工具,讲解技术实现的全过程,并逐行解析代码逻辑。
一、技术选型:Web Workers + Canvas
1. Web Workers:让主线程"松口气"
JavaScript 是单线程的,图片压缩涉及大量像素计算(如缩放、格式转换),如果在主线程执行,会导致页面卡顿。
Web Workers 的优势:
- 后台运行:独立于主线程,避免阻塞用户交互。
- 内存隔离 :通过
postMessage
传递数据,确保安全。
2. Canvas:离屏画布的妙用
HTML5 提供的 Canvas
允许在 Worker 线程中操作画布,无需依赖 DOM。
关键优势:
- 高性能渲染:直接操作像素数据,避免主线程的 UI 渲染压力。
- 支持异步操作:与 Web Workers 协同,实现复杂图像处理。
二、效果展示:
三、核心逻辑:前端与 Worker 的协作
代码展示:
javascript
// main.js
const worker = new Worker('./compressWorker.js');
worker.onmessage = function(e) {
console.log(e.data)
if (e.data.success) {
document.getElementById('output').innerHTML =
`<img src="${e.data.data}" style="max-width: 100%; height: auto;"/>`
} else {
document.getElementById('output').innerHTML =
`<p style="color: red;">压缩失败: ${e.data.data}</p>`
}
}
function handleFile(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
})
}
async function compressFile(file) {
const imgDataUrl = await handleFile(file);
console.log(imgDataUrl, '!!!!!!!!!!!!')
// 显示加载状态
document.getElementById('output').innerHTML = '<p>正在压缩图片...</p>'
worker.postMessage({
imgData: imgDataUrl,
quality: 0.5
})
}
const oFile = document.getElementById('fileInput');
oFile.addEventListener('change', async function(e) {
const file = e.target.files[0];
if (!file) return;
await compressFile(file);
})
代码解析:
1. 创建 Worker 实例
javascript
const worker = new Worker('./compressWorker.js');
- 作用 :在主线程中创建一个 Web Worker 实例,加载
compressWorker.js
脚本。 - 分析 :
Worker
是浏览器提供的 API,用于创建独立于主线程的后台线程。./compressWorker.js
是 Worker 线程的入口文件,所有图像压缩逻辑都写在这里。- 创建后,Worker 线程会立即运行,但此时还没有任何任务(需要通过
postMessage
触发)。
2. 监听文件选择事件
javascript
const oFile = document.getElementById('fileInput');
oFile.addEventListener('change', async function(e) {
const file = e.target.files[0];
if (!file) return;
await compressFile(file);
});
- 作用 :当用户通过
<input type="file">
选择文件后,触发异步处理流程。 - 分析 :
e.target.files[0]
获取用户选择的第一个文件(假设只允许上传一张图片)。if (!file) return;
防止用户未选择文件时继续执行后续代码。async
标记该函数为异步函数,允许使用await
关键字处理异步操作。await compressFile(file)
调用compressFile
函数处理文件。
3. 将文件转为 Base64 数据
javascript
async function compressFile(file) {
const imgDataUrl = await handleFile(file);
console.log(imgDataUrl, '!!!!!!!!!!!!')
// 显示加载状态
document.getElementById('output').innerHTML = '<p>正在压缩图片...</p>'
worker.postMessage({
imgData: imgDataUrl,
quality: 0.5
})
}
- 作用 :将用户上传的文件(如
image.jpg
)转为 Base64 格式的字符串(data:image/jpeg;base64,...
),便于传输给 Worker 线程。 - 分析 :
handleFile
是一个封装了FileReader
的函数,通过readAsDataURL
读取文件内容。await handleFile(file)
等待handleFile
返回结果后再继续执行后续代码(避免异步操作未完成就调用 Worker)。console.log(imgDataUrl, '!!!!!!!!!!!!')
是调试信息,用于确认 Base64 数据是否正确生成。worker.postMessage(...)
向 Worker 线程发送数据和压缩参数。
4. 接收 Worker 返回结果
javascript
worker.onmessage = function(e) {
console.log(e.data)
if (e.data.success) {
document.getElementById('output').innerHTML =
`<img src="${e.data.data}" style="max-width: 100%; height: auto;"/>`
} else {
document.getElementById('output').innerHTML =
`<p style="color: red;">压缩失败: ${e.data.data}</p>`
}
}
- 作用:监听 Worker 线程返回的消息,并根据结果更新页面。
- 分析 :
worker.onmessage
是主线程接收 Worker 消息的回调函数。e.data
是 Worker 返回的数据对象,包含:success
:布尔值,表示任务是否成功。data
:成功时是压缩后的 Base64 图片;失败时是错误信息。
- 成功时:
- 使用
<img>
标签展示压缩后的图片,自动适配屏幕宽度。
- 使用
- 失败时:
- 直接显示错误信息(如"压缩失败: 无效图片格式")。
console.log(e.data)
是调试信息,用于查看返回的数据结构。
5. 封装文件读取逻辑
javascript
function handleFile(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
})
}
- 作用 :将
FileReader
的异步操作封装为 Promise,便于使用await
。 - 分析 :
FileReader
是浏览器提供的 API,用于读取文件内容。readAsDataURL(file)
将文件读取为 Base64 字符串。onload
事件在读取完成后触发,调用resolve(reader.result)
结束 Promise。- 返回的 Promise 会被
await
捕获,得到最终的 Base64 数据。
三、Worker 线程:图像压缩的核心流程
代码展示:
javascript
// compressWorker.js
self.onmessage = async function (e) {
const { imgData, quality = 0.8 } = e.data
try {
// base64 -> blob
const bitmap = await createImageBitmap(
await (await fetch(imgData)).blob()
)
console.log(bitmap, '!!!!!!!!!!!!')
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
const ctx = canvas.getContext('2d')
ctx.drawImage(bitmap, 0, 0)
const compressedBlob = await canvas.convertToBlob(
{
type: 'image/jpeg',
quality: quality
}
)
const reader = new FileReader()
reader.onloadend = () => {
self.postMessage({
success: true,
data: reader.result
})
}
reader.readAsDataURL(compressedBlob)
} catch (err) {
self.postMessage({
success: false,
data: err.message
})
}
}
代码解析:
1. 接收主线程发送的数据
javascript
const { imgData, quality = 0.8 } = e.data
- 作用:从主线程发送的消息中提取图片数据和压缩质量参数。
- 分析 :
e.data
是主线程通过postMessage
发送的完整数据对象。- 使用解构赋值提取
imgData
(Base64 图片)和quality
(压缩质量,默认 0.8)。 - 如果用户未指定
quality
,则自动使用默认值 0.8(较高画质,较低压缩率)。
2. 将 Base64 转为二进制 Blob
javascript
const bitmap = await createImageBitmap(
await (await fetch(imgData)).blob()
)
- 作用 :将 Base64 格式的图片数据转为浏览器可操作的二进制格式(
Blob
)。 - 分析 :
fetch(imgData)
用 Fetch API 请求 Base64 数据(虽然数据已经在内存中,但这是标准转换方式)。.blob()
将响应结果转为Blob
对象(二进制大对象),这是后续处理图像的基础。await
确保转换完成后继续执行(异步操作需等待)。createImageBitmap(...)
将Blob
转为轻量级的ImageBitmap
。
3. 创建位图对象(ImageBitmap)
javascript
const bitmap = await createImageBitmap(blob);
- 作用 :将
Blob
转为轻量级的ImageBitmap
,便于后续绘制到画布。 - 分析 :
createImageBitmap()
是 HTML5 提供的 API,用于快速解析图像数据。ImageBitmap
是一种"只读"的图像表示形式,比普通图片对象更节省内存。- 该步骤完成后,图片数据已准备好用于画布操作。
console.log(bitmap, '!!!!!!!!!!!!')
是调试信息,用于确认位图是否正确生成。
4. 错误处理机制
javascript
catch (err) {
self.postMessage({
success: false,
data: err.message
});
}
- 作用:捕获任何异常并返回错误信息给主线程。
- 分析 :
- 如果 Base64 数据无效(如非图片格式),
fetch
或createImageBitmap
会抛出错误。 self.postMessage(...)
将错误信息以统一格式返回,主线程可据此显示提示。success: false
表示任务失败,data
字段包含具体错误原因(如"无效的图片数据")。
- 如果 Base64 数据无效(如非图片格式),
5. 创建离屏画布(OffscreenCanvas)
javascript
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
- 作用:在 Worker 线程中创建一个与原图尺寸相同的画布。
- 关键点 :
OffscreenCanvas
是 HTML5 提供的 API,允许在 Worker 中直接操作画布。- 与普通
canvas
不同,它不依赖 DOM,也不占用主线程资源。 - 创建时指定宽度和高度,确保画布大小与原图一致(为后续压缩做准备)。
6. 将位图绘制到画布
javascript
const ctx = canvas.getContext('2d')
ctx.drawImage(bitmap, 0, 0)
- 作用 :将
ImageBitmap
绘制到画布上,为压缩做准备。 - 分析 :
getContext('2d')
获取 2D 渲染上下文,用于操作像素。drawImage(bitmap, 0, 0)
将位图绘制到画布左上角(坐标 (0,0))。- 此时画布上已经完整保留了原图内容。
7. 压缩画布内容为 JPEG 格式
javascript
const compressedBlob = await canvas.convertToBlob({
type: 'image/jpeg',
quality: quality
});
- 作用:将画布内容压缩为 JPEG 格式的二进制数据。
- 分析 :
convertToBlob()
是OffscreenCanvas
提供的 API,支持指定输出格式和质量。type: 'image/jpeg'
表示输出为 JPEG 格式(支持有损压缩)。quality
参数控制压缩率(0.5 表示压缩到原图的一半体积,但会损失部分细节)。- 压缩后的数据以
Blob
形式返回,便于后续处理。
8. 返回压缩结果
javascript
const reader = new FileReader()
reader.onloadend = () => {
self.postMessage({
success: true,
data: reader.result
})
}
reader.readAsDataURL(compressedBlob)
- 作用 :将压缩后的
Blob
转为 Base64 字符串,便于主线程展示。 - 分析 :
FileReader
是浏览器提供的 API,用于读取文件/二进制数据。readAsDataURL(compressedBlob)
将Blob
转为data:image/jpeg;base64,...
格式的字符串。onloadend
事件在转换完成后触发,此时reader.result
包含完整的 Base64 数据。self.postMessage(...)
将结果返回给主线程,success: true
表示任务成功,data
字段是压缩后的图片数据。
四、常见提问及解答
1. 为什么主线程和 Worker 线程要分离?
- 主线程负责用户交互和页面渲染,如果执行图像压缩等耗时操作,会导致页面卡顿。
- Worker 线程在后台运行,避免阻塞主线程,提升用户体验。
2. 为什么使用 Canvas
?
- 普通
canvas
依赖 DOM,无法在 Worker 中直接操作。 Canvas
是 HTML5 提供的 API,专为 Worker 设计,支持像素级操作。
3. 如何优化压缩性能?
- 限制最大尺寸:在 Worker 中先缩放图像再压缩,减少像素计算量。
- 批量处理:支持多张图片同时压缩,提升吞吐量。