【问题标题】:bundling precompiled binary into electron app将预编译的二进制文件捆绑到电子应用程序中
【发布时间】:2016-01-14 03:32:06
【问题描述】:

关于如何将第三方预编译的二进制文件(如 imagemagick)包含到电子应用程序中,有没有好的解决方案?有 node.js 模块,但它们都是包装器或本机绑定到系统范围内安装的库。我想知道是否可以在发行版中捆绑预编译的二进制文件。

【问题讨论】:

标签: node.js imagemagick electron


【解决方案1】:

请参阅下面的更新(这种方法现在并不理想)。

我确实找到了解决方案,但我不知道这是否被认为是最佳实践。我找不到任何包含 3rd 方预编译二进制文件的好文档,所以我只是摆弄它,直到它最终与我的 ffmpeg 二进制文件一起工作。这是我所做的(从电子快速入门,node.js v6 开始):

Mac OS X 方法

从 app 目录中,我在终端中运行了以下命令,以将 ffmpeg 二进制文件作为一个模块包含在内:

mkdir node_modules/ffmpeg
cp /usr/local/bin/ffmpeg node_modules/ffmpeg/
cd node_modules/.bin
ln -s ../ffmpeg/ffmpeg ffmpeg

(将/usr/local/bin/ffmpeg替换为您当前的二进制路径,从此处下载)放置链接允许电子打包器包含我保存到node_modules/ffmpeg/的二进制文件。

然后为了获得捆绑的应用程序路径(这样我就可以为我的二进制文件使用绝对路径......无论我做什么,相对路径似乎都不起作用)我安装了 npm 包 app-root-dir运行以下命令:

npm i -S app-root-dir

现在我有了根应用程序目录,我只需为我的二进制文件附加子文件夹并从那里生成。这是我放在 renderer.js 中的代码:。

var appRootDir = require('app-root-dir').get();
var ffmpegpath=appRootDir+'/node_modules/ffmpeg/ffmpeg';
console.log(ffmpegpath);

const
    spawn = require( 'child_process' ).spawn,
    ffmpeg = spawn( ffmpegpath, ['-i',clips_input[0]]);  //add whatever switches you need here

ffmpeg.stdout.on( 'data', data => {
     console.log( `stdout: ${data}` );
    });
   ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
    });

Windows 方法

  1. 打开您的电子基础文件夹(electron-quick-start 是默认名称),然后进入 node_modules 文件夹。在那里创建一个名为 ffmpeg 的文件夹,并将您的静态二进制文件复制到此目录中。注意:它必须是你的二进制文件的静态版本,对于 ffmpeg,我抓取了最新的 Windows 版本here

  2. 为了获得捆绑的应用程序路径(这样我就可以为我的二进制文件使用绝对路径......无论我做什么,相对路径似乎都不起作用)我通过运行安装了 npm 包 app-root-dir从我的应用程序目录中的命令提示符执行以下命令:

     npm i -S app-root-dir
    
  3. 在您的 node_modules 文件夹中,导航到 .bin 子文件夹。您需要在此处创建几个文本文件来告诉 node 包含您刚刚复制的二进制 exe 文件。使用您喜欢的文本编辑器并创建两个文件,一个名为 ffmpeg 的文件具有以下内容:

    #!/bin/sh
    basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
    
    case `uname` in
        *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
    esac
    
    if [ -x "$basedir/node" ]; then
      "$basedir/node"  "$basedir/../ffmpeg/ffmpeg" "$@"
      ret=$?
    else
      node  "$basedir/../ffmpeg/ffmpeg" "$@"
      ret=$?
    fi
    exit $ret
    

还有第二个文本文件,名为ffmpeg.cmd

    @IF EXIST "%~dp0\node.exe" (
     "%~dp0\node.exe"  "%~dp0\..\ffmpeg\ffmpeg" %*
    ) ELSE (
       @SETLOCAL
     @SET PATHEXT=%PATHEXT:;.JS;=;%
     node  "%~dp0\..\ffmpeg\ffmpeg" %*
    )

接下来,您可以在 Windows 电子发行版(在 renderer.js 中)中运行 ffmpeg,如下所示(我也在使用 app-root-dir 节点模块)。请注意添加到二进制路径的引号,如果您的应用安装到带有空格的目录(例如C:\Program Files\YourApp),则没有这些将无法工作。

var appRootDir = require('app-root-dir').get();
var ffmpegpath = appRootDir + '\\node_modules\\ffmpeg\\ffmpeg';

const
    spawn = require( 'child_process' ).spawn;
    var ffmpeg = spawn( 'cmd.exe', ['/c',  '"'+ffmpegpath+ '"', '-i', clips_input[0]]);  //add whatever switches you need here, test on command line first
ffmpeg.stdout.on( 'data', data => {
     console.log( `stdout: ${data}` );
 });
ffmpeg.stderr.on( 'data', data => {
     console.log( `stderr: ${data}` );
 });

更新:统一简单方法

好吧,随着时间的推移和 Node 的更新,这种方法不再是包含预编译二进制文件的最简单方法。它仍然可以工作,但是当npm install 运行时,node_modules 下的二进制文件夹将被删除并且必须再次替换。以下方法适用于 Node v12。

这种新方法无需符号链接,并且在 Mac 和 Windows 上的工作方式类似。相对路径现在似乎可以工作了。

  1. 您仍然需要 appRootDir:npm i -S app-root-dir

  2. 在应用的根目录下创建一个名为bin 的文件夹,并将预编译的静态二进制文件放在这里,我以ffmpeg 为例。

  3. 在渲染器脚本中使用以下代码:

const appRootDir = require('app-root-dir').get();
const ffmpegpath = appRootDir + '/bin/ffmpeg';
const spawn = require( 'child_process' ).spawn;
const child = spawn( ffmpegpath, ['-i', inputfile, 'out.mp4']);  //add whatever switches you need here, test on command line first
child.stdout.on( 'data', data => {
    console.log( `stdout: ${data}` );
});
child.stderr.on( 'data', data => {
    console.log( `stderr: ${data}` );
});

【讨论】:

  • 明确地说,此方法使用 electron-packager 应用程序和预编译的 ffmpeg 二进制文件。 [使用电子应用捆绑模块]方法。
  • 您能否解释一下ln -s ../ffmpeg/ffmpeg node_modules/.bin/ffmpeg 您是否链接了您刚刚制作的ffmpeg 副本?您在ln -s 之前更改过目录吗?
  • 对不起,代码不正确。我确实更改了目录,然后创建了链接。添加和更改代码。
  • 你救了我的命。 :-) 与 dcraw 在我的电子应用程序中进行原始图像转换效果很好
  • 很高兴听到它有帮助。我刚刚添加了Windows方法。
【解决方案2】:

这是另一种方法,目前已在 Mac 和 Windows 上进行了测试。需要 'app-root-dir' 包,不需要手动添加任何东西到 node_modules 目录。

  1. 将文件放在 resources/$os/ 下,其中 $os"mac"" linux”,或“win”。构建过程将根据构建目标操作系统从这些目录中复制文件。

  2. extraFiles 选项放入您的构建配置中,如下所示:

package.json

  "build": {
    "extraFiles": [
      {
        "from": "resources/${os}",
        "to": "Resources/bin",
        "filter": ["**/*"]
      }
    ],
  1. 使用类似的方法来确定当前平台。

get-platform.js

import { platform } from 'os';

export default () => {
  switch (platform()) {
    case 'aix':
    case 'freebsd':
    case 'linux':
    case 'openbsd':
    case 'android':
      return 'linux';
    case 'darwin':
    case 'sunos':
      return 'mac';
    case 'win32':
      return 'win';
  }
};
  1. 根据环境和操作系统从您的应用程序调用可执行文件。这里我假设构建版本处于生产模式,源版本处于其他模式,但您可以创建自己的调用逻辑。
import { join as joinPath, dirname } from 'path';
import { exec } from 'child_process';

import appRootDir from 'app-root-dir';

import env from './env';
import getPlatform from './get-platform';

const execPath = (env.name === 'production') ?
  joinPath(dirname(appRootDir.get()), 'bin'):
  joinPath(appRootDir.get(), 'resources', getPlatform());

const cmd = `${joinPath(execPath, 'my-executable')}`;

exec(cmd, (err, stdout, stderr) => {
  // do things
});

我想我是使用electron-builder 作为基础,env 文件生成是它附带的。基本上它只是一个 JSON 配置文件。

【讨论】:

  • 我的文件系统没有 env 文件,我正在使用电子生成器。
【解决方案3】:

以上答案帮助我弄清楚了它是如何做到的,但是有一种非常有效的方法来分发二进制文件。

借鉴tsuriga's answer,这是我的代码:

注意:根据需要替换或添加OS路径。

更新 - 2020 年 12 月 4 日

此答案已更新。找到此答案底部的先前代码。

  • 下载需要的包
    yarn add electron-root-path electron-is-packaged
    
    # or 
    
    npm i electron-root-path electron-is-packaged
  • 创建目录./resources/mac/bin
  • 将二进制文件放在此文件夹中
  • 创建文件./app/binaries.js并粘贴以下代码:
    import path from 'path';
    import { rootPath as root } from 'electron-root-path';
    import { isPackaged } from 'electron-is-packaged';
    import { getPlatform } from './getPlatform';
    
    const IS_PROD = process.env.NODE_ENV === 'production';
    
    const binariesPath =
      IS_PROD && isPackaged // the path to a bundled electron app.
        ? path.join(root, './Contents', './Resources', './bin')
        : path.join(root, './build', getPlatform(), './bin');
    
    export const execPath = path.resolve(
      path.join(binariesPath, './exec-file-name')
    );
  • 创建文件./app/get-platform.js并粘贴以下代码:

    'use strict';
    
    import { platform } from 'os';
    
    export default () => {
      switch (platform()) {
        case 'aix':
        case 'freebsd':
        case 'linux':
        case 'openbsd':
        case 'android':
          return 'linux';
        case 'darwin':
        case 'sunos':
          return 'mac';
        case 'win32':
          return 'win';
      }
    };

  • ./package.json 文件中添加这些行:

    "build": {
    ....
    
     "extraFiles": [
          {
            "from": "resources/mac/bin",
            "to": "Resources/bin",
            "filter": [
              "**/*"
            ]
          }
        ],
    
    ....
    },

  • 将二进制文件导入为:

    import { execPath } from './binaries';
        
    #your program code:
    var command = spawn(execPath, arg, {});

为什么这样更好?

  • 上述答案需要一个名为 app-root-dir

    的附加包
  • tsuriga 的回答没有正确处理 (env=production) buildpre-packed 版本。他/她只负责开发和后打包版本。

上一个答案

  • 避免使用electron.remote,因为它正在贬值
  • app.getAppPath 可能会在主进程中抛出错误。

./app/binaries.js

    'use strict';
    
    import path from 'path';
    import { remote } from 'electron';
    import getPlatform from './get-platform';
    
    const IS_PROD = process.env.NODE_ENV === 'production';
    const root = process.cwd();
    const { isPackaged, getAppPath } = remote.app;
    
    const binariesPath =
      IS_PROD && isPackaged
        ? path.join(path.dirname(getAppPath()), '..', './Resources', './bin')
        : path.join(root, './resources', getPlatform(), './bin');
    
    export const execPath = path.resolve(path.join(binariesPath, './exec-file-name'));

【讨论】:

  • 你是救生员!非常感谢!
【解决方案4】:

tl;博士:

是的,你可以!但它要求您编写自己的独立插件,该插件不对系统库做出任何假设。此外,在某些情况下,您必须确保您的插件已针对所需的操作系统进行编译。


让我们把这个问题分成几个部分:

-Addons(原生模块)

插件是动态链接的共享对象。

换句话说,您可以编写自己的插件,而不依赖于包含您需要的所有代码的系统范围的库(例如,通过静态链接所需的模块)。

您必须考虑到这种方法是特定于操作系统的,这意味着您需要为每个要支持的操作系统编译插件! (取决于您可能使用的其他库)

- Native modules 用于电子

Electron 支持原生 Node 模块,但由于 Electron 使用的 V8 版本与官方 Node 不同,因此在构建原生模块时,您必须手动指定 Electron 头文件的位置

这意味着必须重新构建针对节点标头构建的本机模块才能在电子内部使用。您可以在电子文档中找到方法。

- 将模块与电子应用程序捆绑

我想您希望将您的应用程序作为独立的可执行文件,而不需要用户在他们的机器上安装电子。如果是这样,我可以建议使用electron-packager

【讨论】:

  • 所以我可以下载为每个操作系统预编译的 imagemagick,将其打包到子文件夹中并对其执行?
  • 编辑:如果是 Windows,就像在开关中一样,我使用 imagemagick.exe 等
  • @Tobias 如果您获得可执行文件,您可以使用child_process 模块运行程序。如果您想访问库的功能,则需要整个原生插件方法。
  • 您有没有想过如何将您的应用程序与预编译的二进制文件捆绑在一起?
【解决方案5】:

按照 Ganesh 的回答,这确实是一个很大的帮助,在我的情况下,在 binaries.js 中工作的内容(对于 mac 构建 - 没有测试 windows 或 linux)是:

"use strict";
import path from "path";
import { app } from "electron";

const IS_PROD = process.env.NODE_ENV === "production";
const root = process.cwd();
const { isPackaged } = app;

const binariesPath =
  IS_PROD && isPackaged
    ? path.join(process.resourcesPath, "./bin")
    : path.join(root, "./external");

export const execPath = path.join(binariesPath, "./my_exec_name");

考虑到my_exec_name 位于文件夹./external/bin 中并复制到./Resources/bin 的应用程序包中。我没有使用 get_platforms.js 脚本(在我的情况下不需要)。打包应用程序时 app.getAppPath() 正在生成崩溃。 希望对您有所帮助。

【讨论】:

    【解决方案6】:

    很大程度上基于 Ganesh 的回答,但有所简化。另外我使用的是Vue CLI Electron Builder Plugin,所以配置必须放在稍微不同的地方。

    1. 创建一个resources 目录。把你所有的文件放在那里。
    2. 将此添加到vue.config.js
    module.exports = {
      pluginOptions: {
        electronBuilder: {
          builderOptions: {
            ...
            "extraResources": [
              {
                "from": "resources",
                "to": ".",
                "filter": "**/*"
              }
            ],
            ...
          }
        }
      }
    }
    
    1. 在您的 src 文件夹中创建一个名为 resources.ts 的文件,其中包含以下内容:
    import path from 'path';
    import { remote } from 'electron';
    
    // Get the path that `extraResources` are sent to. This is `<app>/Resources`
    // on macOS. remote.app.getAppPath() returns `<app>/Resources/app.asar` so
    // we just get the parent directory. If the app is not packaged we just use
    // `<current working directory>/resources`.
    export const resourcesPath = remote.app.isPackaged ?
                                 path.dirname(remote.app.getAppPath()) :
                                 path.resolve('resources');
    

    请注意,我尚未在 Windows/Linux 上对此进行测试,但假设 app.asar 位于这些平台的资源目录中(我假设如此),它应该可以工作。

    1. 像这样使用它:
    import { resourcesPath } from '../resources'; // Path to resources.ts
    
    ...
        loadFromFile(resourcesPath + '/your_file');
    

    【讨论】:

      猜你喜欢
      • 2016-11-16
      • 2016-02-24
      • 2014-06-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多