在带有 Webpack 的 Electron 渲染器中使用 Node.js 插件

我有以下渲染器:

import SerialPort from "serialport";

new SerialPort("/dev/tty-usbserial1", { baudRate: 57600 });

它由 Webpack 构建,具有以下配置(为简洁起见):

It's built by Webpack, with the following config (trimmed for brevity):

const config = {
  entry: { renderer: ["./src/renderer"] }
  output: {
    path: `${__dirname}/dist`,
    filename: "[name].js",
  },
  target: "electron-renderer",
  node: false, // Disables __dirname mocking and such
};

它由开发服务器提供服务,与 index.html 一起,并由主进程作为网页加载(这是开发过程中热模块更换所需要的).

It's served by a development server, along with an index.html, and is loaded by the main process as a web page (this is needed for hot module replacement during development).

主进程由 Webpack 构建并发送到 dist.Webpack 插件还会生成以下 dist/package.json:

The main process is built by Webpack and emitted to dist too. A Webpack plugin also generates the following dist/package.json:

{
  "name": "my-app",
  "main": "main.js"
}

当我运行 electron dist 时,渲染器进程崩溃并出现以下错误:

When I run electron dist, the renderer process crashes with the following error:

Uncaught TypeError: Path must be a string. Received undefined
    at assertPath (path.js:28)
    at dirname (path.js:1364)
    at Function.getRoot (bindings.js?dfc1:151)
    at bindings (bindings.js?dfc1:60)
    at eval (linux.js?d488:2)
    at Object../node_modules/serialport/lib/bindings/linux.js (renderer.js:12686)
    at __webpack_require__ (renderer.js:712)
    at fn (renderer.js:95)
    at eval (auto-detect.js?3cc7:16)
    at Object../node_modules/serialport/lib/bindings/auto-detect.js (renderer.js:12638)

我该如何解决这个问题?

How do I fix this?

推荐答案

问题

第一个问题是 node-bindingsnode-serialport 依赖于解析其 Node.js 插件的路径,在 Electron 中根本不起作用.有一个 未决问题 ,我认为相关的 PR 不是甚至是一个完整的修复,因为我已经进行了一些调试,并且似乎 fileName 在整个 getFileName 中仍然是 undefined.

Problem

The first problem is that node-bindings, which node-serialport relies on to resolve the path to its Node.js addon, simply doesn't work in Electron. There's an open issue for this, and I don't think the associated PR is even a complete fix, since I've done some debugging, and it appears that fileName remains undefined throughout the whole getFileName.

第二个问题:即使它以某种方式在某处找到了 serialport.node,在打包应用程序以进行分发后它也无法工作,因为插件本身不在 dist 中 目录,Webpack 不能把它和主 JS 文件捆绑在一起.

The second problem: even if it somehow found a serialport.node somewhere, it wouldn't work after packaging the application for distribution, since the addon itself isn't in the dist directory, and Webpack can't just bundle it together with the main JS file.

可以尝试使用 node-loader,给定一个正确工作的 node-bindings,但这也无济于事,因为 node-bindings 使用精细的启发式方法,Webpack 根本无法从中推断,当试图了解其 require 可能需要哪些文件.Webpack 可以做的唯一安全的事情是包含整个项目,以防万一",显然这是绝对不行的,所以 node-loader 只是不复制任何东西.

One could attempt to solve this with node-loader, given a correctly working node-bindings, but that wouldn't help either, since node-bindings uses elaborate heuristics, which Webpack simply can't extrapolate from, when trying to understand what files could be required by its require. The only safe thing Webpack could do is include the whole project, "just in case", and that's a certain no-go, obviously, so node-loader just doesn't copy anything.

所以,我们需要手动替换node-bindings并复制serialport.node.

So, we need to replace node-bindings and copy serialport.node manually.

首先,我们必须抓取插件并将其放入dist.这需要在 main 的 Webpack 构建中完成,因为渲染器作为网页提供,可能来自内存中的文件系统(因此 *.node 文件可能不会发送到磁盘,而 Electron永远不会看到它).方法如下:

First, we must grab the addon and put it in dist. This needs to be done in main's Webpack build, since the renderer is served as web page, potentially from an in-memory file system (so the *.node file may not be emitted to disk, and Electron will never see it). Here's how:

import CopyWebpackPlugin from "copy-webpack-plugin";

const config = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      "node_modules/serialport/build/Release/serialport.node",
    ]),
  ],
  // ...
};

不幸的是,硬编码,但如果发生变化,很容易修复.

Hardcoded, unfortunately, but easy to fix if something changes.

其次,我们必须用我们自己的 shim 替换 node-bindingssrc/bindings.js:

Second, we must substitute node-bindings with our own shim, src/bindings.js:

module.exports = x =>
  __non_webpack_require__(
    `${require("electron").remote.app.getAppPath()}/${x}`
  );

__non_webpack_require__ 是不言自明的(是的,普通的 require 不起作用,没有一些技巧,因为它由 Webpack 处理),并且 require("electron").remote.app.getAppPath() 是必要的,因为 __dirname 实际上并没有解决人们所期望的 - dist 的绝对路径- 而是放到 Electron 深处的某个目录.

__non_webpack_require__ is self-explanatory (yes, plain require won't work, without some trickery, as it's handled by Webpack), and the require("electron").remote.app.getAppPath() is necessary because __dirname doesn't actually resolve to what one would expect - an absolute path to dist - but rather to some directory buried deep inside Electron.

在渲染器的 Webpack 配置中,替换是这样完成的:

And here's how the replacement is done, in renderer's Webpack config:

import { NormalModuleReplacementPlugin } from "webpack";

const config = {
  // ...
  plugins: [
    new NormalModuleReplacementPlugin(
      /^bindings$/,
      `${__dirname}/src/bindings`
    ),
  ],
  // ...
};

就是这样!一旦完成上述操作,并且 index.html + renderer.js 正在由某个服务器(或任何您的方法)提供服务,并且 dist看起来像这样:

And that's it! Once the above is done, and index.html + renderer.js are being served by some server (or whatever your approach is), and the dist looks something like this:

dist/
  main.js
  package.json
  serialport.node

electron dist 应该正常工作".

node-serialport 作为对生成的 dist/package.json 的依赖项添加并且只需 npm installing 可能会成功在那里,并将 serialport 标记为 Webpack 中的外部,但这感觉更脏(包版本不匹配等).

Could potentially get away with adding node-serialport as a dependency to the generated dist/package.json and just npm installing it in there, and marking serialport as an external in Webpack, but that feels even dirtier (package version mismatches, etc.).

另一种方法是将所有内容声明为外部,并让 electron-packager 只需将 node_modules 的整个生产部分复制到 dist 即可你,但那是一大堆兆字节,基本上什么都没有.

Another way is to just declare everything as externals, and have electron-packager just copy the whole production part of node_modules to dist for you, but that's a whole lot of megabytes for basically nothing.

相关文章