Web Workers實戰詳解:以圖片壓縮為例
你是否注意过在执行繁重任务时网页会冻结?这是因为JavaScript默认在一个单线程上运行,导致用户体验不佳。用户无法进行任何操作,只能等待任务完成。这个问题可以通过使用Web Workers来解决,下面我们就来详细了解。我们将讨论Web Workers是什么,为什么它们很有用,以及如何通过一个实际的图像压缩应用示例来使用它们。这很令人兴奋,不是吗?那么,让我们开始吧。
Web Worker线程是什么?Web Workers 允许 JavaScript 在后台运行任务 不会干扰到主线程,这会让界面保持流畅和快速响应。你可以用 Web Workers API 来创建它们,该 API 接受两个参数:url
和 options
。下面是一个简单的创建 Web Worker 的例子。
创建一个新的 Worker 对象,使用 'worker.js' 文件,并指定其类型为 'module'。
全屏 退出全屏
为什么要用Web Workers?简单来说...正如之前提到的,Web Workers 可以在后台运行任务。下面有几个理由说明为什么要使用它们:
-
防止页面在进行大量计算时变得卡顿
-
能够高效处理海量数据
- 提升复杂 web 应用程序的性能
-
主线程 创建一个工人 并给它分配工作
-
员工正在后台处理这个任务
- 完成后,它将结果送回主执行线程
好了,现在我们知道了什么是Web Worker,为什么要使用它们,以及它们是如何工作的。但这还不够,对吧?所以让我们做个图像压缩的应用,看看如何在实际中使用Web Worker。
项目启动创建一个用 TypeScript 和 Tailwind CSS 的 Next.js 项目
在终端中运行以下命令以创建一个新的 Next.js 应用程序,该应用程序使用 TypeScript:
npx create-next-app@latest --typescript web-worker-with-example
cd web-worker-with-example
全屏/退出全屏
要在浏览器中压缩图片,我们将使用 @jsquash/web
这个npm库来对WebP图片进行编码和解码。此库是基于WebAssembly的,我们来安装这个库。
运行以下命令来安装webp模块:
npm install @jsquash/webp
全屏显示,退出全屏
好的,我们的项目设置已经完成了。接下来,我们将创建一个脚本以管理图片压缩。
创建工作脚本一个工作脚本是一个包含了处理消息事件代码的 JavaScript 或 TypeScript 文件。
在 src/worker
文件夹中创建一个名为 imageCompressionWorker.ts
的文件,并添加以下代码。
/// <参考 lib="webworker" /> <!-- 注意:这是注释部分 -->
const ctx = self as DedicatedWorkerGlobalScope;
import { decode, encode } from '@jsquash/webp';
ctx.onmessage = async (
event: MessageEvent<{
id: number;
imageFile: File;
options: { quality: number };
}>
) => {
// 确保 wasm 已加载完毕
await import('@jsquash/webp');
const { imageFile, options, id } = event.data;
const fileBuffer = await imageFile.arrayBuffer();
try {
const imageData = await decode(fileBuffer);
const compressedBuffer = await encode(imageData, options);
const compressedBlob = new Blob([compressedBuffer], {
type: imageFile.type,
});
ctx.postMessage({ id, blob: compressedBlob });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : '未知错误';
ctx.postMessage({ id, error: message });
}
};
全屏模式 退出全屏
这里,我们从 @jsquash/webp
库中导入 encode
和 decode
方法,这两个方法分别用于编码和解码。并使用工作线程的全局作用域 self
来监听主线程的消息。
当有新消息时,我们拿到图像文件和选项,然后先解码图像,再用质量选项重新编码以压缩图像。最后,我们用 postMessage
将压缩后的图像 blob 发送回主线程。如果有错误,我们处理它并通过 postMessage
发送错误消息回来。
工人脚本已经准备好了。接下来,我们将构建图像列表组件,调整样式,更新页面内容,并用它来处理压缩。
使用 Web 工作线程在我们开始之前,先用以下内容更新global.css
文件,并移除其中的默认样式。
@tailwind 基础样式;
@tailwind 组件样式;
@tailwind 工具类;
切换到全屏模式,退出全屏
在 src/components
文件夹里创建一个 ImageList.tsx
文件,并将以下代码添加到文件中。
/* eslint-disable @next/next/no-img-element */
import React from 'react';
export type ImgData = {
id: number;
file: File;
status: '压缩' | '已完成' | '错误';
originalUrl: string;
compressedUrl?: string;
error?: string;
compressedSize?: number;
};
interface ImageListProps {
images: ImgData[];
}
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 字节';
const k = 1024;
const dm = 2;
const sizes = ['字节', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const ImageList: React.FC<ImageListProps> = ({ images }) => {
return (
<div className="mt-4">
<h2 className="text-xl font-semibold mb-2">图片列表</h2>
<div className="space-y-4">
{images.map((img) => (
<div
key={img.id}
className="flex flex-col md:flex-row items-center border p-4 rounded"
>
<div className="flex-1 flex flex-col items-center">
<p className="font-bold mb-2">原始</p>
<img
src={img.originalUrl}
alt="原始"
className="w-32 h-32 object-cover rounded border mb-2"
/>
<p className="text-sm">大小:{formatBytes(img.file.size)}</p>
</div>
{img.status === '已完成' && img.compressedUrl ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold mb-2">压缩后</p>
<img
src={img.compressedUrl}
alt="压缩后"
className="w-32 h-32 object-cover rounded border mb-2"
/>
<p className="text-sm">
大小:{' '}
{img.compressedSize ? formatBytes(img.compressedSize) : '未提供'}
</p>
<a
href={img.compressedUrl}
download={`${img.file.name.replace(
/\.[^/.]+$/,
''
)}-压缩.webp`}
className="mt-2 inline-block px-3 py-1 bg-blue-500 text-white rounded"
>
下载压缩版
</a>
</div>
) : img.status === '压缩' ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold">正在压缩...</p>
</div>
) : img.status === '错误' ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold text-red-500">压缩错误:</p>
</div>
) : null}
</div>
))}
</div>
</div>
);
};
export default ImageList;
点击以进入全屏模式,点击以退出全屏模式
ImageList 组件接收一个名为 images
的属性,该属性是一个由 ImgData
对象组成的列表。接着展示原始图像和压缩后的图像,并显示它们的大小,同时提供压缩图像的下载链接。
接下来,用下面的代码更新 app/page.tsx
,我们一起看看各个部分吧。
'use client';
import { useState, useRef, useEffect } from 'react';
import ImageList, { ImgData } from '../components/ImageList';
export default function Home() {
const [images, setImages] = useState<ImgData[]>([]);
const [text, setText] = useState('');
const workerRef = useRef<Worker | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || !workerRef.current) return;
const filesArray = Array.from(files);
filesArray.forEach((file, index) => {
const id = Date.now() + index;
const originalUrl = URL.createObjectURL(file);
setImages((prev) => [
...prev,
{ id, file, status: '正在压缩', originalUrl },
]);
// 将文件及其id发送给worker。
workerRef.current!.postMessage({
id,
imageFile: file,
options: { quality: 75 },
});
});
};
// 组件挂载时初始化worker。
useEffect(() => {
const worker = new Worker(
new URL('../worker/imageCompressionWorker.ts', import.meta.url),
{ type: 'module' }
);
workerRef.current = worker;
// 监听worker的消息。
worker.onmessage = (
event: MessageEvent<{ id: number; blob?: Blob; error?: string }>
) => {
const { id, blob: compressedBlob, error } = event.data;
setImages((prev) =>
prev.map((img) => {
if (img.id === id) {
if (error) return { ...img, status: '出错', error };
const compressedSize = compressedBlob!.size;
const compressedUrl = URL.createObjectURL(compressedBlob!);
return { ...img, status: '完成压缩', compressedUrl, compressedSize };
}
return img;
})
);
};
return () => {
worker.terminate();
};
}, []);
return (
<div className="min-h-screen p-8">
<h1 className="text-2xl font-bold text-center mb-4">
使用Web Worker进行图像压缩
</h1>
<div className="rounded shadow p-4 mb-4 flex flex-col gap-2">
<p className="text-sm">
在压缩图像的同时,您可以使用下方的文本区域,观察输入的文本,UI不会因此变得卡顿。
</p>
<p className="text-sm">
您甚至可以在打开开发者工具的性能标签时看到INP(交互到下一次绘制)非常低。
</p>
<textarea
className="w-full h-32 border rounded p-2 text-black"
placeholder="在压缩图像的同时,您可以在这里输入..."
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
</div>
<div className="rounded shadow p-4">
<input
type="file"
multiple
accept="image/webp"
onChange={handleFileChange}
/>
<ImageList images={images} />
</div>
</div>
);
}
全屏模式,退出全屏
首先,我们导入了钩子、ImageList组件和ImgData类型。
import { useState, useRef, useEffect } from 'react'; // 导入React的useState, useRef, useEffect钩子
import ImageList, { ImgData } from '../components/ImageList'; // 导入ImageList组件及其ImgData类型
请进入全屏,请退出全屏
接着,我们创建一个 ref 来保存 worker 实例的引用,因为我们不想在每次渲染时都重复创建 worker 实例。我们也希望在 worker 实例发生变化时避免重新渲染组件。
const workerRef = useRef<Worker | null>(null);
点击进入或退出全屏模式
我们使用 useEffect 初始化 worker 实例,通过使用之前创建的 imageCompressionWorker.ts
工作脚本。
我们使用
import.meta.url
,这样可以调用 URL API。这使路径相对于当前脚本,而不是相对于 HTML 页面。这样,打包工具可以安全地进行优化操作,例如重命名,否则worker.js
的 URL 可能会指向一个未由打包工具管理的文件,导致无法做出假设。更多详情请参阅 这里。
一旦工人初始化完成,我们就监听它发出的消息。当我们接收到消息时,我们提取 id、blob 和 error,然后用新值更新 images 的状态。
最后,当组件被卸载时,我们会清理工人。
useEffect(() => {
const worker = new Worker(
new URL('../worker/imageCompressionWorker.ts', import.meta.url),
{ type: 'module' }
);
workerRef.current = worker;
// 监听 worker 发送的消息。
worker.onmessage = (
event: MessageEvent<{ id: number; blob?: Blob; error?: string }>
) => {
const { id, blob: compressedBlob, error } = event.data;
setImages((prev) =>
prev.map((img) => {
if (img.id === id) {
if (error) return { ...img, status: 'error', error };
const compressedSize = compressedBlob!.size;
const compressedUrl = URL.createObjectURL(compressedBlob!);
return { ...img, status: 'done', compressedUrl, compressedSize };
}
return img;
})
);
};
return () => {
// 返回一个函数,用于终止 worker。
worker.terminate();
};
}, []);
全屏,退出
为了管理图片文件的上传,我们使用handleFileChange
方法。该方法监听文件输入的onchange
事件,处理这些文件,并将其发送到工作线程进行压缩。
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || !workerRef.current) return;
const filesArray = Array.from(files);
filesArray.forEach((file, index) => {
const id = Date.now() + index;
const originalUrl = URL.createObjectURL(file);
setImages((prev) => [
...prev,
{ id, file, status: '正在压缩', originalUrl },
]);
// 将文件和ID发送给工作者。
workerRef.current!.postMessage({
id,
imageFile: file,
options: { 质量: 75 },
});
});
};
切换到全屏模式,退出全屏
最后,呈现文本框、图片上传和图片列表。
如图所示,这个流程图
-
用户选择图片时: 用户通过文件输入选择图片时,这会使得组件为每张图片生成对象URL,并标记为“正在压缩”。
-
工人的通讯: 该组件将每个带有选项的图像文件发送给Web Worker(Web工件)。
-
并行处理:。
- 文本区域互动: 同时,用户可以在文本区域中输入文字,表明UI并未被卡住。
- 图像压缩: 处理者在后台压缩图像。
- 完成: 压缩完成后,处理者将结果回传给组件,组件用压缩后的图像更新UI,同时文本区域仍可正常工作。
好的,一切都设置好了。接下来,我们运行程序,看看Web Worker是怎么运作的。
这是一张图片,点击可以查看大图。
打开命令行终端并运行下面的命令,之后在浏览器中输入网址 localhost:3000 进行访问。
运行开发模式: `npm run dev`
全屏查看,退出全屏
如图所示
试试这个演示:https://web-worker-with-example.vercel.app/
最后Web 工作线程是一个很好的工具,可以提升应用性能。通过使用 Web Workers,你可以确保应用更快、更流畅、更灵敏的响应。然而,不应滥用它们,只在必要时使用。此外,请检查浏览器兼容性,目前全球兼容性约为 98%。你可以在这里查看:here。
关于这个话题的内容就到这里了。感谢您的阅读,希望您喜欢!如果您觉得这篇文章对您有所帮助,请点赞、评论并分享给您的朋友们。
资源-
代码示例 (代码 dāi mǎ)
- Jsquash webp 相关资料
共同學習,寫下你的評論
作者其他優質文章