如何使用 worker-loader 创建 webworker 的 typescript 库
How to create typescript library with webworkers using worker-loader
我尝试用网络工作者创建打字稿库。当我使用 webpack-dev-server 测试我的代码时,一切看起来都很好,找到了所有文件,但是当我进行 npm 运行 build 并尝试在另一个本地项目(npm install /local/path)中使用 lib 时,我在浏览器控制台中看到 GET http://localhost:8080/X.worker.js
。
webpack.config.js:
const path = require('path');
module.exports = {
devtool: 'inline-source-map',
entry: {
'mylib': './src/index.ts',
'mylib.min': './src/index.ts',
},
output: {
path: path.resolve(__dirname, '_bundles'),
filename: '[name].js',
libraryTarget: 'umd',
library: 'mylib',
umdNamedDefine: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
optimization: {
minimize: true
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
exclude: /node_modules/,
query: {
declaration: false,
}
},
{
test: /\.worker\.js$/,
use: {
loader: "worker-loader"
}
},
]
}
};
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "es6",
"lib": [
"webworker",
"es2015",
"dom"
],
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"alwaysStrict": true,
"outDir": "lib",
"resolveJsonModule": true,
"declaration": true,
"skipLibCheck": true,
"allowJs": true
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"lib",
]
}
package.json
{
"name": "mylib",
"version": "1.0.0",
"description": "",
"main": "_bundles/mylib.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"webpack": "webpack",
"build": "rm -rf ./lib && tsc",
"serve": "webpack-dev-server",
"clean": "rm -rf _bundles lib lib-esm",
"newbuild": "npm run clean && tsc && tsc -m es6 --outDir lib-esm && webpack"
},
"repository": {
"type": "git",
"url": "..."
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "..."
},
"homepage": "...e",
"devDependencies": {
"prettier": "^2.1.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"worker-loader": "^3.0.5"
},
"dependencies": {
...
}
}
关于我如何导入工人的示例:
import X from "worker-loader!./X";
几个月前我发现自己处于完全相同的情况。我找到了适合我的解决方案,但首先让我们讨论一下为什么会这样。
问题:
这里有3层。
- 你的库的开发层
- 你的库的构建层
- 使用库构建的应用程序
第 1 层很简单。在任何你想创建一个新工人的文件中,说它 index.ts
,你做你的 import X from "worker-loader!./X"
。您的 index.ts
确切地知道在哪里可以找到您的工作文件。这就是为什么你的 webpack-dev-server 可以正常工作的原因。
第 2 层是事情变得奇怪的地方。当你用 worker-loader
处理 worker 文件时,webpack 会输出几个文件。您的配置显示 filename: '[name].js'
,它将为源文件夹中的每个文件输出一个文件,所有文件都在 _bundles
文件夹中的同一级别。 Webpack 会看到您的 import X from "worker-loader!./X"
,它还会看到导入文件的目标名称和位置,以及执行导入的文件。它将 within index.js
输出包中的 web worker 文件的位置重写为相对于包其余部分的 absolute 路径。您可以使用 worker-loader 中的 publicPath
选项更仔细地控制它。但这并没有真正解决问题,因为您只是将 publicPath
设置为绝对路径,这导致我们进入第 3 步...
第 3 层,即您尝试使用您的包裹的地方,是出了问题的地方。您永远无法预料到有人可能会在他们的代码中尝试 import { makeAWorker } from 'your-library'
。无论他们在哪里导入它,构建文件(在消费者应用程序的 node_modules 中)将使用 webpack 写入 index.js
构建的路径来查找工作文件,但现在是绝对路径相对于您的消费者项目(通常是主路径,例如 index.html 所在的位置),而不是构建的 node_modules 文件夹。所以您的消费者应用程序不知道在哪里可以找到工作文件。
我的解决方案:有点乱七八糟
在我的场景中,我认为我的工作文件的内容足够简单,可以从字符串创建一个工作人员,然后以这种方式导入它。例如,工作文件如下所示:
// worker.ts
// Build a worker from an anonymous function body
export default URL.createObjectURL(
new Blob([
'(',
function () {
// actual worker content here
}.toString(),
')()', ],
{ type: 'application/javascript' }
)
);
然后在我要生成工人的地方:
// index.ts
import workerScript from './worker';
const myWorker = new Worker(workerScript, {worker_options});
这是可行的,因为现在您不再要求 webpack 创建文件并为您编写正确的导入位置。最后,您甚至不会在您的工作脚本包中有单独的文件。您可以完全放弃 worker-loader
。您的 index.ts
将创建 Blob 及其 URL,您的消费者应用程序将在运行时动态生成的 URL 找到工作脚本。
确实是黑客攻击
这种方法有一些严重的缺点,这让我提出了这个问题 bundle web workers as integral part of npm package with single file webpack output。问题是在你的工作脚本中,你真的没有导入任何东西的选项。我很幸运,因为我的工人比较简单。它依赖于一个 node_module,它本身没有依赖性。我一开始只是简单地在脚本中包含该模块的源代码,但由于我有多个脚本需要相同的外部模块,所以我最终在它生成时将它作为一条数据传递给工作人员:
import { Rainbow } from './utils';
var data = {
id,
data
RainbowAsString: Rainbow.toString(),
};
myWorker.postMessage(data);
然后在 worker 中我简单地将 RainbowAsString
转换回一个函数并使用它。如果您想了解更多细节,可以查看我用这种方法构建的库:leaflet-topography。查看 src/TopoLayer.ts
文件以查看工作人员的使用方式,并查看 src//workers
文件夹以查看我如何设置工作人员 blob。
结论
我想一定有更好的方法。一种可能的快速修复方法是编写一个复制脚本,将工作文件从 node_modules/yourLibrary
复制到您的消费者应用程序的构建文件夹中。但这并不能带来很好的可移植性,其他人将不得不做同样的事情来让你的图书馆与他们的应用程序一起工作。我的解决方案并不完美,但它适用于简单的 worker 脚本。我仍在考虑一个更强大的解决方案,让工人自己做进口。
编辑:正确的解决方案:
所以在写完这个答案后,我受到启发加入了 webpack-loader 存储库中的对话 How can I use this to bundle a library?。显然,大约 2 个月前发布 webpack 5 时,他们添加了对这种语法的支持:
new Worker(new URL('./path/to/worker.js', import.meta.url))
我没有尝试过这个,但它看起来像你需要的解决方案(也是我 2 个月前想出我的 hack 时需要的解决方案)。试试这个 - 这可能正是您需要告诉 webpack 捆绑您的库,同时仍然保持工作脚本和导入它的脚本之间的关系。
我尝试用网络工作者创建打字稿库。当我使用 webpack-dev-server 测试我的代码时,一切看起来都很好,找到了所有文件,但是当我进行 npm 运行 build 并尝试在另一个本地项目(npm install /local/path)中使用 lib 时,我在浏览器控制台中看到 GET http://localhost:8080/X.worker.js
。
webpack.config.js:
const path = require('path');
module.exports = {
devtool: 'inline-source-map',
entry: {
'mylib': './src/index.ts',
'mylib.min': './src/index.ts',
},
output: {
path: path.resolve(__dirname, '_bundles'),
filename: '[name].js',
libraryTarget: 'umd',
library: 'mylib',
umdNamedDefine: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
optimization: {
minimize: true
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader',
exclude: /node_modules/,
query: {
declaration: false,
}
},
{
test: /\.worker\.js$/,
use: {
loader: "worker-loader"
}
},
]
}
};
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "es6",
"lib": [
"webworker",
"es2015",
"dom"
],
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"alwaysStrict": true,
"outDir": "lib",
"resolveJsonModule": true,
"declaration": true,
"skipLibCheck": true,
"allowJs": true
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"lib",
]
}
package.json
{
"name": "mylib",
"version": "1.0.0",
"description": "",
"main": "_bundles/mylib.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"webpack": "webpack",
"build": "rm -rf ./lib && tsc",
"serve": "webpack-dev-server",
"clean": "rm -rf _bundles lib lib-esm",
"newbuild": "npm run clean && tsc && tsc -m es6 --outDir lib-esm && webpack"
},
"repository": {
"type": "git",
"url": "..."
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "..."
},
"homepage": "...e",
"devDependencies": {
"prettier": "^2.1.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"worker-loader": "^3.0.5"
},
"dependencies": {
...
}
}
关于我如何导入工人的示例:
import X from "worker-loader!./X";
几个月前我发现自己处于完全相同的情况。我找到了适合我的解决方案,但首先让我们讨论一下为什么会这样。
问题:
这里有3层。
- 你的库的开发层
- 你的库的构建层
- 使用库构建的应用程序
第 1 层很简单。在任何你想创建一个新工人的文件中,说它 index.ts
,你做你的 import X from "worker-loader!./X"
。您的 index.ts
确切地知道在哪里可以找到您的工作文件。这就是为什么你的 webpack-dev-server 可以正常工作的原因。
第 2 层是事情变得奇怪的地方。当你用 worker-loader
处理 worker 文件时,webpack 会输出几个文件。您的配置显示 filename: '[name].js'
,它将为源文件夹中的每个文件输出一个文件,所有文件都在 _bundles
文件夹中的同一级别。 Webpack 会看到您的 import X from "worker-loader!./X"
,它还会看到导入文件的目标名称和位置,以及执行导入的文件。它将 within index.js
输出包中的 web worker 文件的位置重写为相对于包其余部分的 absolute 路径。您可以使用 worker-loader 中的 publicPath
选项更仔细地控制它。但这并没有真正解决问题,因为您只是将 publicPath
设置为绝对路径,这导致我们进入第 3 步...
第 3 层,即您尝试使用您的包裹的地方,是出了问题的地方。您永远无法预料到有人可能会在他们的代码中尝试 import { makeAWorker } from 'your-library'
。无论他们在哪里导入它,构建文件(在消费者应用程序的 node_modules 中)将使用 webpack 写入 index.js
构建的路径来查找工作文件,但现在是绝对路径相对于您的消费者项目(通常是主路径,例如 index.html 所在的位置),而不是构建的 node_modules 文件夹。所以您的消费者应用程序不知道在哪里可以找到工作文件。
我的解决方案:有点乱七八糟
在我的场景中,我认为我的工作文件的内容足够简单,可以从字符串创建一个工作人员,然后以这种方式导入它。例如,工作文件如下所示:
// worker.ts
// Build a worker from an anonymous function body
export default URL.createObjectURL(
new Blob([
'(',
function () {
// actual worker content here
}.toString(),
')()', ],
{ type: 'application/javascript' }
)
);
然后在我要生成工人的地方:
// index.ts
import workerScript from './worker';
const myWorker = new Worker(workerScript, {worker_options});
这是可行的,因为现在您不再要求 webpack 创建文件并为您编写正确的导入位置。最后,您甚至不会在您的工作脚本包中有单独的文件。您可以完全放弃 worker-loader
。您的 index.ts
将创建 Blob 及其 URL,您的消费者应用程序将在运行时动态生成的 URL 找到工作脚本。
确实是黑客攻击
这种方法有一些严重的缺点,这让我提出了这个问题 bundle web workers as integral part of npm package with single file webpack output。问题是在你的工作脚本中,你真的没有导入任何东西的选项。我很幸运,因为我的工人比较简单。它依赖于一个 node_module,它本身没有依赖性。我一开始只是简单地在脚本中包含该模块的源代码,但由于我有多个脚本需要相同的外部模块,所以我最终在它生成时将它作为一条数据传递给工作人员:
import { Rainbow } from './utils';
var data = {
id,
data
RainbowAsString: Rainbow.toString(),
};
myWorker.postMessage(data);
然后在 worker 中我简单地将 RainbowAsString
转换回一个函数并使用它。如果您想了解更多细节,可以查看我用这种方法构建的库:leaflet-topography。查看 src/TopoLayer.ts
文件以查看工作人员的使用方式,并查看 src//workers
文件夹以查看我如何设置工作人员 blob。
结论
我想一定有更好的方法。一种可能的快速修复方法是编写一个复制脚本,将工作文件从 node_modules/yourLibrary
复制到您的消费者应用程序的构建文件夹中。但这并不能带来很好的可移植性,其他人将不得不做同样的事情来让你的图书馆与他们的应用程序一起工作。我的解决方案并不完美,但它适用于简单的 worker 脚本。我仍在考虑一个更强大的解决方案,让工人自己做进口。
编辑:正确的解决方案:
所以在写完这个答案后,我受到启发加入了 webpack-loader 存储库中的对话 How can I use this to bundle a library?。显然,大约 2 个月前发布 webpack 5 时,他们添加了对这种语法的支持:
new Worker(new URL('./path/to/worker.js', import.meta.url))
我没有尝试过这个,但它看起来像你需要的解决方案(也是我 2 个月前想出我的 hack 时需要的解决方案)。试试这个 - 这可能正是您需要告诉 webpack 捆绑您的库,同时仍然保持工作脚本和导入它的脚本之间的关系。