大数跨境

Web Worker 如何工作以及实际示例

Web Worker 如何工作以及实际示例 索引目录
2025-02-23
2

有没有注意到网页在繁重任务期间冻结?发生这种情况是因为 JavaScript 默认在单个线程上运行,导致用户体验不佳。用户无法交互,必须等到任务完成。可以使用 Web Worker 解决此问题。在本文中,我们将通过构建图像压缩应用程序,讨论什么是 Web Worker、它们为什么有用以及如何使用它们以及实际示例。很令人兴奋,对吧?让我们开始吧。

什么是 Web Worker?

Web Workers 允许 JavaScript 在后台运行任务而不阻塞主线程,从而使您的 UI 保持流畅和响应迅速。您可以使用 Web Workers API 创建它们,它接受两个参数:urloptions。以下是创建 Web Workers 的简单示例。

const worker = new Worker('./worker.js', { type: 'module' });

为什么要使用 Web Worker?

如前所述,Web Worker 在后台运行任务。以下是使用它们的几个原因

  • 防止大量计算时的页面滞后

  • 高效处理大数据

  • 提高复杂 Web 应用程序的性能

它们如何工作?

  1. 主线程创建一个 worker并赋予它一个任务

  2. worker在后台处理任务

  3. 完成后,它将结果发送回主线程



好了,现在我们知道了什么是 Web Worker、为什么要使用它们以及它们如何工作。但这还不够,对吧?让我们构建图像压缩应用程序,看看如何在实践中使用 Web Worker。

项目设置

使用 TypeScript 和 Tailwind CSS 创建 Next.js 项目

npx create-next-app@latest --typescript web-worker-with-example

cd web-worker-with-example



为了在浏览器中压缩图像,我们将使用@jsquash/webnpm 库对 WebP 图像进行编码和解码。此库由 WebAssembly 提供支持,因此让我们安装它。

npm install @jsquash/webp

太好了,我们的项目设置已完成。在下一节中,我们将创建一个工作脚本来管理图像压缩。

创建工作者脚本

工作脚本是一个 JavaScript 或 TypeScript 文件,其中包含处理工作消息事件的代码。

在文件夹内创建一个imageCompressionWorker.ts文件src/worker并添加以下代码。

/// <reference 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 };
}>
) => {
// make sure the wasm is loaded
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 : 'Unknown error';
ctx.postMessage({ id, error: message });
}
};

export {};

在这里,我们从库中导入encode和方法,并使用 worker 全局范围来监听来自主线程的消息。decode@jsquash/webpself

当消息到达时,我们会获取图像文件和选项,然后通过先解码再使用质量选项编码来压缩图像。最后,我们使用 将postMessage压缩后的图像 blob 发送回主线程。如果出现错误,我们会处理它并使用 将错误消息发送回去postMessage

工作脚本已准备就绪。在下一节中,我们将构建 Imagelist 组件、更新样式、更新页面,并使用工作脚本来处理压缩。

使用 Web Worker

在开始之前,让我们global.css用以下内容更新文件并删除默认样式。

@tailwind base;
@tailwind components;
@tailwind utilities;

ImageList.tsx在文件夹中创建src/components并添加以下代码。

/* eslint-disable @next/next/no-img-element */
import React from 'react';

export type ImgData = {
id: number;
file: File;
status: 'compressing' | 'done' | 'error';
originalUrl: string;
compressedUrl?: string;
error?: string;
compressedSize?: number;
};

interface ImageListProps {
images: ImgData[];
}

const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = 2;
const sizes = ['Bytes', '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">Image List</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">Original</p>
<img
src={img.originalUrl}
alt="Original"
className="w-32 h-32 object-cover rounded border mb-2"
/>
<p className="text-sm">Size: {formatBytes(img.file.size)}</p>
</div>
{img.status === 'done' && img.compressedUrl ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold mb-2">Compressed</p>
<img
src={img.compressedUrl}
alt="Compressed"
className="w-32 h-32 object-cover rounded border mb-2"
/>
<p className="text-sm">
Size:{' '}
{img.compressedSize ? formatBytes(img.compressedSize) : 'N/A'}
</p>
<a
href={img.compressedUrl}
download={`${img.file.name.replace(
/\.[^/.]+$/,
''
)}-compressed.webp`}
className="mt-2 inline-block px-3 py-1 bg-blue-500 text-white rounded"
>
Download
</a>
</div>
) : img.status === 'compressing' ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold">Compressing...</p>
</div>
) : img.status === 'error' ? (
<div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
<p className="font-bold text-red-500">Error in compression</p>
</div>
) : null}
</div>
))}
</div>
</div>
);
};

export default ImageList;

ImageList 组件接受一个 prop,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: 'compressing', originalUrl },
]);

// Send the file with its id to the worker.
workerRef.current!.postMessage({
id,
imageFile: file,
options: { quality: 75 },
});
});
};

// Initialize the worker once when the component mounts.
useEffect(() => {
const worker = new Worker(
new URL('../worker/imageCompressionWorker.ts', import.meta.url),
{ type: 'module' }
);
workerRef.current = worker;

// Listen for messages from the 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.terminate();
};
}, []);

return (
<div className="min-h-screen p-8">
<h1 className="text-2xl font-bold text-center mb-4">
Image Compression with Web Workers
</h1>
<div className="rounded shadow p-4 mb-4 flex flex-col gap-2">
<p className="text-sm">
While images are compressing, you can interact with the textarea below
and observe the text being typed and UI is not frozen.
</p>
<p className="text-sm">
Even you can open the dev tools and then open the performance tab and
see the INP(Interaction to Next Paint) is very low.
</p>
<textarea
className="w-full h-32 border rounded p-2 text-black"
placeholder="Type here while images are compressing..."
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';
import ImageList, { ImgData } from '../components/ImageList';

然后,我们创建一个 ref 来保存 worker 实例,因为我们不想在每次重新渲染时重复创建 worker。我们还希望避免在 worker 实例发生变化时重新渲染组件。

const workerRef = useRef<Worker | null>(null);

在 useEffect 中,我们使用imageCompressionWorker.ts之前创建的 worker 脚本来初始化 worker 实例。

我们使用 URL API import.meta.url。这使得路径相对于当前脚本而不是 HTML 页面。这样,捆绑器就可以安全地进行优化,例如重命名,因为否则,URLworker.js可能会指向捆绑器未管理的文件,从而阻止它做出假设。在此处阅读更多信息。

一旦初始化了 Worker,我们就会监听来自它的消息。收到消息后,我们会提取 id、blob 和 error,然后使用新值更新图像状态。

最后,我们在组件卸载时清理工作进程。

useEffect(() => {
const worker = new Worker(
new URL('../worker/imageCompressionWorker.ts', import.meta.url),
{ type: 'module' }
);
workerRef.current = worker;

// Listen for messages from the 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.terminate();
};
}, []);

为了管理图片文件上传,我们使用handleFileChange方法。该方法监听onchange文件输入事件,处理文件,并发送给 worker 进行压缩。

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: 'compressing', originalUrl },
]);

// Send the file with its id to the worker.
workerRef.current!.postMessage({
id,
imageFile: file,
options: { quality: 75 },
});
});
};

最后,渲染元素文本区域、图像输入和图像列表。



  • 用户选择图像:用户使用文件输入选择图像,这使得组件创建对象 URL 并将每个图像标记为“压缩”。

  • 工作者通信:组件将每个图像文件(带有选项)发送到 Web 工作者。

  • 并行进程:

    • 文本区域交互:同时用户可以在文本区域进行输入,体现不阻挡UI。

    • 图像压缩:工作人员在后台压缩图像。


  • 完成:压缩完成后,工作者将结果发送回组件,组件使用压缩图像更新 UI,同时文本区域保持平稳运行。

太好了,一切都设置好了。在下一节中,我们将运行该应用程序并查看 Web Worker 的工作原理。



运行示例

打开终端并运行以下命令,然后转到http://localhost:3000/

npm run dev





结论

Web Workers 是提高应用程序性能的绝佳工具。通过使用 Web Workers,您可以确保应用程序更快、更流畅、响应更快。但是,不应过度使用它们,只应在必要时使用。

另外,请检查浏览器支持情况,目前全球支持率约为 98%。


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读1.1k
粉丝0
内容444