亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

Web Workers實戰詳解:以圖片壓縮為例

你是否注意过在执行繁重任务时网页会冻结?这是因为JavaScript默认在一个单线程上运行,导致用户体验不佳。用户无法进行任何操作,只能等待任务完成。这个问题可以通过使用Web Workers来解决,下面我们就来详细了解。我们将讨论Web Workers是什么,为什么它们很有用,以及如何通过一个实际的图像压缩应用示例来使用它们。这很令人兴奋,不是吗?那么,让我们开始吧。

Web Worker线程是什么?

Web Workers 允许 JavaScript 在后台运行任务 不会干扰到主线程,这会让界面保持流畅和快速响应。你可以用 Web Workers API 来创建它们,该 API 接受两个参数:urloptions。下面是一个简单的创建 Web Worker 的例子。

创建一个新的 Worker 对象,使用 'worker.js' 文件,并指定其类型为 'module'。

全屏 退出全屏

为什么要用Web Workers?简单来说...

正如之前提到的,Web Workers 可以在后台运行任务。下面有几个理由说明为什么要使用它们:

  • 防止页面在进行大量计算时变得卡顿

  • 能够高效处理海量数据

  • 提升复杂 web 应用程序的性能
它们是怎么工作的?
  1. 主线程 创建一个工人 并给它分配工作

  2. 员工正在后台处理这个任务

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

如何WebWorker工作

好了,现在我们知道了什么是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 库中导入 encodedecode 方法,这两个方法分别用于编码和解码。并使用工作线程的全局作用域 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是怎么运作的。

giphy 这是一张图片,点击可以查看大图。

运行这个示例代码

打开命令行终端并运行下面的命令,之后在浏览器中输入网址 localhost:3000 进行访问。

运行开发模式: `npm run dev`

全屏查看,退出全屏

示例1的截图 如图所示

截图2工作示例

试试这个演示:https://web-worker-with-example.vercel.app/

最后

Web 工作线程是一个很好的工具,可以提升应用性能。通过使用 Web Workers,你可以确保应用更快、更流畅、更灵敏的响应。然而,不应滥用它们,只在必要时使用。此外,请检查浏览器兼容性,目前全球兼容性约为 98%。你可以在这里查看:here

关于这个话题的内容就到这里了。感谢您的阅读,希望您喜欢!如果您觉得这篇文章对您有所帮助,请点赞、评论并分享给您的朋友们。

资源
點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消