在 Electron 的渲染器中使用 Node.js 插件和 Webpack
Using Node.js addons in Electron's renderer with Webpack
我有以下渲染器:
import SerialPort from "serialport";
new SerialPort("/dev/tty-usbserial1", { baudRate: 57600 });
它由 Webpack 构建,具有以下配置(为简洁起见进行了删减):
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
一起提供,并由主进程作为网页加载(这是开发期间热模块替换所必需的)。
主进程由 Webpack 构建并发送到 dist
。 Webpack 插件还会生成以下 dist/package.json
:
{
"name": "my-app",
"main": "main.js"
}
当我 运行 electron dist
时,渲染器进程崩溃并出现以下错误:
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)
我该如何解决这个问题?
问题
第一个问题是 node-bindings
,node-serialport
依赖它来解析其 Node.js 插件的路径,但在 Electron 中根本不起作用。为此有一个 open issue,我认为相关的 PR 甚至都不是一个完整的修复程序,因为我已经进行了一些调试,并且看起来 fileName
在整个过程中仍然是 undefined
整个getFileName
.
第二个问题:即使它以某种方式在某处找到了 serialport.node
,在打包应用程序进行分发后它也不会工作,因为插件本身不在 dist
目录中,而且 Webpack 不能将它与主 JS 文件捆绑在一起。
人们可以尝试用 node-loader
来解决这个问题,给定一个正常工作的 node-bindings
,但这也无济于事,因为 node-bindings
使用了复杂的启发式算法,Webpack 可以简单地做到这一点当试图了解其 require
可能需要哪些文件时,不要从中推断。 Webpack 可以做的唯一安全的事情是包含整个项目,"just in case",显然,这是绝对不行的,所以 node-loader
不会复制任何东西。
所以,我们需要手动替换node-bindings
和复制serialport.node
。
解决方案
首先,我们必须抓取插件并将其放入 dist
。这需要在 main 的 Webpack 构建中完成,因为渲染器作为网页提供,可能来自内存文件系统(因此 *.node
文件可能不会发送到磁盘,Electron 永远不会看到它) .方法如下:
import CopyWebpackPlugin from "copy-webpack-plugin";
const config = {
// ...
plugins: [
new CopyWebpackPlugin([
"node_modules/serialport/build/Release/serialport.node",
]),
],
// ...
};
不幸的是,它是硬编码的,但是如果有什么变化很容易修复。
其次,我们必须用我们自己的垫片替换node-bindings
,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 中的某个目录。
这是在渲染器的 Webpack 配置中完成替换的方法:
import { NormalModuleReplacementPlugin } from "webpack";
const config = {
// ...
plugins: [
new NormalModuleReplacementPlugin(
/^bindings$/,
`${__dirname}/src/bindings`
),
],
// ...
};
就是这样!完成上述操作后,index.html
+ renderer.js
将由某个服务器(或任何您的方法)提供服务,并且 dist
看起来像这样:
dist/
main.js
package.json
serialport.node
electron dist
应该 "just work".
备选方案
可能会添加 node-serialport
作为对生成的 dist/package.json
的依赖项,然后 npm i
将其安装在那里,并将 serialport
标记为外部Webpack,但感觉更脏(包版本不匹配等)。
另一种方法是将所有内容声明为外部,让 electron-packager
为您将 node_modules
的整个生产部分复制到 dist
,但这会占用很多兆字节基本上没有。
我要感谢@Alec Mev,因为他实际上是整个互联网上唯一真正有效且内含真正知识的答案。我花了几天时间解决 webpack 和电子本机模块的问题,直到我尝试了 Alec 的答案并且它完美运行。
只是一个小的补充,如果在主进程中使用本机代码而不是渲染器(最常见的情况是应用程序安全性更强),则需要按以下方式调整 shim:
module.exports = x =>
__non_webpack_require__(
`${require('electron').app.getAppPath()}/${x}`
);
写的时候觉得@Alec Mev
回答很精彩
就我而言,所有互联网建议的黑客攻击(如复制、加载程序等)都没有成功,而且真的不清楚。
我想通过电子渲染器调用我自己的本机节点插件,但正如 Alec Webpack 所提到的那样,它没有按要求处理 .node 文件。所以就我而言:
文件夹结构
Electron project dir
│ electron.main.js
│ package.json
│
├───Addon
│ │ addon.js
│ │ package.json
│ │
│ └───dist
│ addon.node
│
└───render-site
│ index.html
│ js.js
│ webpack.config.js
│
└───build
- 这是演示 -
render-site
是您的 webpack 站点,dist
是您的插件二进制文件位置。
非常重要的是将 webpack 目标设置为 'electron-renderer'
,它允许您更轻松地使用 electron
和其他节点模块 fs
的 require。
在您的站点中调用插件,就像 docs 建议的那样
const addon = require('electron').remote.require('./addon/dist/addon.node');
就是这样。您可以 运行 webpack 并在使用插件的同时为站点提供服务。
生产非常简单,您只需确保将插件复制到 electron 的构建(我正在使用 electron-builder 复制系统),相对路径为 electron.main.js.
希望对您有所帮助。
我在 electron-renderer
中尝试使用本机模块时遇到了类似的错误。
虽然在我的例子中错误信息是这样的:
node-loader: The specified module could not be found
我相信这都与节点加载器有关。对我来说,native-addon-loader 是运行良好的加载器。
另外,请确保您的 Webpack 目标是 node
、electron-main
或 electron-renderer
,否则它将无法工作(无法在客户端使用本机模块)。
我有以下渲染器:
import SerialPort from "serialport";
new SerialPort("/dev/tty-usbserial1", { baudRate: 57600 });
它由 Webpack 构建,具有以下配置(为简洁起见进行了删减):
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
一起提供,并由主进程作为网页加载(这是开发期间热模块替换所必需的)。
主进程由 Webpack 构建并发送到 dist
。 Webpack 插件还会生成以下 dist/package.json
:
{
"name": "my-app",
"main": "main.js"
}
当我 运行 electron dist
时,渲染器进程崩溃并出现以下错误:
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)
我该如何解决这个问题?
问题
第一个问题是 node-bindings
,node-serialport
依赖它来解析其 Node.js 插件的路径,但在 Electron 中根本不起作用。为此有一个 open issue,我认为相关的 PR 甚至都不是一个完整的修复程序,因为我已经进行了一些调试,并且看起来 fileName
在整个过程中仍然是 undefined
整个getFileName
.
第二个问题:即使它以某种方式在某处找到了 serialport.node
,在打包应用程序进行分发后它也不会工作,因为插件本身不在 dist
目录中,而且 Webpack 不能将它与主 JS 文件捆绑在一起。
人们可以尝试用 node-loader
来解决这个问题,给定一个正常工作的 node-bindings
,但这也无济于事,因为 node-bindings
使用了复杂的启发式算法,Webpack 可以简单地做到这一点当试图了解其 require
可能需要哪些文件时,不要从中推断。 Webpack 可以做的唯一安全的事情是包含整个项目,"just in case",显然,这是绝对不行的,所以 node-loader
不会复制任何东西。
所以,我们需要手动替换node-bindings
和复制serialport.node
。
解决方案
首先,我们必须抓取插件并将其放入 dist
。这需要在 main 的 Webpack 构建中完成,因为渲染器作为网页提供,可能来自内存文件系统(因此 *.node
文件可能不会发送到磁盘,Electron 永远不会看到它) .方法如下:
import CopyWebpackPlugin from "copy-webpack-plugin";
const config = {
// ...
plugins: [
new CopyWebpackPlugin([
"node_modules/serialport/build/Release/serialport.node",
]),
],
// ...
};
不幸的是,它是硬编码的,但是如果有什么变化很容易修复。
其次,我们必须用我们自己的垫片替换node-bindings
,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 中的某个目录。
这是在渲染器的 Webpack 配置中完成替换的方法:
import { NormalModuleReplacementPlugin } from "webpack";
const config = {
// ...
plugins: [
new NormalModuleReplacementPlugin(
/^bindings$/,
`${__dirname}/src/bindings`
),
],
// ...
};
就是这样!完成上述操作后,index.html
+ renderer.js
将由某个服务器(或任何您的方法)提供服务,并且 dist
看起来像这样:
dist/
main.js
package.json
serialport.node
electron dist
应该 "just work".
备选方案
可能会添加 node-serialport
作为对生成的 dist/package.json
的依赖项,然后 npm i
将其安装在那里,并将 serialport
标记为外部Webpack,但感觉更脏(包版本不匹配等)。
另一种方法是将所有内容声明为外部,让 electron-packager
为您将 node_modules
的整个生产部分复制到 dist
,但这会占用很多兆字节基本上没有。
我要感谢@Alec Mev,因为他实际上是整个互联网上唯一真正有效且内含真正知识的答案。我花了几天时间解决 webpack 和电子本机模块的问题,直到我尝试了 Alec 的答案并且它完美运行。
只是一个小的补充,如果在主进程中使用本机代码而不是渲染器(最常见的情况是应用程序安全性更强),则需要按以下方式调整 shim:
module.exports = x =>
__non_webpack_require__(
`${require('electron').app.getAppPath()}/${x}`
);
写的时候觉得@Alec Mev
回答很精彩
就我而言,所有互联网建议的黑客攻击(如复制、加载程序等)都没有成功,而且真的不清楚。
我想通过电子渲染器调用我自己的本机节点插件,但正如 Alec Webpack 所提到的那样,它没有按要求处理 .node 文件。所以就我而言:
文件夹结构
Electron project dir
│ electron.main.js
│ package.json
│
├───Addon
│ │ addon.js
│ │ package.json
│ │
│ └───dist
│ addon.node
│
└───render-site
│ index.html
│ js.js
│ webpack.config.js
│
└───build
- 这是演示 -
render-site
是您的 webpack 站点,dist
是您的插件二进制文件位置。
非常重要的是将 webpack 目标设置为 'electron-renderer'
,它允许您更轻松地使用 electron
和其他节点模块 fs
的 require。
在您的站点中调用插件,就像 docs 建议的那样
const addon = require('electron').remote.require('./addon/dist/addon.node');
就是这样。您可以 运行 webpack 并在使用插件的同时为站点提供服务。
生产非常简单,您只需确保将插件复制到 electron 的构建(我正在使用 electron-builder 复制系统),相对路径为 electron.main.js.
希望对您有所帮助。
我在 electron-renderer
中尝试使用本机模块时遇到了类似的错误。
虽然在我的例子中错误信息是这样的:
node-loader: The specified module could not be found
我相信这都与节点加载器有关。对我来说,native-addon-loader 是运行良好的加载器。
另外,请确保您的 Webpack 目标是 node
、electron-main
或 electron-renderer
,否则它将无法工作(无法在客户端使用本机模块)。