Web Worker 的工作原理(实际示例)
您是否曾注意到网页在执行繁重任务时卡顿?这是因为 JavaScript 默认在单线程上运行,导致用户体验不佳。用户无法进行交互,只能等待任务完成。这个问题可以通过使用 Web Worker 来解决。在本文中,我们将通过一个实际示例(构建一个图像压缩应用程序)来探讨 Web Worker 是什么、它们为何如此有用以及如何使用它们。是不是感觉很刺激?那就开始吧。
什么是 Web Worker?
Web Workers 允许 JavaScript 在后台运行任务,而不会阻塞主线程,从而保持 UI 的流畅性和响应速度。您可以使用 Web Workers API 创建它们,它接受两个参数:url和options。以下是创建 Web Workers 的一个简单示例。
const worker = new Worker('./worker.js', { type: 'module' });
 为什么要使用 Web Worker?
如前所述,Web Worker 在后台运行任务。以下是使用它们的几个理由
- 
  
防止在繁重计算期间页面滞后
 - 
  
高效处理大数据
 - 
  
提高复杂 Web 应用程序的性能
 
它们是如何工作的?
- 
  
主线程创建一个worker并赋予它一个任务
 - 
  
工作人员在后台处理任务
 - 
  
完成后,将结果发送回主线程
 
好了,现在我们知道了什么是 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和方法,并使用工作者全局范围来监听来自主线程的消息。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
 在此处尝试现场演示:https://web-worker-with-example.vercel.app/
结论
Web Workers 是提升应用程序性能的绝佳工具。使用 Web Workers,您可以确保应用程序运行更快、更流畅、响应更灵敏。但是,不应过度使用,仅在必要时使用。此外,请检查浏览器支持情况,目前全球浏览器支持率约为 98%。您可以点击此处查看。
本期内容就到这里。感谢您的阅读!如果您觉得本文有帮助,请点赞、评论并与他人分享。
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          




