# electron + flask server 开发/打包项目示例
## 开发环境
**python3.6.8**
**node v12.22.5**
**win7**
## 初始化项目
```
mkdir python-electron-app
cd python-electron-app
npm init -y
```
初始化后,在项目python-electron-app中,生成的pacakge.json大致如下
```json
{
"name": "python-electron-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
```
## 开发 flask server
* (建议) 创建python虚拟环境进行管理
```cmd
python -m venv venv
```
```cmd
venv\Scripts\activate.bat
```
* 安装本相关python模块
```cmd
pip install flask==2.0.1
pip install flask-cors==3.0.10
pip install simplecalculator==0.0.4
pip install dataclasses==0.6
pip install pyinstaller==4.5.1
```
* 在项目根目录下,创建py文件夹,后续在py文件夹下进行python代码开发
* 创建flask app.py 内容如下:
```python
from calculator.simple import SimpleCalculator
from flask import Flask, render_template
from flask_cors import cross_origin
app = Flask(__name__)
def calcOp(text):
"""based on the input text, return the operation result"""
try:
c = SimpleCalculator()
c.run(text)
return c.log[-1]
except Exception as e:
print(e)
return 0.0
@app.route('/')
def homepage():
home = 'flask_welcome.html'
return render_template(home)
@app.route("/<input>")
@cross_origin()
def calc(input):
return calcOp(input)
if __name__ == "__main__":
app.run(host='127.0.0.1', port=5001, use_reloader=False)
# 注意,如果没有指定use_reloader=False,后续将其打包成exe后,运行exe会产生两个进程,在electron窗口关闭时kill掉进程时,会有一个守护进程无法kill掉
```
* 创建flask template 如下:
templates/flask_welcome.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Welcome to Flask</h1>
<h3>This is a homepage rendered by flask.</h3>
</body>
</html>
```
* 测试 flask server 运行情况
```cmd
python py/app.py
```
访问 127.0.0.1:5001 后,正常情况会返回flask_welcome.html中的内容;
访问 127.0.0.1:5001/1 + 1 ,正常情况会返回calc视图的响应内容:result:2.0
## 安装局部electron
`cnpm install --save-dev electron@14.0.1 -S`
本版本安装的electron版本为“14.0.1”
## 创建electron主入口
主入口由package.json 中的main指定,如本项目,主入口为`index.js`
* 在项目根目录下创建index.js 如下
```js
// 引入nodejs模块
const {app, BrowserWindow} = require('electron');
const path = require('path');
// 创建窗口函数
function createWindow() {
win = new BrowserWindow({ // 设置窗口option
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false, // 注意如果没有该选项,在renderer.js 中 require is not defined
enableRemoteModule: true
}
});
win.loadFile('index.html');// 窗口加载本地html
win.webContents.openDevTools(); // 打开开发者工具调试选项
}
// 启动flask server,通过python-shell 调用python脚本(开发调试阶段)
function startServer_PY() {
var {PythonShell} = require('python-shell');
let options = {
mode: 'text',
pythonPath: 'venv/Scripts/python'
};
PythonShell.run('./py/app.py', options, function (err, results) {
if (err) throw err;
// results is an array consisting of messages collected during execution
console.log('response: ', results);
});
}
// 初始化函数
function initApp() {
startServer_PY();
createWindow();
}
// electron ready 事件触发
app.on('ready', initApp);
// electron 窗口关闭事件触发
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
});
```
* 安装python-shell 模块
`cnpm install python-shell -S`
* 在项目根目录下创建index.html 如下
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Calling Python from Electron!</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';connect-src *">
</head>
<body>
<h1>Simple Python Calculator!</h1>
<p>Input something like <code>1 + 1</code>.</p>
<input id="input" value="1 + 1"></input>
<input id="btn" type="button" value="Send to Python!"></input>
</br>
Got <span id="result"></span>
<a href="http://127.0.0.1:5001/">go flask template</a>
<script src="./renderer.js"></script>
</body>
</html>
```
* 创建渲染进程renderer.js 如下
```js
let input = document.querySelector('#input');
let result = document.querySelector('#result');
let btn = document.querySelector('#btn');
function onclick() {
// 发送http请求
fetch(`http://127.0.0.1:5001/${input.value}`).then((data) => {
return data.text();
}).then((text) => {
console.log("data: ", text);
result.textContent = text;
}).catch(e => {
console.log(e);
})
}
// 添加按钮点击事件
btn.addEventListener('click', () => {
onclick();
});
```
## 运行electron
在package.json 的scripts中添加运行命令如下
`"start": "electron ."`
在项目根目录下,执行命令运行
`npm run start`
运行后,正确情况是打开electron窗口:
点击按钮"Send to Python"后,会正确发送请求到flask中并获得响应;
点击链接“go flask template” 则会跳转到flask_welcome.html中。
## flask 打包成exe
* 在package.json 中的scripts 里面添加python打包脚本
` "build-python": "pyinstaller -D -p ./venv/Lib/site-packages py/app.py --add-data=py/templates;templates --distpath ./pydist",`
-p 指定依赖包位置,如果没有指定,打包后会缺少响应的依赖模块
--add-data 指定外部资源位置,如果没有指定,运行后会找不到flask template 资源
* 运行打包脚本
`npm run build-python`
打包完成后,会生成可执行文件 pydist/app/app.exe,可运行该exe检查flask是否正确运行
## 调整electron index.js
调整前,是使用python-shell调用app.py 脚本来启动flask。当flask打包成exe后,需调整启动flask位置的命令
调整后如下
```js
// 引入nodejs模块
const {app, BrowserWindow} = require('electron');
const path = require('path');
// 创建窗口函数
function createWindow() {
win = new BrowserWindow({ // 设置窗口option
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false, // 注意如果没有该选项,在renderer.js 中 require is not defined
enableRemoteModule: true
}
});
win.loadFile('index.html');// 窗口加载本地html
win.webContents.openDevTools(); // 打开开发者工具调试选项
}
// 启动flask server,通过python-shell 调用python脚本(开发调试阶段)
function startServer_PY() {
var {PythonShell} = require('python-shell');
let options = {
mode: 'text',
pythonPath: 'venv/Scripts/python'
};
PythonShell.run('./py/app.py', options, function (err, results) {
if (err) throw err;
// results is an array consisting of messages collected during execution
console.log('response: ', results);
});
}
// 启动flask server,通过子进程执行已经将python项目打包好的exe文件(打包阶段)
function startServer_EXE() {
let script = path.join(__dirname, 'pydist', 'app', 'app.exe')
pyProc = require('child_process').execFile(script)
if (pyProc != null) {
console.log('flask server start success')
}
}
// 停止flask server 函数
function stopServer() {
pyProc.kill()
console.log('kill flask server success')
pyProc = null
}
// 初始化函数
function initApp() {
startServer_EXE();
createWindow();
}
// electron ready 事件触发
app.on('ready', initApp);
// electron 窗口关闭事件触发
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
stopServer()
});
```
* 重新运行electron进行调试
`npm run start`
## electron 打包
* 安装electron打包模块
`cnpm install --save-dev electron-packager@15.4.0 -S`
* 在package.json 的scripts中添加打包命令
`"pack-app": "electron-packager . --overwrite --ignore=py$ --arch=x64 --download.mirrorOptions.mirror=https://npm.taobao.org/mirrors/electron/"`
注意,如果没有指定--download.mirrorOptions.mirror,下载对应系统的electron-xxx.zip 包会耗时非常长!!!
* 运行electron 打包命令
`npm run pack-app`
## electron-winstaller 打包成一个可执行exe
** (1)安装electron-winstaller、electron-squirrel-startup
`npm install electron-winstaller@5.0.0 --save-dev`
`npm install electron-squirrel-startup --save`
** (2)在根目录下新建打包需要的一个build.js文件
```js
var electronInstaller = require('electron-winstaller');
var path = require("path");
resultPromise = electronInstaller.createWindowsInstaller({
appDirectory: path.join('./python-electron-app-win32-x64'), //入口,electron-package生成的文件目录
outputDirectory: path.join('./installer64'), //出口,electron-winstaller生成的文件目录
authors: 'Milton',
exe: "python-electron-app.exe", //在appDirectory 目录下已生成的exe
setupIcon: "./icon.ico",//安装图标,必须本地
// iconUrl: 'http://pm72qibzx.bkt.clouddn.com/icon.ico',//程序图标,必须url
noMsi: true,
setupExe:'python-electron-app.exe',
title:'python-electron-app',
description: "python-electron-app"
});
resultPromise.then(() => console.log("It worked!"), (e) => console.log(`No dice: ${e.message}`));
```
** (3) 执行打包命令
`node build.js`
打包完成后,会在installer64目录下生成一个可执行exe文件。
## electron-builder 打包成可安装文件
** (1) 安装electron-builder
`npm install electron-builder@22.9.1 --save-dev`
** (2) 在package.json 中添加 build配置
```js
"build": {
"appId": "com.guanfc.app",
"productName": "demo",
"win": {
"icon": "chrome.png",
"target": [
"nsis"
],
"arch": [
"ia32"
]
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
```
** (3) 在package.json 的Scripts中添加打包命令
`"build": "electron-builder"`
执行打包命令 `npm run build` ,打包过程要下载资源,大概率要翻墙才可以成功,OMG!