如何在Angular Web Worker中导入一个Node模块?

8
我尝试在Angular 8 web worker中引入一个节点模块,但是出现了编译错误'无法找到模块'。有人知道如何解决吗?
我按照以上提到的ng文档中所描述的方式,在我的Electron项目中使用ng generate web-worker app创建了一个新的worker。
一切都很正常,直到我添加了一些类似于pathfs-extra的引用:
/// <reference lib="webworker" />    
import * as path from 'path';

addEventListener('message', ({ data }) => {    
  console.log(path.resolve('/'))
  const response = `worker response to ${data}`;
  postMessage(response);
});

这个导入语句在任何其他的TS组件中都可以正常工作,但是在Web Worker中却会出现编译错误,错误信息如下:

Error: app/app.worker.ts:3:23 - error TS2307: Cannot find module 'path'.

我该如何解决这个问题?也许我需要在生成的tsconfig.worker.json中添加一些附加参数?

要重现错误,请运行:

$ git clone https://github.com/hoefling/stackoverflow-57774039
$ cd stackoverflow-57774039
$ yarn build

或者查看项目在Travis上的构建日志build log

注意:

1)我只找到了这个类似的问题,但是答案只处理自定义模块。

2)我使用了minimal electron seed进行了相同的导入测试,它使用了Web Worker,并且能够工作,但是该示例使用了不带Angular的纯JavaScript。

1个回答

20

1. TypeScript错误

正如您所注意到的,第一个错误是TypeScript错误。查看tsconfig.worker.json文件,我发现它将types设置为空数组:

{
  "compilerOptions": {
    "types": [],
    // ...
  }
  // ...
}

指定types会关闭自动包含@types,这在这种情况下是个问题,因为path的类型定义在@types/node中。

因此,让我们通过将node明确添加到types数组中来解决这个问题:

{
  "compilerOptions": {
    "types": [
        "node"
    ],
    // ...
  }
  // ...
}

这个修复了TypeScript的错误,但是再次尝试构建时,我们会遇到一个非常类似的错误。这次是来自Webpack直接报错。

2. Webpack 错误

ERROR in ./src/app/app.worker.ts (./node_modules/worker-plugin/dist/loader.js!./src/app/app.worker.ts)
Module build failed (from ./node_modules/worker-plugin/dist/loader.js):
ModuleNotFoundError: Module not found: Error: Can't resolve 'path' in './src/app'

为了解决这个问题,我们需要深入挖掘一下......
为什么在其他所有模块中导入 path 都能正常工作很重要。Webpack 有一个叫做targets的概念(web、node 等)。Webpack 使用这个目标来决定使用哪些默认选项和插件。
通常情况下,使用 @angular-devkit/build-angular:browser 的 Angular 应用程序的目标是 web。但是在你的情况下,postinstall:electron 脚本实际上会修补 node_modules 来更改它: postinstall.js(为简洁起见省略部分)
const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';

fs.readFile(f_angular, 'utf8', function (err, data) {
  var result = data.replace(/target: "electron-renderer",/g, '');
  var result = result.replace(/target: "web",/g, '');
  var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');

  fs.writeFile(f_angular, result, 'utf8');
});

目标electron-renderer被Webpack视为与node类似的处理方式。特别有趣的是:它默认添加了NodeTargetPlugin
那么这个插件是干什么用的呢?它将所有已知的内建Node.js模块作为外部变量添加进来。在构建应用程序时,Webpack不会尝试捆绑这些外部变量。相反,它们会在运行时使用require解析。这就是为什么即使path没有安装为Webpack所知的模块,也可以通过导入使用。
为什么它对worker无效呢?
Worker使用WorkerPlugin单独编译。在他们的文档中,他们表示:
引用:"默认情况下,当打包工人代码时,WorkerPlugin不会运行任何配置的Webpack插件-这避免了重复运行像html-webpack-plugin这样的东西。对于必须对Worker代码应用插件的情况,请使用plugins选项。"
在深入研究@angular-devkit中使用WorkerPlugin时,我们可以看到以下内容: @angular-devkit/src/angular-cli-files/models/webpack-configs/worker.js(简化)
new WorkerPlugin({
    globalObject: false,
    plugins: [
        getTypescriptWorkerPlugin(wco, workerTsConfigPath)
    ],
})

我们可以看到它使用了plugins选项,但仅用于单个插件,该插件负责TypeScript编译。这样,Webpack配置的默认插件,包括NodeTargetPlugin,会丢失并且不用于worker。
解决方法:
为了解决这个问题,我们需要修改Webpack配置。为此,我们将使用@angular-builders/custom-webpack。请安装该软件包。
接下来,打开angular.json并更新projects > angular-electron > architect > build
"build": {
  "builder": "@angular-builders/custom-webpack:browser",
  "options": {
    "customWebpackConfig": {
      "path": "./extra-webpack.config.js"
    }
    // existing options
  }
}

对于serve,请重复上述步骤。

现在,在与angular.json相同的目录中创建extra-webpack.config.js

const WorkerPlugin = require('worker-plugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');

module.exports = (config, options) => {
  let workerPlugin = config.plugins.find(p => p instanceof WorkerPlugin);
  if (workerPlugin) {
    workerPlugin.options.plugins.push(new NodeTargetPlugin());
  }
  return config;
};

文件会导出一个函数,该函数将被 @angular-builders/custom-webpack 调用,并传入现有的 Webpack 配置对象。然后我们可以搜索所有的 plugins,找到 WorkerPlugin 实例,并在其选项中添加 NodeTargetPlugin 来进行修补。

感谢您详细的回答。确实,编译错误已经消失了,应用程序现在可以启动了,但是“path”导入不起作用,并显示“Uncaught ReferenceError: require is not defined”。我搜索了这个错误,但是找不到任何合适的解决方案。我应该添加其他插件吗?有什么想法吗? - zerocewl
3
您需要在 worker 中启用节点集成。在 main.ts 中创建浏览器窗口的位置,添加 nodeIntegrationInWorker: truewebPreferences 下面即可。 - lukasgeiter
1
太棒了,感谢您提供的解决方案和对问题原因的深入解释! - hoefling

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接