退出

  • 文章收藏

  • 消息

  • 修改资料

  • 该文章原文为《Electron as GUI of Python Applications》, 原文链接在文末标明,本站保留译文版权。

    这篇文章展示了如何用 Electron 作为 Python 应用的 GUI 部分(是我之前文章的一个更新)。前后端之间的通信通过 zerorpc 来实现。完整的代码发布在 Github

    原文及争论

    注意

    这篇文章是我几年前所写的一篇文章的更新版本。如果你没有读过那篇文章,你也没有必要去读。

    争论

    我没有想到之前那篇文章吸引了那么多的读者。有些人还将它发布在 Haker News 和 Reddit 上。同样也有很多对它的批评(criticisms)。针对这些争论,我想分享一下我的回复。

    你不知道 Tkinter, GTK, QT(PySide 和 PyQT), wxPython, Kivy, thrust, ...?

    当然,至少我知道它们的存在,而且试过它们中间的几个。我仍然认为 QT 是它们中间最好的一个。另外,pyotherside 也是一个活跃的 Python 绑定。我只是在这里提供另外一种“Web技术驱动”的方法。

    ...还有 cefpython。

    或多或少,相比起 Electron,它处在更底层的位置。例如,PySide 就是基于它的。

    我可以直接用 Javascript 来写!

    是的。除非有些库——例如 numpy——JS 里没有。另外,我们关注的是如何用 Electron / Javascript / web 技术来改善Python应用。

    我可以用 QT WebEngine。

    那就去试试吧。不过既然你都用了“web 引擎”,干嘛不给 Electron 一个机会呢?

    你有两个运行时!

    是的。一个 Javascript 的,一个 Python 的。没办法,Python 和 Javascript 都是动态语言,很多时候都需要运行时的支持。

    架构和选项

    在之前的文章中,我展示了一个架构的示例:通过 Python 构建一个本地服务器,而Electron 则作为一个本地浏览器。

    start
     |
     V
    +------------+
    |            | start
    |            +-------------> +-------------------+
    |  electron  | sub process   |                   |
    |            |               | python web server |
    | (basically |     http      |                   |
    |  browser)  | <-----------> | (business logic)  |
    |            | communication |                   |
    |            |               | (all html/css/js) |
    |            |               |                   |
    +------------+               +-------------------+
    

    这是一个不太优雅(not-so-efficient)的解决办法。

    让我们重新思考一下我们的核心需求:我们有一个Python应用,和一个Node.js应用(Electron)。如何将它们结合起来,并让它们能够彼此通信?

    我们事实上需要一种跨进程通信(IPC, interprocess Communication)机制。这是无法避免的,除非 Python 和 javascript 互相可以外部调用。

    HTTP 只是流行的IPC方式中的一种,而且仅仅是在写上篇文章的时候第一个跑到我脑子里而已。

    我们有更多的选择。

    我们可以(而且应该)使用 socket。然后,在此基础上,我们需要一个抽象的消息层(messaging layer),可以用 ZeroMq 部署,因为它是最好的消息库(messaging libraries)之一。更进一步,我们需要再原始数据之上定义一些 schema,这可以由zerorpc实现。

    (幸运的是,zerorpc符合我们的需求,因为它支持 Python 和 Node.js。如果需要支持更多语言,你可以看看 gRPC。)

    因此,再这篇文章中,我将展示一个用 zerorpc 通信的例子,这个例子将比我之前展示的方法更高效。

    start
     |
     V
    +--------------------+
    |                    | start
    |  electron          +-------------> +------------------+
    |                    | sub process   |                  |
    | (browser)          |               | python server    |
    |                    |               |                  |
    | (all html/css/js)  |               | (business logic) |
    |                    |   zerorpc     |                  |
    | (node.js runtime,  | <-----------> | (zeromq server)  |
    |  zeromq client)    | communication |                  |
    |                    |               |                  |
    +--------------------+               +------------------+

    准备

    注意,这个示例可以成功在以下环境运行:Windows 10,Python 3.6,Electron 1.7,Node.js v6。

    我们需要用到:python 应用,pip,node,npm,并能通过命令行使用。为了使用zerorpc,我们还需要C/C++编译器(cc 和 c++ 的命令行工具,以及 Windows 上的 MSVC)。

    这个project的架构是:

    .
    |-- index.html
    |-- main.js
    |-- package.json
    |-- renderer.js
    |
    |-- pycalc
    |   |-- api.py
    |   |-- calc.py
    |   `-- requirements.txt
    |
    |-- LICENSE
    `-- README.md

    正如上图所示,Python应用被放置在一个子文件夹(subfolder)里。在这个例子中,Python 应用 pycalc/calc.py 提供以下功能:calc(text),可以接受一段文本,例如 1 + 1/2,并返回结果,例如1.5。pycalc/api.py就是我们的目标。

    index.html, main.js, package.json 和 renderer.js 都是从 electron-quick-start 修改而来。

    Python的部分

    首先,我们已经可以运行Python应用,那么Python环境应该是没问题的。我强烈建议你们使用 virtualenv 来开发 Python 应用。

    试着安装 zerorpc 和 pyinstaller。

    pip install zerorpc
    pip install pyinstaller
    

    如果成功的话,上述命令应该不会出现问题。否则请到网上寻找解决办法(guide)。

    Node.js / Electron 的部分

    第二步,试着配置 Node.js 和 Electron 环境。我假设 node 和 npm 已经能通过 command  line 使用,并且是最新版本的。

    我们需要配置 package.json,  特别是 main 这个入口:

    {
      "name": "pretty-calculator",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "dependencies": {
        "zerorpc": "git+https://github.com/fyears/zerorpc-node.git"
      },
      "devDependencies": {
        "electron": "^1.7.6",
        "electron-packager": "^9.0.1"
      }
    }

    清除缓存:

    # On Linux / OS X
    # clean caches, very important!!!!!
    rm -rf ~/.node-gyp
    rm -rf ~/.electron-gyp
    rm -rf ./node_modules
    # On Window PowerShell (not cmd.exe!!!)
    # clean caches, very important!!!!!
    Remove-Item "$($env:USERPROFILE)\.node-gyp" -Force -Recurse -ErrorAction Ignore
    Remove-Item "$($env:USERPROFILE)\.electron-gyp" -Force -Recurse -ErrorAction Ignore
    Remove-Item .\node_modules -Force -Recurse -ErrorAction Ignore

    (译者注:如果在 Windows 中执行上述命令,提示 Ignore 不是有效的枚举值,可以将该值更改为 SilentlyContinue 后执行。因为 Ignore 是在 Powershell 3.0 加入的。参考微软官方文档

    然后运行npm:

    # 1.7.6 is the version of electron
    # It's very important to set the electron version correctly!!!
    # check out the version value in your package.json
    npm install --runtime=electron --target=1.7.6
    
    # verify the electron binary and its version by opening it
    ./node_modules/.bin/electron

    Npm install 将会从我的复刻安装 zerorpc-node,这样就可以不用从源代码编译了。

    (如果需要,在项目文件夹中添加 ./.npmrc 文件夹)

    现在所有的库都应该部署完毕了。

    可选:从源码编译

    如果上面的安装方式出现了错误,即使你设置了正确的 electron 版本,我们也许只能从源码来编译了。

    讽刺的是,为了编译 Node.js C/C++  native code,我们必须配置 python2,不管你的 Python 应用用的是什么版本。你可以查看官方说明

    特别是,如果你的工作环境是 Windows,用管理员权限打开 PowerShell,运行 npm install --global --production windows-build-tools  来在 %USERPROFILE%\.windows-build-tools\python27 中安装一个独立的 Python 2.7 以及其他所需的 VS 库。我们只需要使用这一次。

    接下来,按照上面所说的方法清理缓存。

    设定好 npm 的版本,并且安装好所需的库。

    配置环境变量:

    # On Linux / OS X:
    
    # env
    export npm_config_target=1.7.6 # electron version
    export npm_config_runtime=electron
    export npm_config_disturl=https://atom.io/download/electron
    export npm_config_build_from_source=true
    
    # may not be necessary
    #export npm_config_arch=x64
    #export npm_config_target_arch=x64
    
    npm config ls
    # On Window PowerShell (not cmd.exe!!!)
    
    $env:npm_config_target="1.7.6" # electron version
    $env:npm_config_runtime="electron"
    $env:npm_config_disturl="https://atom.io/download/electron"
    $env:npm_config_build_from_source="true"
    
    # may not be necessary
    #$env:npm_config_arch="x64"
    #$env:npm_config_target_arch="x64"
    
    npm config ls

    接下来安装:

    # in the same shell as above!!!
    # because you want to make good use of the above environment variables
    
    # install everything based on the package.json
    npm install
    
    # verify the electron binary and its version by opening it
    ./node_modules/.bin/electron

    (如果需要,在项目文件夹中添加 ./.npmrc 文件夹)

    核心功能

    Python 部分

    我们要再 Python 端建立一个 ZeroMQ  服务器。

    将 calc.py 放到 pycalc/ 文件夹中。然后在这个路径下再创建一个 pycalc/api.py。查看 zerorpc-python 作为参考。

    from __future__ import print_function
    from calc import calc as real_calc
    import sys
    import zerorpc
    
    class CalcApi(object):
        def calc(self, text):
            """based on the input text, return the int result"""
            try:
                return real_calc(text)
            except Exception as e:
                return 0.0
        def echo(self, text):
            """echo any text"""
            return text
    
    def parse_port():
        return 4242
    
    def main():
        addr = 'tcp://127.0.0.1:' + parse_port()
        s = zerorpc.Server(CalcApi())
        s.bind(addr)
        print('start running on {}'.format(addr))
        s.run()
    
    if __name__ == '__main__':
        main()

    为了测试,在终端运行 python pycalc/api.py。然后打开另一个终端,运行下面的命令,查看结果:

    zerorpc tcp://localhost:4242 calc "1 + 1"
    ## connecting to "tcp://localhost:4242"
    ## 2.0

    结束调试后,记得终止 Python 程序。

    事实上,这只是另外一个服务器,通过构建于 TCP 之上的 zeromq 进行通信,而不是构建于 HTTP 之上的传统 web 服务器。

    Node.js / Electron 部分

    基本思想:在主进程(main process)中,引用 Python 作为子进程(child process),并构建窗口。在渲染进程中,使用 Node.js 运行时和 zerorpc 库来 Python 子进程通信。所有的 HTML / Javascript / CSS 都由 Electron 管理,而不是 Python web 服务器(像我之前那个例子里一样)。

    在 main.js 中,这些是默认的一些初始代码,没什么特别的:

    // main.js
    
    const electron = require('electron')
    const app = electron.app
    const BrowserWindow = electron.BrowserWindow
    const path = require('path')
    
    let mainWindow = null
    const createWindow = () => {
      mainWindow = new BrowserWindow({width: 800, height: 600})
      mainWindow.loadURL(require('url').format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
      }))
      mainWindow.webContents.openDevTools()
      mainWindow.on('closed', () => {
        mainWindow = null
      })
    }
    app.on('ready', createWindow)
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit()
      }
    })
    app.on('activate', () => {
      if (mainWindow === null) {
        createWindow()
      }
    })

    我们要加上一些代码来运行 Python 子进程:

    // add these to the end or middle of main.js
    
    let pyProc = null
    let pyPort = null
    
    const selectPort = () => {
      pyPort = 4242
      return pyPort
    }
    
    const createPyProc = () => {
      let port = '' + selectPort()
      let script = path.join(__dirname, 'pycalc', 'api.py')
      pyProc = require('child_process').spawn('python', [script, port])
      if (pyProc != null) {
        console.log('child process success')
      }
    }
    
    const exitPyProc = () => {
      pyProc.kill()
      pyProc = null
      pyPort = null
    }
    
    app.on('ready', createPyProc)
    app.on('will-quit', exitPyProc)

    在 index.html 中,我们有一个 <input> 来接收用户输入,还有一个 <div> 负责输出:

    <!-- index.html -->
    <!DOCTYPE html>
    <html>
     <head>
     <meta charset="UTF-8">
     <title>Hello Calculator!</title>
     </head>
     <body>
     <h1>Hello Calculator!</h1>
     <p>Input something like <code>1 + 1</code>.</p>
     <p>This calculator supports <code>+-*/^()</code>,
     whitespaces, and integers and floating numbers.</p>
     <input id="formula" value="1 + 2.0 * 3.1 / (4 ^ 5.6)"></input>
     <div id="result"></div>
     </body>
     <script>
     require('./renderer.js')
     </script>
    </html>

    在 renderer.js 中,我们有一些用来初始化 zerorpc 客户端的代码,以及查看输入变化的代码。当用户输入一些公式的时候,JS 会发送这些文字到 Python 后端,并返回计算结果。

    // renderer.js
    
    const zerorpc = require("zerorpc")
    let client = new zerorpc.Client()
    client.connect("tcp://127.0.0.1:4242")
    
    let formula = document.querySelector('#formula')
    let result = document.querySelector('#result')
    formula.addEventListener('input', () => {
      client.invoke("calc", formula.value, (error, res) => {
        if(error) {
          console.error(error)
        } else {
          result.textContent = res
        }
      })
    })
    formula.dispatchEvent(new Event('input'))

    运行

    运行下面的命令,神奇的事发生了:

    ./node_modules/.bin/electron .

    Awesome!

    如果出现错误,类似动态链接错误,试着清除缓存,然后重新安装库:

    rm -rf node_modules
    rm -rf ~/.node-gyp ~/.electron-gyp
    
    npm install

    打包

    有人问我应该怎么打包。这很简单:运用如何打包 Python 应用以及 Electron 应用的知识。

    Python 部分

    使用 PyInstaller。

    在终端运行以下命令:

    pyinstaller pycalc/api.py --distpath pycalcdist
    
    rm -rf build/
    rm -rf api.spec

    如果一切正常的话,会生成 pycalcdist/api/ 文件夹,包含了一个 exe 执行文件。这是完全独立的 Python exe,可以移植到其它地方。

    注意:你必须生成这个独立的 exe 执行文件,因为其它电脑上可能没有运行这些代码所需的 python 环境和所需的库。直接把 Python 代码复制到别的地方是不行的。

    Node.js / Electron 部分

    这有点难,因为我们将 Python 打包成了 exe。

    在上面的代码中,我写过:

     // part of main.js
      let script = path.join(__dirname, 'pycalc', 'api.py')
      pyProc = require('child_process').spawn('python', [script, port])

    然而,当我们把 Python 打包后,我们就不能再对 Python 代码使用 spawn 方法。取而代之的是,我们必须使用 execFile 方法来运行 exe 文件。
    Electron 不知道应用是否已经被分发出去了,所以在 mail.js 里,我加上了这段函数:

    // main.js
    
    const PY_DIST_FOLDER = 'pycalcdist'
    const PY_FOLDER = 'pycalc'
    const PY_MODULE = 'api' // without .py suffix
    
    const guessPackaged = () => {
      const fullPath = path.join(__dirname, PY_DIST_FOLDER)
      return require('fs').existsSync(fullPath)
    }
    
    const getScriptPath = () => {
      if (!guessPackaged()) {
        return path.join(__dirname, PY_FOLDER, PY_MODULE + '.py')
      }
      if (process.platform === 'win32') {
        return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE + '.exe')
      }
      return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE)
    }

    然后修改 createPyProc 方法:

    // main.js
    // the improved version
    const createPyProc = () => {
      let script = getScriptPath()
      let port = '' + selectPort()
    
      if (guessPackaged()) {
        pyProc = require('child_process').execFile(script, [port])
      } else {
        pyProc = require('child_process').spawn('python', [script, port])
      }
    
      if (pyProc != null) {
        //console.log(pyProc)
        console.log('child process success on port ' + port)
      }
    }

    关键点是,检查 *dist 文件夹是否被生成。如果生成了,那么说明我们在生产模式下,那么就直接运行 exe 文件;如果没有,我们就需要对 Python 代码用 spawn 方法,在 Python shell 里面运行。

    最后,运行 Electron-Packager 来生成最终的应用。我们需要排除一些文件夹,例如,pycalc/ 已经不需要了。应用的名称,平台等等需要在 package.json 里面配置。请参阅相关文档。

    # we need to make sure we have bundled the latest Python code
    # before running the below command!
    # Or, actually, we could bundle the Python executable later,
    # and copy the output into the correct distributable Electron folder...
    
    ./node_modules/.bin/electron-packager . --overwrite --ignore="pycalc$" --ignore="\.venv" --ignore="old-post-backup"
    ## Packaging app for platform win32 x64 using electron v1.7.6
    ## Wrote new app to ./pretty-calculator-win32-x64

    最后,我们就得到了最终生成的应用!对我来说,结果在 ./pretty-calculator-win32-x64 里面。在我的电脑上,大小是 170MB 左右。我也试着压缩了一下,生成的 .7z 文件是 43.3MB 左右。

    将最后生成的文件移植到其它电脑上看看结果吧!

    支付宝 微信 BTC
    支付宝扫一扫,向我打赏
    来源:Github

    声明:本站原创文章采用 BY-NC-SA 创作共用协议,转载时请以链接形式标明本文地址;非原创(转载)文章版权归原作者所有。 ©查看版权声明

  • 白銀の魔法師
  • 所有的信徒都别无二致,所有的信仰都一文不值
  • 发表评论

    你目前的身份是游客,请输入昵称和邮箱! 输入资料 关闭

    3条评论

    1. cc

      最近也打算写个 python+electron的应用 多谢 参考下

      1. 醉花猫

        比你晚一个月,希望能一起交流

        cc2018-07-02

        最近也打算写个 python+electron的应用 多谢 参考下

        1. 如果遇到问题可以一起交流,我在使用过程中已经踩了很多坑了,但是由于时间问题不可能详细的写出来。

          醉花猫2018-08-04

          比你晚一个月,希望能一起交流