起因是我在使用vite-plugin-monkey编写油猴脚本,用ts编写webworker脚本,然后在一些网站创建webworker准备做一些耗时任务时,webworker一直没生效。我一直以为new Worker()梭哈就好了,没想到里面的门道这么多,整理了一些问题。
1.webworker不能直接使用非同源
假设谷歌有一个w.js脚本,在你的网站里面,不能直接new Worker('https://www.google.com/w.js')去加载。注意,这里是限制非同源,和跨域CORS没关系。
2.worker导入方式
1.new Worker()
传统worker的导入方式是new Worker(''), new Worker做了什么?它会发起一个http请求,请求自身站点的这个worker文件。(要求这个文件真实存在)
2.new Worker(BlobURL)
传统worker导入方式又有另一种变体,就是获取到js的字符串,通过new Blob([jsCodeString], { type: "application/javascript" })得到blob,然后再通过URL.createObjectURL(blob)生成blob url,最后new Worker(blob url), 就不需要文件真实存在了。
js
const jsContent = `const a = 1;console.log(a)`
const blob = new Blob([jsCodeString], { type: "application/javascript" })
const url = URL.createObjectURL(blob)
new Worker(url)
3.new Worker(new URL('./w.js', import.meta.url).href, { type: 'module' })
new Worker('')是以浏览器地址栏页面 URL 为基准解析,可是在vue/react等应用出现后,这些SPA的History 多层路由(/user/detail/123)会拼接错误路径,导致找不到这个worker文件,所以Chrome在2020年推出用import.meta.url来查找worker文件,无论页面路由多深、页面在哪一级,永远以当前脚本文件目录查找 worker。但是一定要在script module里面执行
js
<script type="module">
new Worker(new URL('./w.js', import.meta.url).href, { type: 'module' })
</script>
4.import Worker from './worker?worker&inline'
这是vite的语法,用来支持将外部worker文件内联到代码中的场景。
原理就是第二种方式,先把外部js文件转成code strig,再用Blob内联,只不过这个过程不用你手动处理了,vite帮你处理了。
3.代码实战
按照以上顺序,我让AI写了4组代码,并且分js/ts来测试。

代码具体实现如下:
A-js
js
const w = new Worker('./fib-worker.js');
A-ts
ts
const w = new Worker('./fib-worker.ts');
B-js
js
import jsWorkerCode from './fib-worker.js?raw';
const blob = new Blob([jsWorkerCode], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const w = new Worker(url);
URL.revokeObjectURL(url);
B-ts
ts
import tsWorkerCode from './fib-worker.ts?raw';
const blob = new Blob([tsWorkerCode], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const w = new Worker(url);
URL.revokeObjectURL(url);
C-js
js
const url = new URL('./fib-worker.js', import.meta.url).href;
const w = new Worker(url, { type: 'module' });
C-ts
ts
const url = new URL('./fib-worker.ts', import.meta.url).href;
const w = new Worker(url, { type: 'module' });
D-js
js
import FibWorkerJS from './fib-worker.js?worker&inline';
const w = new FibWorkerJS();
D-ts
ts
import FibWorkerTS from './fib-worker.ts?worker&inline';
const w = new FibWorkerTS();
以下是用vite-plugin-monkey在掘金的页面开发环境下跑的:
然后来解释一下:
A-js是向掘金发起了一个http请求,去请求这个worker文件,而这个文件不存在,掘金返回了一个200的html <!DOCTYPE html><html>。然后Worker尝试去执行,发现第一行是<,于是报出具体的语法错误:

注意,不是说worker文件不存在就会返回html,是要看网站的处理,如果面对资源不存在,网站返回html,那就是这个。如果网站不处理,那么这个请求就会一直pending等待中,然后触发浏览器的报错。

A-ts和A-js一样
B-js就是标准的内联js worker,所以可以运行
B-ts是因为导入了ts源码,里面含有一些类型,而浏览器不支持解析这些类型,所以报错了。(有时候有人导入ts也成功,是因为里面不含类型,浏览器按js来解析了)
C-js和A-js差不多都是new Worker(), 为什么报错不一样呢?问题在于这个import.meta.url,此时worker不是向掘金发起http请求,而是向vite-plugin-monkey本地的vite服务器发起请求,实际上是new Worker('http://127.0.0.1:5173/src/fib-worker.js'),违反了第一条规则webworker不能直接使用非同源
C-ts同上
D-js不是说会内联worker吗?为什么还是和A-js一样,请求不存在的worker?我们可以打开面板,看到这条请求

里面的内容是这样的

也就是说,因为vite在开发环境下是不打包的,所以只是对其简单的包装了一下,由于这样写,实际上还是向掘金发起了http请求,然后由于掘金对这个请求没有处理,一直pending,所以触发了浏览器的错误事件。

4.打包结果


我们可以看到CD变了,C-js是因为tampermonkey打包出来的是普通script,通过iife运行,所以外层script没有module,导致import.meta.url在这里是undefined,从而构造URL失败。
而D则是正确的内联在code里面,所以在打包后能够正常运行。
5.怎么在开发环境下使用ts+webworker?
我们可以看到,在开发环境下,只有一种方式,那就是使用js内联。但是我就是想让它支持ts丰富的类型提示,总不能每次修改一点,就打包测试吧?这样调试起来太麻烦了,我之前试过,每次改一点->打包->删除旧的油猴脚本->替换新的脚本,非常繁琐。
其实也有看到社区自己写了一些plugin,在检测到?worker&inline时,就先打包,直接内联,这样就ok了,但是感觉还是不够优雅。
终于被我找到了另一种方式:
ts
const url = new URL('./fib-worker.ts', import.meta.url).href
const res = await fetch(url)
const jsCodeString = await res.text()
const blob = new Blob([jsCodeString], { type: 'application/javascript' })
const blobUrl = URL.createObjectURL(blob)
const w = new Worker(blobUrl)

这里涉及到vite的服务器原理了。我们在开发vue/react项目的时候,在开发环境下,你会发现浏览器发起了很多请求,像vue,ts,tsx等等。

可是浏览器怎么会认识vue、ts呢?那是因为浏览器向vite请求vue文件,先不直接返回vue文件,而是通过@vue/compiler-sfc先把vue文件转成js文件,再返回。

同样的,浏览器向vite请求ts文件时,vite会先做一遍类型擦除,类型擦除后不就是js文件吗?所以也不用编译,直接返回。

那么回到刚才的问题,const url = new URL('./fib-worker.ts', import.meta.url).href,这里得到的其实就是vite服务器的ts文件的地址: http://127.0.0.1:5173/src/fib-worker.ts

而刚才说了,向vite服务器请求ts,vite会自动做一次类型擦除,所以得到的是纯净的js文件

既然能得到纯净的js文件,那么我们就要得到它。通过fetch去获取,而vite服务器设置的是允许跨域,所以我们就得到了纯净的jsCodeString。既然得到了jsCodeString,上面开发环境演示的几个demo中,只有B-js是成功的,那么就只需要转Blob url,就能在开发环境下使用webworker了。
完整封装如下:
ts
import TSWorker from './worker?worker&inline'
async function createWorker(): Promise<Worker> {
if (import.meta.env.DEV) {
const url = new URL('./worker.ts', import.meta.url).href
const res = await fetch(url)
if (!res.ok) throw new Error('获取worker代码失败')
const jsCodeString = await res.text()
const blob = new Blob([jsCodeString], { type: 'application/javascript' })
return new Worker(URL.createObjectURL(blob))
}
return new TSWorker()
}
await createWorker()
开发环境下走fetch,打包时走vite的worker inline