如何将自己的库与需要跨越 WebWorkers 和 AudioWorklets 的 WebPack 集成
目标 我是 JavaScript 库的作者,该库可通过 AMD 或 ESM 在各种运行时环境(浏览器、Node.js、开发服务器)中使用。我的库需要使用其包含的文件生成 WebWorkers 和 AudioWorklet。该库会检测其运行的上下文,并为执行上下文设置所需的内容。
只要用户(用户 = 我的库的集成者)不将 WebPack 等捆绑器带入游戏,这种方法就很好用。要生成 WebWorker 和 AudioWorklet,我需要包含我的库的文件的 URL,并且我需要确保调用我的库的全局初始化例程。
我更愿意在我的库中完成尽可能多的繁重工作,而不是要求用户仅为使用我的库而进行非常专业的自定义设置。把这项工作交给他们通常会立即产生适得其反的效果,人们会提出问题,寻求帮助将我的库集成到他们的项目中。
问题 1: 我建议我的用户确保将我的库放入自己的块中。用户可以根据自己的设置设置块,只要其他库不会给工作人员带来任何麻烦或副作用即可。尤其是像 React、Angular 和 Vue.js 这样的现代 Web 框架是这里典型的问题,但也有人们试图将我的库与 jQuery 和 Bootstrap 捆绑在一起。所有这些库在包含在 Workers/Worklets 中时都会导致运行时错误。
分块通常使用一些 WebPack 配置完成,例如:
config.optimization.splitChunks.cacheGroups.alphatab = {
chunks: 'all',
name: 'chunk-mylib',
priority: config.optimization.splitChunks.cacheGroups.defaultVendors.priority + 10,
test: /.*node_modules.*mylib.*/
};
mylib 现在有一个大问题:生成的
chunk-mylib.js
的绝对 URL 是什么,因为现在这是我的库的准入口点,并且已经进行了捆绑和代码拆分:
-
document.currentScript
通常指向某个入口点,例如app.js
,而不是块。 -
__webpack_public_path__
指向用户在 webpack 配置中设置的任何内容。 -
__webpack_get_script_filename__
可以使用 如果 知道块名称,但我还没有找到获取我的库所包含的块名称的方法。 -
import.meta.url
指向我的库的原始.mjs
的某个绝对file://
url。 -
new URL(import.meta.url, import.meta.url)
导致 WebPack 生成带有一些哈希值的附加.mjs
文件。此附加文件不是所需的,并且生成的.mjs
包含一些附加代码,破坏了它在浏览器中的使用。
我已经在考虑创建一个自定义 WebPack 插件,它可以解析我的库所包含的块,以便我可以在运行时使用它。我更愿意使用尽可能多的内置功能。</p>
问题 2:
假设问题 1 已解决,我现在可以使用正确的文件生成一个新的 WebWorker 和 AudioWorklet。但由于我的库被包装到 WebPack 模块中,因此我的初始化代码将不会被执行。我的库仅存在于“块”中,而不是
entry
,我不知道这种拆分会允许 mylib 在浏览器加载块后运行一些代码。
在这里我相当无知。也许分块并不是实现此目的的正确拆分方式。也许需要一些其他设置,我尚未意识到这是可能的?
也许使用自定义 WebPack 插件也可以最好地完成此操作。
问题的可视化表示 :使用建议的分块规则,我们得到如块中所示的输出。问题 1 是红色部分(如何获取此 URL),问题 2 是橙色部分(如何确保在后台工作程序/工作单元启动时调用我的启动逻辑)
实际项目 我想分享我的实际项目,以便更好地理解我的用例。我说的是我的项目 alphaTab ,一个音乐符号渲染和播放库。在浏览器 UI 线程 (app.js) 上,人们将组件集成到 UI 中,并获得一个与组件交互的 API 对象。一个 WebWorker 负责音乐表的布局和渲染,第二个 WebWorker 合成音频样本以供播放,AudioWorklet 将缓冲的样本发送到音频上下文以供播放。
我认为应该将 worker 代码作为资产而不是源代码来处理。也许您可以添加一个简单的 CLI 来在项目根目录下生成一个“.alphaTab”文件夹,并添加指示,让用户将其复制到“dist”或“public 文件夹”。 即使想出了一个 Webpack 特定的解决方案,您也必须绕过其他捆绑器/设置(Vite、rollup、CRA 等)。
编辑:您还需要在初始化中添加一个可选参数来传递脚本路径。不是完全自动化的,但比设置复杂的捆绑器配置更简单
禁用
import.meta
关于
import.meta.url
,
此链接
可能会有所帮助。您似乎可以通过将
module.parser.javascript.importMeta
设置为
false
来禁用 webpack 配置。
重新设计整体架构
对于其余部分,这听起来有点混乱。您可能不应该尝试将完全相同的块代码导入到您的 worker/worklet 中,因为这高度依赖于 webpack 如何生成和使用块。即使你今天设法让它工作,如果 webpack 团队改变了他们内部表示块的方式,它将来可能会崩溃。
同样,从用户的角度来看,他们只想导入库并让它工作,而不用摆弄所有不同的构建步骤。
相反,一种更干净的方法是为主库、AudioWorklet 和 Web Worker 生成单独的文件。而且由于您已经设计了 worklet 和 web worker 来使用您的库,您可以只为它们使用预先构建的非模块库,并为 webpack/其他捆绑器的入口点提供单独的文件。
最直接的方式是让用户将您原始的非模块 js 库添加到他们构建的捆绑包中,并让 es 模块使用该非模块库的 url 加载 Web Workers 和 Audio Worklet。
当然,从用户的角度来看,如果他们不必复制其他文件并将它们放在正确的目录中(或配置脚本目录),那会更容易。直接的方法是从 CDN 加载 Web Worker 或 Worklet(例如 https://unpkg.com/@coderline/ [email protected] /dist/alphaTab.js ),但是跨源加载 Web Worker 存在限制,因此您必须使用一种解决方法,例如先获取它,然后从 Blob URL 加载它(例如 此处 )。不幸的是,这会导致初始化 Worker/Worklet 异步。
捆绑 Worker 代码
如果这不是一个选项,您可以通过将 Worker/Worklet 代码字符串化并通过 blob 或数据 url 加载它,将库、Web Worker/Worklet 代码捆绑到一个文件中。在您的特定用例中,考虑到捆绑输出中将重复多少代码,从效率的角度来看,这有点痛苦。
对于这种方法,您将有多个构建步骤:
- 构建 Web Worker 和/或 Audio Worklet 使用的库。
- 通过字符串化以前的库/库来构建单个库。
这一切都很复杂,因为库、Web Worker 和 Audio Worklet 只有一个入口文件。从长远来看,您可能会通过重写这些不同目标的入口点而受益,但目前,我们可以重用当前的工作流程并使用不同的插件更改构建步骤。对于第一次构建,我们将制作一个插件,当它尝试导入工作库时,它会返回一个虚拟字符串;对于第二次构建,我们将让它返回该库的字符串化内容。我将使用 rollup,因为这是您的项目使用的。下面的代码主要用于说明目的(将工作库保存为
dist/worker-library.js
);我还没有真正测试过它。
第一个插件:
var firstBuildPlugin = {
load(id) {
if (id.includes('worker-library.js')) {
return 'export default "";';
}
return null;
}
}
第二个插件:
var secondBuildPlugin = {
transform(code, id) {
if (id.includes('worker-library.js')) {
return {
code: 'export default ' + JSON.stringify(code) + ';',
map: { mappings: '' }
};
}
return null;
}
}
使用这些插件,我们可以通过
import rawCode from './path/to/worker-library.js';
导入 web worker/audio worklet 库。对于您的情况,由于您将重复使用同一个库,您可能需要创建一个带有导出的新文件,以防止多次捆绑相同的代码:
libraryObjectURL.js
:
import rawCode from '../dist/worker-library.js'; // may need to tweak the path here
export default URL.createObjectURL(
new Blob([rawCode], { type: 'application/javascript' })
);
并实际使用它:
import libraryObjectURL from './libraryObjectURL.js'; // may need to tweak the path here
//...
var worker = new Worker(libraryObjectURL);
然后实际构建它,您的
rollup.config.js
看起来像:
module.exports = [
{
input: `dist/lib/alphatab.js`,
output: {
file: `dist/worker-library.js`,
format: 'iife', // or maybe umd
//...
plugins: [
firstBuildPlugin,
//...
]
}
},
{
input: `dist/lib/alphatab.js`,
output: {
file: `dist/complete-library.mjs`,
format: 'es',
//...
plugins: [
secondBuildPlugin,
//...
]
}
},
// ...
保留旧代码
最后,对于您的其他构建,您可能仍希望保留旧路径。您可以使用 @rollup/plugin-replace 来实现此目的,方法是使用将在构建过程中替换的占位符。
在您的文件中,您可以将:
var worker = new Worker(libraryObjectURL);
替换为:
var worker = new Worker(__workerLibraryURL__);
并在构建过程中使用:
// ...
// for the first build:
plugins: [
firstBuildPlugin,
replace({ __workerLibraryURL__: 'libraryObjectURL')
// ...
],
// ...
// for the second build:
plugins: [
secondBuildPlugin,
replace({ __workerLibraryURL__: 'libraryObjectURL')
// ...
],
// ...
// for all other builds:
plugins: [
firstBuildPlugin,
replace({ __workerLibraryURL__: 'new URL(import.meta.url)') // or whatever the old code was
// ...
],
如果 AudioWorklet 网址不同,您可能需要使用另一个替代网址。如果未使用 worker-library 文件,则导入的
libraryObjectURL
将被树状图删除。
未来工作:
您可能需要研究为不同的目标提供多个输出:Web Worker、音频 Worklet 和库代码。它们实际上不应该加载完全相同的文件。这将消除对第一个插件(忽略某些文件)的需求,并且可能使事情更易于管理和高效。
更多阅读:
- 将文件加载为原始字符串(您可以在此处查看/使用 TrySound 的插件 ;这是一个简单的插件)
- 将字符串加载为 blob 或数据 URL(请参阅 https://stackoverflow.com/a/10372280/ )
我找到了一种解决所述问题的方法,但仍然存在一些未解决的痛点,因为 WebPack 开发人员更愿意尝试避免使用特定于供应商的表达式,而更愿意依赖“可识别的语法结构”来根据他们的需要重写代码。
该解决方案无法在完全本地的环境中工作,但它可以与 NPM 一起使用:
我现在使用
/* webpackChunkName: "alphatab.worker" */ new Worker(new URL('@coderline/alphatab', import.meta.url)))
启动我的工作器,其中
@coderline/alphatab
是通过 NPM 安装的库的名称。此语法结构
由 WebPack 检测
,并将触发生成一个新的特殊 JS 文件,其中包含一些 WebPack 引导程序/入口点,用于加载启动时的库。因此,它在编译后有效地处理如下:
要使其正常工作,用户应配置 WebPack 以将库放在自己的块中。否则,可能会发生库内联到 webpack 生成的工作文件中,而不是也从公共块加载的情况。在没有公共块的情况下,它也可以工作,但它甚至会违背使用 webpack 的好处,因为它会复制库的代码以将其生成为 worker(加载时间和磁盘使用量增加一倍)。
不幸的是,目前这仅适用于 Web Workers,因为 WebPack 目前不支持 Audio Worklet 。 。
此外,由于 WebPack 产生的循环依赖关系,还会出现一些警告,因为 chunk-alphatab.js 和 alphatab.worker.js 之间似乎存在一个循环。在此设置中,这应该不是问题。
就我而言,UI 线程代码和在 worker 中运行的代码没有区别。如果用户决定通过设置渲染到 HTML5 画布,则渲染发生在 UI 线程中,如果使用 SVG 渲染,则会将其卸载到 worker。两侧的整个布局和渲染管道均相同。