用 Electron 作為 Python 的 GUI 界面
該文章原文為《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 .
ode_modules -Force -Recurse -ErrorAction Ignore
(譯者註:如果在 Windows 中執行上述命令,提示 Ignore 不是有效的枚舉值,可以將該值更改為 SilentlyContinue 後執行。因為 Ignore 是在 Powershell 3.0 加入的。參考微軟官方文檔)
然後運行npm:
# 1.7.6 is the version of electron
# Its 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-toolspython27 中安裝一個獨立的 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 左右。
將最後生成的文件移植到其它電腦上看看結果吧!
Swiity
推薦閱讀: