Skip to page content

useWebWorker 组合式 API
v2.34+

useWebWorker() composable 把一个组件连接到一个 Web Worker:它会根据你编写的脚本创建 worker,把最近收到的消息暴露为一个响应式值,让你可以发送消息(并附带一个 transfer 列表),并在组件销毁时终止该 worker。

当你需要一个长期存活、拥有自身协议的 worker 时(比如分块喂入数据的解析器、搜索索引、持续流式上报进度的模拟计算),就用它。而如果只是想把某个函数放到主线程之外去运行并等待它的结果,useWebWorkerFn 会更合适。

TIP

在 SSR 或 SSG 模式的服务端,不会创建任何 worker:workerStatus 保持为 idle,postWorkerMessage() 什么都不做,也永远不会有消息到达。worker 是在客户端、组件挂载之后才创建的,因此在 hydration 之前状态也同样是 idle。

在组件之外使用

该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。此时没有挂载时机可等待,因此 worker 会立即创建(除非设置了 lazy),并且不会自行终止:完成后请调用 terminateWorker()。

语法

import { useWebWorker } from 'quasar'

setup () {
  const {
    workerStatus,
    workerData,
    workerError,
    postWorkerMessage,
    terminateWorker
  } = useWebWorker(
    source,
    {
      // all optional:

      lazy: true, // do not create the worker on mount;
                  // the first postWorkerMessage() does it

      // the native Worker options, used when "source" is a URL:
      type: 'module',            // 'module' (default) or 'classic'
      name: 'primes',            // labels the worker in the devtools
      credentials: 'same-origin', // for a module worker script

      onMessage (data, evt) { // called with each message from the worker
        // ...
      },
      onError (evt) { // called with the "error" / "messageerror" events
        // ...
      },
      onCreate (worker) { // called with each Worker created
        // ...
      },
      onTerminate (worker, reason) { // called after a worker got killed
        // ...
      }
    }
  )

  // ...
}
function useWebWorker<Data = any>(
  source:
    | string
    | URL
    | Worker
    | ((options?: WorkerOptions) => Worker)
    | (new (options?: WorkerOptions) => Worker),
  options?: WorkerOptions & {
    lazy?: boolean
    onMessage?: (data: Data, evt: MessageEvent<Data>) => void
    onError?: (evt: ErrorEvent | MessageEvent) => void
    onCreate?: (worker: Worker) => void
    onTerminate?: (worker: Worker, reason: 'terminate' | 'unmount') => void
  }
): {
  workerStatus: Ref<'idle' | 'running' | 'terminated'>
  workerData: ShallowRef<Data | null>
  workerError: ShallowRef<ErrorEvent | MessageEvent | null>
  postWorkerMessage: (message: any, transfer?: Transferable[]) => void
  terminateWorker: () => void
}

source 可以是:

  • worker 脚本的 URL(一个字符串或一个 URL 对象);worker 会使用原生的 type、name 和 credentials 选项创建,其中 type 默认为 'module'(如果脚本依赖 importScripts(),请把它设为 'classic')
  • 一个你已经创建好的 Worker 实例
  • 一个返回 Worker 的函数;Vite ?worker 导入的默认导出就是这样一个函数,形如 new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) 的箭头函数也是——后者能让脚本保持可被打包器静态分析。该函数会以原生选项作为参数,因此一个 ?worker 构造器会接收到你设置的 name

worker 会在组件挂载时创建(或者当 composable 在组件之外使用时,立即创建),因此一个会自行发送消息的 worker(比如启动 WASM 模块并在就绪时上报的脚本、一个定时器)从一开始就处于运行状态。如果你想让 worker 等到你第一次调用 postWorkerMessage() 时才启动,请设置 lazy: true:这样一个从不与它通信的组件就永远不会启动线程。无论哪种方式,也无论 source 是何种形式,worker 都会在组件销毁时被终止。workerStatus 遵循这样的生命周期:没有 worker 时为 idle(在服务端和客户端都是如此,因此依赖它的标记在 hydration 时不会出现不匹配),worker 存活时为 running,组件销毁后为 terminated。

workerData 保存从 worker 收到的最后一条消息的 data,workerError 保存最后一次的 error(worker 抛出异常或加载失败)或 messageerror(某条消息无法被反序列化)事件。onMessage 和 onError 钩子会在相同的情形下被调用,因此你无需自己去 watch 这些 ref。worker 的错误仍会像往常一样上报到控制台;如果你已经处理了它,请在 onError 中调用 evt.preventDefault()。

postWorkerMessage(message, transfer) 向 worker 发送一条消息(如果当前没有 worker 则先创建一个),并把可选的 transfer 列表中的对象(ArrayBuffer、MessagePort、ImageBitmap 等)移交(transfer)而非拷贝过去。组件销毁后它什么都不做。

terminateWorker() 会杀掉 worker,并把 workerStatus 重新置回 idle:下一次 postWorkerMessage() 会根据 source 创建一个新的 worker(挂载时的那次创建不会再发生)。你可以用它在一项任务完成后释放线程,或中止一个运行过久的任务,之后在需要时再次与 worker 通信。例外情况是作为 source 传入的 Worker 实例:它无法被重新创建,因此对它而言 terminateWorker() 是终局操作,workerStatus 会变为 terminated。

onCreate(worker) 会在该 composable 每次开始使用一个 Worker 时被调用,因此它正是发送初始化消息(配置、一个 MessagePort)的地方——每个新创建的 worker 都需要这些初始化。onTerminate(worker, reason) 会在一个 worker 被杀掉之后立即被调用,reason 为 'terminate' 表示是 terminateWorker() 调用所致,为 'unmount' 表示是组件被销毁所致;你可以在这里 reject 掉待处理的请求或重置进度状态。

编写 worker

使用 Quasar CLI(Vite)时,worker 脚本是你自己的一个文件,你用 new URL() 指向它,从而让它被打包:

// src/workers/primes.js
onmessage = ({ data }) => {
  const primes = []
  for (let n = 2; primes.length < data.count; n++) {
    if (primes.every(p => n % p !== 0)) primes.push(n)
  }
  postMessage(primes)
}
import { useWebWorker } from 'quasar'

setup () {
  const { workerData, postWorkerMessage } = useWebWorker(
    () => new Worker(new URL('../workers/primes.js', import.meta.url), { type: 'module' })
  )

  function compute () {
    postWorkerMessage({ count: 1000 })
  }

  // ...
}

?worker 的导入形式也同样可用:

import PrimesWorker from '../workers/primes.js?worker'

const { workerData, postWorkerMessage } = useWebWorker(PrimesWorker)

示例

下面的示例从一段内联脚本构建它的 worker(通过一个 Blob URL,useObjectUrl 会在组件销毁时把它释放掉),这样所有内容都能塞进一个文件里。在你自己的应用中,你更应该像上面演示的那样,把 worker 放在它自己的文件里。

<template>
  <div class="q-pa-md">
    <div class="row items-center q-gutter-sm q-mb-md">
      <q-input
        v-model.number="count"
        type="number"
        dense
        outlined
        label="Primes to find"
        style="width: 160px"
      />
      <q-btn
        color="primary"
        label="postWorkerMessage()"
        no-caps
        @click="postWorkerMessage({ count })"
      />
      <q-btn
        v-if="workerStatus === 'running'"
        color="negative"
        label="terminateWorker()"
        no-caps
        @click="terminateWorker"
      />
    </div>

    <div v-if="workerStatus === 'idle'"
      >No worker running (the next postWorkerMessage creates one)</div
    >
    <div v-else-if="workerData === null">No message received yet</div>
    <div v-else>
      Largest prime among the first {{ workerData.count }}:
      {{ workerData.largest }} (computed in {{ workerData.ms }}ms)
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useObjectUrl, useWebWorker } from 'quasar'

// In your app the worker lives in its own file:
//   () => new Worker(new URL('./primes.js', import.meta.url), { type: 'module' })
// The example inlines the script so that it fits in one file.
const script = `
onmessage = ({ data }) => {
  const start = performance.now()
  const primes = []
  for (let n = 2; primes.length < data.count; n++) {
    if (primes.every(p => n % p !== 0)) primes.push(n)
  }
  postMessage({
    count: data.count,
    largest: primes[primes.length - 1],
    ms: Math.round(performance.now() - start)
  })
}`

const count = ref(2000)

// revoked when the component gets destroyed
const { objectUrl } = useObjectUrl(
  new Blob([script], { type: 'text/javascript' })
)

const { workerStatus, workerData, postWorkerMessage, terminateWorker } =
  useWebWorker(() => new Worker(objectUrl.value), { lazy: true })
</script>
内容安全策略(Content Security Policy)

worker 脚本必须被你的 CSP 中的 worker-src 指令所允许(该指令会回退到 script-src)。当从 Blob URL 创建 worker 时(如上面的示例以及 useWebWorkerFn 所做的那样),请把 blob: 加入其中。