WebWorker 使用示例(Vue3 + TypeScript + Setup 语法糖)

本文展示 WebWorker 在前端开发中的实际应用场景。实现批量视频第一帧提取并生成缩略图的功能、大数组排序和实时 CSV 数据解析。每个示例包含完整的代码,结合 Tailwind CSS 优化 UI,确保代码清晰、类型安全且易于理解。准备好让 WebWorker 帮你把重活干了吧!


1. 项目环境准备

1.1 技术栈

  • Vue3:使用 Composition API 和 Setup 语法糖。
  • TypeScript:确保类型安全。
  • WebWorker:处理 CPU 密集型任务。
  • Vite:作为构建工具。
  • Tailwind CSS:美化界面。

1.2 项目初始化

npm create vite@latest webworker-examples -- --template vue-ts
cd webworker-examples
npm install
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
npm run dev

1.3 配置 Tailwind CSS

src/style.css 中添加:

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

更新 vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  css: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
})

1.4 依赖

无额外运行时依赖,使用浏览器原生 WebWorker 和 <canvas> API。


2. 示例一:批量视频第一帧提取并生成缩略图

2.1 场景描述

在一个视频管理应用中,用户上传多个视频文件,页面需要快速生成每个视频的第一帧作为缩略图。直接在主线程处理会导致页面卡顿。使用 WebWorker 在后台提取帧并生成缩略图,主线程负责渲染结果,保持 UI 流畅。

2.2 实现思路

  1. 用户选择多个视频文件,Vue 组件收集文件列表。
  2. 主线程创建 WebWorker,传递视频文件的 ArrayBuffer(通过 Transferable 优化传输)。
  3. Worker 使用 <video><canvas> API 提取第一帧,生成缩略图的 Base64 数据。
  4. Worker 返回缩略图数据,主线程更新 UI。
  5. 使用 Tailwind CSS 美化缩略图展示。

2.3 完整代码

2.3.1 主组件 (src/App.vue)
<template>
  <div class="p-6 max-w-4xl mx-auto">
    <h1 class="text-3xl font-bold mb-6">WebWorker 示例:批量视频缩略图生成</h1>
    <input
      type="file"
      multiple
      accept="video/*"
      ref="fileInput"
      @change="handleFileChange"
      class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
    />
    <button
      @click="generateThumbnails"
      :disabled="isProcessing || !videos.length"
      class="mt-4 bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
    >
      {{ isProcessing ? '处理中...' : '生成缩略图' }}
    </button>
    <div v-if="error" class="mt-4 text-red-600">{{ error }}</div>
    <div class="mt-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
      <div v-for="thumb in thumbnails" :key="thumb.fileName" class="border rounded-md p-2">
        <img :src="thumb.dataUrl" alt="Thumbnail" class="w-full h-32 object-cover rounded-md" />
        <p class="mt-2 text-sm text-gray-600">{{ thumb.fileName }}</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

interface Thumbnail {
  fileName: string;
  dataUrl: string;
}

const fileInput = ref<HTMLInputElement | null>(null);
const videos = ref<File[]>([]);
const thumbnails = ref<Thumbnail[]>([]);
const isProcessing = ref(false);
const error = ref('');
let worker: Worker | null = null;

onMounted(() => {
  if (typeof Worker === 'undefined') {
    error.value = '浏览器不支持 WebWorker,请使用现代浏览器!';
    return;
  }
  worker = new Worker(new URL('./thumbnailWorker.ts', import.meta.url), { type: 'module' });

  worker.onmessage = (event: MessageEvent<Thumbnail>) => {
    thumbnails.value.push(event.data);
    if (thumbnails.value.length === videos.value.length) {
      isProcessing.value = false;
    }
  };

  worker.onerror = (err: ErrorEvent) => {
    error.value = `Worker 错误: ${err.message}`;
    isProcessing.value = false;
  };
});

const handleFileChange = (event: Event) => {
  const input = event.target as HTMLInputElement;
  if (input.files) {
    videos.value = Array.from(input.files);
    thumbnails.value = [];
    error.value = '';
  }
};

const generateThumbnails = async () => {
  if (!worker || !videos.value.length) return;

  isProcessing.value = true;
  error.value = '';

  for (const video of videos.value) {
    try {
      const buffer = await video.arrayBuffer();
      worker.postMessage({ fileName: video.name, buffer }, [buffer]);
    } catch (err) {
      error.value = `处理文件 ${video.name} 失败: ${err instanceof Error ? err.message : '未知错误'}`;
      isProcessing.value = false;
      break;
    }
  }
};

onUnmounted(() => {
  if (worker) {
    worker.terminate();
    worker = null;
  }
});
</script>
2.3.2 Worker 脚本 (src/thumbnailWorker.ts)
interface VideoMessage {
  fileName: string;
  buffer: ArrayBuffer;
}

interface Thumbnail {
  fileName: string;
  dataUrl: string;
}

self.onmessage = async (event: MessageEvent<VideoMessage>) => {
  const { fileName, buffer } = event.data;
  try {
    const blob = new Blob([buffer], { type: 'video/mp4' });
    const url = URL.createObjectURL(blob);
    const video = document.createElement('video');
    video.src = url;
    video.muted = true;

    await new Promise<void>((resolve, reject) => {
      video.onloadedmetadata = () => resolve();
      video.onerror = () => reject(new Error('无法加载视频'));
    });

    video.currentTime = 0;
    await new Promise<void>((resolve) => {
      video.onseeked = () => resolve();
    });

    const canvas = new OffscreenCanvas(320, 180);
    const ctx = canvas.getContext('2d');
    if (!ctx) throw new Error('无法获取 Canvas 上下文');

    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
    const dataUrl = await blobToDataUrl(blob);

    URL.revokeObjectURL(url);
    self.postMessage({ fileName, dataUrl });
  } catch (err) {
    console.error(`处理 ${fileName} 失败:`, err);
  }
};

function blobToDataUrl(blob: Blob): Promise<string> {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as string);
    reader.readAsDataURL(blob);
  });
}

2.4 代码说明

  • 主线程:Vue 组件收集视频文件,转换为 ArrayBuffer 传递给 Worker,使用 Transferable 优化大数据传输。
  • Worker:创建 <video> 加载视频,跳转到第一帧,使用 OffscreenCanvas 绘制帧并生成 JPEG 缩略图,返回 Base64 数据。
  • Vue 响应式thumbnails 存储缩略图数据,实时更新 UI。
  • TypeScript:定义 ThumbnailVideoMessage 接口,确保类型安全。
  • Tailwind CSS:网格布局展示缩略图,响应式设计。
  • 注意:需通过 HTTPS 或本地服务器运行,file:// 协议不支持 Worker。

2.5 应用场景

  • 视频管理平台生成预览图。
  • 在线视频编辑器显示时间轴缩略图。
  • 批量上传视频的预览功能。

3. 示例二:大数组排序

3.1 场景描述

在一个数据分析应用中,用户需要对一个包含数十万条数据的数组进行排序。主线程直接排序会导致页面卡顿。使用 WebWorker 在后台排序,主线程保持流畅。

3.2 实现思路

  1. 用户输入数组大小,Vue 组件生成随机数组。
  2. 主线程创建 WebWorker,传递数组。
  3. Worker 执行排序算法(如快速排序),返回结果。
  4. 主线程更新排序结果。

3.3 完整代码

3.3.1 主组件 (src/components/ArraySort.vue)
<template>
  <div class="p-6 max-w-md mx-auto">
    <h1 class="text-3xl font-bold mb-6">WebWorker 示例:大数组排序</h1>
    <input
      v-model.number="arraySize"
      type="number"
      placeholder="输入数组大小"
      class="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
    />
    <button
      @click="sortArray"
      :disabled="isSorting || !arraySize"
      class="mt-4 bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
    >
      {{ isSorting ? '排序中...' : '开始排序' }}
    </button>
    <div v-if="error" class="mt-4 text-red-600">{{ error }}</div>
    <p v-if="sortedArray" class="mt-4 text-lg">排序结果(前 10 项):{{ sortedArray.slice(0, 10).join(', ') }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const arraySize = ref<number | null>(null);
const sortedArray = ref<number[] | null>(null);
const isSorting = ref(false);
const error = ref('');
let worker: Worker | null = null;

onMounted(() => {
  if (typeof Worker === 'undefined') {
    error.value = '浏览器不支持 WebWorker,请使用现代浏览器!';
    return;
  }
  worker = new Worker(new URL('./sortWorker.ts', import.meta.url), { type: 'module' });

  worker.onmessage = (event: MessageEvent<number[]>) => {
    sortedArray.value = event.data;
    isSorting.value = false;
  };

  worker.onerror = (err: ErrorEvent) => {
    error.value = `Worker 错误: ${err.message}`;
    isSorting.value = false;
  };
});

const sortArray = () => {
  if (!worker || !arraySize.value || arraySize.value <= 0) {
    error.value = '请输入有效的数组大小!';
    return;
  }
  isSorting.value = true;
  error.value = '';
  const array = Array.from({ length: arraySize.value }, () => Math.random() * 1000);
  worker.postMessage(array);
};

onUnmounted(() => {
  if (worker) {
    worker.terminate();
    worker = null;
  }
});
</script>
3.3.2 Worker 脚本 (src/sortWorker.ts)
self.onmessage = (event: MessageEvent<number[]>) => {
  const array = event.data;
  const sorted = quickSort(array);
  self.postMessage(sorted);
};

function quickSort(arr: number[]): number[] {
  if (arr.length <= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

3.4 代码说明

  • 主线程:生成随机数组,传递给 Worker,接收排序结果。
  • Worker:使用快速排序算法处理数组,返回排序结果。
  • Vue 响应式sortedArray 存储结果,显示前 10 项避免 UI 卡顿。
  • TypeScript:确保数组类型为 number[]
  • Tailwind CSS:美化输入框和按钮。

3.5 应用场景

  • 数据分析工具排序大型数据集。
  • 在线表格应用对列数据排序。
  • 实时报表生成排序结果。

4. 示例三:实时 CSV 数据解析

4.1 场景描述

在一个数据导入应用中,用户上传 CSV 文件,页面需要快速解析并显示数据。主线程直接解析大文件会导致卡顿。使用 WebWorker 在后台解析 CSV,主线程渲染表格。

4.2 实现思路

  1. 用户上传 CSV 文件,Vue 组件读取文件内容。
  2. 主线程创建 WebWorker,传递 CSV 文本。
  3. Worker 解析 CSV 为二维数组,返回结果。
  4. 主线程渲染解析结果到表格。

4.3 完整代码

4.3.1 主组件 (src/components/CsvParser.vue)
<template>
  <div class="p-6 max-w-4xl mx-auto">
    <h1 class="text-3xl font-bold mb-6">WebWorker 示例:CSV 数据解析</h1>
    <input
      type="file"
      accept=".csv"
      ref="fileInput"
      @change="handleFileChange"
      class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
    />
    <button
      @click="parseCsv"
      :disabled="isParsing || !csvFile"
      class="mt-4 bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
    >
      {{ isParsing ? '解析中...' : '开始解析' }}
    </button>
    <div v-if="error" class="mt-4 text-red-600">{{ error }}</div>
    <div v-if="parsedData.length" class="mt-6 overflow-x-auto">
      <table class="min-w-full border-collapse border border-gray-300">
        <thead>
          <tr>
            <th v-for="header in parsedData[0]" :key="header" class="border border-gray-300 p-2 bg-gray-100">
              {{ header }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, index) in parsedData.slice(1, 10)" :key="index">
            <td v-for="cell in row" :key="cell" class="border border-gray-300 p-2">
              {{ cell }}
            </td>
          </tr>
        </tbody>
      </table>
      <p v-if="parsedData.length > 10" class="mt-2 text-sm text-gray-600">仅显示前 10 行,共 {{ parsedData.length - 1 }} 行数据</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const fileInput = ref<HTMLInputElement | null>(null);
const csvFile = ref<File | null>(null);
const parsedData = ref<string[][]>([]);
const isParsing = ref(false);
const error = ref('');
let worker: Worker | null = null;

onMounted(() => {
  if (typeof Worker === 'undefined') {
    error.value = '浏览器不支持 WebWorker,请使用现代浏览器!';
    return;
  }
  worker = new Worker(new URL('./csvWorker.ts', import.meta.url), { type: 'module' });

  worker.onmessage = (event: MessageEvent<string[][]>) => {
    parsedData.value = event.data;
    isParsing.value = false;
  };

  worker.onerror = (err: ErrorEvent) => {
    error.value = `Worker 错误: ${err.message}`;
    isParsing.value = false;
  };
});

const handleFileChange = (event: Event) => {
  const input = event.target as HTMLInputElement;
  if (input.files?.length) {
    csvFile.value = input.files[0];
    parsedData.value = [];
    error.value = '';
  }
};

const parseCsv = async () => {
  if (!worker || !csvFile.value) return;

  isParsing.value = true;
  error.value = '';

  try {
    const text = await csvFile.value.text();
    worker.postMessage(text);
  } catch (err) {
    error.value = `读取文件失败: ${err instanceof Error ? err.message : '未知错误'}`;
    isParsing.value = false;
  }
};

onUnmounted(() => {
  if (worker) {
    worker.terminate();
    worker = null;
  }
});
</script>
4.3.2 Worker 脚本 (src/csvWorker.ts)
self.onmessage = (event: MessageEvent<string>) => {
  const csvText = event.data;
  const parsed = parseCsv(csvText);
  self.postMessage(parsed);
};

function parseCsv(text: string): string[][] {
  const rows = text.split('\n').map(row => row.trim()).filter(row => row);
  return rows.map(row => row.split(',').map(cell => cell.trim()));
}

4.4 代码说明

  • 主线程:读取 CSV 文件内容,传递给 Worker,渲染解析结果到表格。
  • Worker:简单解析 CSV 文本为二维数组(假设逗号分隔,生产环境可使用更健壮的解析库)。
  • Vue 响应式parsedData 存储解析结果,显示前 10 行避免卡顿。
  • TypeScript:确保数据类型为 string[][]
  • Tailwind CSS:美化表格,添加滚动支持。

4.5 应用场景

  • 数据导入工具解析 CSV 文件。
  • 在线报表工具处理上传的数据。
  • 批量数据分析应用预览 CSV 内容。

5. 注意事项与优化

5.1 错误处理

  • 所有 WebWorker 和文件操作均包含错误处理,显示用户友好的提示。
  • 检查浏览器是否支持 WebWorker:
if (typeof Worker === 'undefined') {
  error.value = '浏览器不支持 WebWorker,请使用现代浏览器!';
}

5.2 性能优化

  • 使用 Transferable 对象(如 ArrayBuffer)优化大数据传输。
  • 及时终止 Worker(onUnmounted 中调用 terminate)。
  • 分块处理超大文件,定期发送进度更新。

5.3 兼容性

  • WebWorker 在现代浏览器(Chrome 4+、Firefox 3.5+、Safari 4+、Edge 12+)支持良好。
  • 示例需通过 HTTP/HTTPS 运行,file:// 协议不支持 Worker。

5.4 改进建议

  • 使用 Dexie.js 或其他库在 Worker 中结合 IndexedDB 存储中间结果。
  • 封装 Worker 逻辑为自定义 Hook(如 useWorker)。
  • 添加进度条显示 Worker 处理进度。
  • 使用成熟库(如 Papa Parse)增强 CSV 解析健壮性。

6. 总结

本文展示了 WebWorker 在批量视频缩略图生成、大数组排序和 CSV 数据解析中的应用。WebWorker 让主线程专注于 UI 渲染,将计算密集型任务交给后台线程,确保页面流畅。结合 Tailwind CSS 和 TypeScript,代码既美观又安全,适用于视频管理、数据分析等场景。开发者可根据需求调整 Worker 任务,释放 WebWorker 的性能潜力!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐