为了实现之后的一个大项目,测试了一下 NodeJS 和 Python 的通信方式,采用 PythonShell 现实。
参考文章:
How to Execute Python Scripts in Electron and NodeJS
python-shell node, keep a python script running
描述如下:
通过 Electron 运行一个 HTML 页面,上面有两个按钮,分别是 Sit 和 Roll Over;
在 Python 中定义一个 Dog 类,定了两个方法,sit 和 roll_over,其中 roll_over 可接受一个参数,让小狗打滚几次;动作结束之后将结果通知给 HTML 页面;
先来简单的 Python 端:
dog.py
class Dog: def __init__(self, name, age): self.name = name self.age = age def sit(self): print(f'{self.name.title()} is now sitting.') def roll_over(self, count): print(f'{self.name.title()} rolled over {count} times!')
main.py
import json from random import randint from dog import Dog def main(): rnd = randint(1, 6) my_dog = Dog('Youyou' + str(rnd), 3) while True: inp = input(' ') data = json.loads(inp) command = data['command'] if command == 'do-sit': my_dog.sit() elif command == 'do-roll-over': my_dog.roll_over(data['value']) else: print('??') if __name__ == '__main__': try: main() except KeyboardInterrupt: print('error')
这里有一个问题要说明一下:对于我这种简单的需求,完全不必要把 Python 程序弄这么复杂啊?!完全可以这么写
import sys, json from dog import Dog my_dog = Dog('Youyou', 3) data = sys.stdin.readlines() data = json.loads(data[0]) if data['command'] == 'do-sit': my_dog.sit() elif data['command'] == 'do-roll-over': my_dog.roll_over(data['value'])
之所以没有这么去弄,是要为之后的项目考虑,这个 Python 程序不可能每次都执行,结束,执行,结束……;假设这个 Python 需要访问网络获取一些前期数据(或者和其他设备联通),那么每次都得创建连接;所以采用了上面复杂的写法,其中每次运行 Python 程序,都在小狗名字后面加了一个随机数,来模拟一次性。
OK,那么 Python 的工作做完了,接下来是比较复杂的部分 Electron:
main.js
const { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') const { PythonShell } = require('python-shell') let mainWindow, pyshell const createWindow = () => { mainWindow = new BrowserWindow({ width: 1024, height: 768, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) pyshell = new PythonShell('main.py') pyshell.on('message', (message) => { console.log(message) // 通过 preload.js 将结果发送给 renderer.js mainWindow.webContents.send('to-renderer', message) }) // 通过 preload.js 监听来自 renderer.js 的消息 ipcMain.on('from-renderer', (event, data) => { // 上面的 event 一定要有,因为这是一个 IpcMainEvent pyshell.send(JSON.stringify(data)) }) mainWindow.loadFile('index.html') mainWindow.webContents.openDevTools() } app.whenReady().then(() => { createWindow() app.on('activate', () => { if(BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { if(process.platform !== 'darwin') app.quit() })
preload.js
const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { fromRenderer: (data) => { ipcRenderer.send('from-renderer', data) }, toRenderer: (callback) => { ipcRenderer.on('to-renderer', callback) } })
renderer.js
document.addEventListener('DOMContentLoaded', () => { console.log('Ready') if(window.electronAPI) { window.electronAPI.toRenderer((event, value) => { console.log(value) }) } const fooBtn = document.getElementById('btn-sit') fooBtn.addEventListener('click', () => { if(window.electronAPI) { window.electronAPI.fromRenderer({ command: 'do-sit' }) } }) const barBtn = document.getElementById('btn-roll-over') barBtn.addEventListener('click', () => { if(window.electronAPI) { window.electronAPI.fromRenderer({ command: 'do-roll-over', value: 2 }) } }) })
Electron 里面注意的问题比较多,首先要通过 npm 安装 elecron 和 python-shell
npm i electron npm i python-shell
如果安装 electron 碰到问题,请参考官方文章或参考我之前的文章;安装 python-shell 应该没有任何问题。
其次,推荐在 main.js 中运行 PythonShell;当然你也可以在 renderer.js 中通过 require 引入 PythonShell 来运行,但是官方不推荐(参考文章),还是建议通过 preload.js 来实现。
至此这个简单的 Demo 就实现了,WOW