useWebWorker() composable 把一个组件连接到一个 Web Worker:它会根据你编写的脚本创建 worker,把最近收到的消息暴露为一个响应式值,让你可以发送消息(并附带一个 transfer 列表),并在组件销毁时终止该 worker。
当你需要一个长期存活、拥有自身协议的 worker 时(比如分块喂入数据的解析器、搜索索引、持续流式上报进度的模拟计算),就用它。而如果只是想把某个函数放到主线程之外去运行并等待它的结果,useWebWorkerFn 会更合适。
在 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>worker 脚本必须被你的 CSP 中的 worker-src 指令所允许(该指令会回退到 script-src)。当从 Blob URL 创建 worker 时(如上面的示例以及 useWebWorkerFn 所做的那样),请把 blob: 加入其中。