目录

前言 1.1

Electron简介 1.2

应用举例 1.2.1

安装 1.3

基本使用 1.4

打包和部署 1.5

python支持 1.6

心得 1.7

开发 1.7.1

Web技术 1.7.1.1

Log日志 1.7.1.2

打包 1.7.2

制作app的Logo 1.7.2.1

asar 1.7.2.2

files 1.7.2.3

electron-builder 1.7.2.4

electron-rebuild 1.7.2.5

不同系统 1.7.3

Win 1.7.3.1

Mac 1.7.3.2

不同语言 1.7.4

Python 1.7.4.1

electron-python-example 1.7.4.1.1

js 1.7.4.2

Node 1.7.5

electron和node版本对应关系 1.7.5.1

NODE_MODULE_VERSION和node版本对应关系 1.7.5.2

附录 1.8

文档和教程 1.8.1

参考资料 1.8.2

前言

跨平台桌面应用框架：Electron

最新版本：v1.0

更新时间：20200808

简介

介绍跨平台桌面应用框架Electron的基本概念、基本原理、核心优势以及额外特性；介绍了用Electron开发出的常见应用有哪些；解释了如何安装Electron；以及如何快速上手使用；以及如何打包和部署；专门整理了如何让Electron支持Python以及其后的相关心得；总结了Electron的各种开发经验和心得，比如开发方面的Web技术、Log日志等；打包方面的如何制作app的logo、asar加密压缩、files参数、打包工具electron-builder、编译工具electron-rebuild等，以及常见的系统如Windows和Mac系统中常见问题和解决方案；以及不同语言，比如Python、js等的支持，整理了Node相关信息，最后附上相关文档和教程等资料。

源码+浏览+下载

本书的各种源码、在线浏览地址、多种格式文件下载如下：

Gitbook源码

crifan/desktop_app_framework_electron: 跨平台桌面应用框架：Electron

如何使用此Gitbook源码去生成发布为电子书

详见：crifan/gitbook_template: demo how to use crifan gitbook template and demo

在线浏览

跨平台桌面应用框架：Electron book.crifan.com

跨平台桌面应用框架：Electron crifan.github.io

离线下载阅读

跨平台桌面应用框架：Electron PDF

跨平台桌面应用框架：Electron ePub

跨平台桌面应用框架：Electron Mobi

版权说明

此电子书教程的全部内容，如无特别说明，均为本人原创和整理。其中部分内容参考自网络，均已备注了出处。如有发现侵犯您版权，请通过邮箱联系我 admin 艾特 crifan.com，我会尽快删除。谢谢合作。

鸣谢

感谢我的老婆陈雪的包容理解和悉心照料，才使得我crifan有更多精力去专注技术专研和整理归纳出这些电子书和技术教程，特此鸣谢。

更多其他电子书

本人crifan还写了其他100+本电子书教程，感兴趣可移步至：

crifan/crifan_ebook_readme: Crifan的电子书的使用说明

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-01-17 00:08:40

Electron简介

Electron简介

Electron概述

所属技术领域：

跨平台的桌面端应用开发

谁开发的：Github

旧称：Atom Shell

历史

2013年作为构建Github上可编程的文本编辑器Atom的框架而被开发出来

2014春季这两个项目开源出来

一句话描述：一个用纯Web技术来构建跨平台桌面应用程序的开源框架

Web技术：HTML、CSS和JavaScript

对比：传统桌面应用都是非Web技术开发的

跨平台：Win/Mac/Linux等多个平台

桌面应用：主要用来开发桌面端应用

而不是Web应用

基本原理

将Chromium和Node.js合并到同一个运行时环境中

让你使用纯JavaScript调用丰富的原生(操作系统) APIs

并将其打包为Mac、Windows和Linux系统下的应用

架构

后端：挣钱养家 的 Node.js

负责：实现业务逻辑

注：Node.js是Javascript的一个Web Server

前端：貌美如花 的 Chromium

负责：显示内容

注：可以看成Chromium是Chrome的浏览器的内核实现

可把Electron看成

一个被 JavaScript 控制的，精简版的 Chromium 浏览器

一个 Node. js 的变体

专注于桌面应用而不是 Web Server 端

使用 web 页面作为它的 GUI

主页

Electron | 使用 JavaScript，HTML 和 CSS 构建跨平台的桌面应用程序

竞品

nw.js

现状

已成为开源开发者、初创企业和老牌公司常用的开发工具

详见后续章节：应用举例

优势

颜值高=界面美观

原因：Web技术（HTML+CSS+JS）天生适合页面信息展示

截图

对比其他技术：很多是基于各种图形库开发出的桌面应用，很多颜值一般

界面统一

Win/Mac/Linux中显示效果几乎完全一样

典型例子：VSCode

对比其他技术：界面效果多数不太一样

开发难度低 = 入门快 = 上手方便

= 像开发网站一样开发（跨平台桌面）应用

比传统方式要简单很多

说明

传统方式：只支持某个平台，特定语言和框架，才能开发出该平台中的桌面端应用

举例：

Win

IDE：Visual Studio + 语言：C# + 框架：WinForm/WPF

截图

Mac

IDE：XCode + 语言：Objective-C/Swift + 框架：Cocoa

截图

额外特性

自动更新

支持平台

不支持Linux

支持Mac和Win

都是基于Squirrel去实现的

原生的菜单和通知

崩溃报告

调试和性能分析

Windows 安装程序

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

应用举例

应用举例

目前已经有众多很出名的跨平台桌面应用都是用Electron开发的。

举例如下：

Atom

GitHub 用 Electron 开发了 Atom

截图

Win

Mac

Visual Studio Code

微软 用 Electron 开发了 Visual Studio Code

截图

Win

Mac

WhatsApp 桌面版

截图

Win

MongoDB Compass

MongoDB 用Electron开发了 MongoDB Compass

截图

Win

Mac

迅雷X

发布于2018年的迅雷X

InVision Studio

截图

Slack

Slack 用 Electron 开发了 Slack

Microsoft Teams

Twitch

更多例子详见官网

看看谁在使用Electron

https://electronjs.org/apps

https://github.com/electron/apps/tree/master/apps

单独举例：用Electron可以开发出足够复杂的应用

之前某人用Electron开发出一款足够复杂的应用。举例如下，供了解：

多皮肤实时切换：

集装箱船 任意放大缩小 拖动：

html5 网页版 安卓版：

增加了Web 3D显示，放大缩小旋转拾取：

主界面功能布局做了调整：

窗口折叠：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

安装

Electron安装

初始化环境

用Electron之前，需要确保本地环境已安装好npm和node

确保已安装了Node.js：

node -v

如果没有安装，则先去安装：

brew install node

确保已安装了npm：

npm -v

安装Electron

npm i -D electron@latest

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

基本使用

Electron基本使用

官网入门教程

官网已有很好的入门教程：

electron/electron-quick-start: Clone to try a simple Electron app

跟着教程一步步操作即可。

核心步骤：

# 克隆仓库
$ git clone https://github.com/electron/electron-quick-start
# 进入仓库
$ cd electron-quick-start
# 安装依赖库
$ npm install
# 运行应用
$ npm start

基本效果：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

打包和部署

Electron打包和部署

用Electron开发完桌面应用后，接着需要把代码打包在一起发布出来，叫做：打包 或 部署 或 发布

常见打包packaging工具

目前主要有3种可以用来打包Electron应用的工具：

electron-forge

https://github.com/electron-userland/electron-forge

electron-builder

官网

electron-builder

GitHub

electron-userland/electron-builder: A complete solution to package and build a ready for distribution Electron app with 'auto update' support out of the box

文档

不同部分，分页显示的

比如：

PlatformSpecificBuildOptions - electron-builder

全部文档在一起的，单页显示的

Common Configuration - electron-builder

electron-packager

electron/electron-packager: Customize and package your Electron app with OS-specific bundles (.app, .exe, etc.) via JS or CLI

更多内容详见官网：

应用程序打包 | Electron

应用部署 | Electron

用electron-builder打包

下面举例用electron-builder去打包Electron的应用。

Mac中用electron-builder打包

此处介绍如何在Mac中用electron-builder去打包Electron的应用，生成Mac中的app和pkg文件。

安装electron-builder

npm install -D electron-builder@21.2.0

添加build配置

以及给package.json加上必要的build的配置，主要增加的部分是：

{
  "scripts": {
    "pack": "electron-builder --dir",
    "dist": "electron-builder",
    "dist_all": "electron-builder -mw"
  },

  "build": {
    "productName": "ElectronDemo",
    "appId": "com.crifan.electronDemo",
    "copyright": "Copyright © 2020 ${author} String",
    "directories": {
      "output": "release"
    },
    "mac": {
      "category": "public.app-category.developer-tools",
      "type": "distribution",
      "target": [
        "pkg",
        "zip"
      ]
    }
  },

}

更新后package.json完整配置如下：

{
  "name": "electron-quick-start",
  "version": "1.0.0",
  "description": "A minimal Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "pack": "electron-builder --dir",
    "dist": "electron-builder",
    "dist_all": "electron-builder -mw"
  },
  "repository": "https://github.com/electron/electron-quick-start",
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "Crifan Li",
  "license": "CC0-1.0",
  "devDependencies": {
    "electron": "^9.0.0",
    "electron-builder": "^21.2.0"
  },
  "build": {
    "productName": "ElectronDemo",
    "appId": "com.crifan.electronDemo",
    "copyright": "Copyright © 2020 ${author} String",
    "directories": {
      "output": "release"
    },
    "mac": {
      "category": "public.app-category.developer-tools",
      "type": "distribution",
      "target": [
        "pkg",
        "zip"
      ]
    }
  },
  "dependencies": {}
}

打包

然后去打包

npm run dist

打包后输出文件：

 ~/dev/src/electron/electron-quick-start/release   master ●  tree -L 2 .
.
├── ElectronDemo-1.0.0-mac.zip
├── ElectronDemo-1.0.0.pkg
├── builder-effective-config.yaml
├── com.crifan.electronDemo.plist
├── latest-mac.yml
└── mac
    └── ElectronDemo.app

2 directories, 5 files

其中：

ElectronDemo-1.0.0-mac.zip

解压得到ElectronDemo.app

双击即可运行

效果截图

ElectronDemo-1.0.0.pkg

双击即可安装

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

python支持

Electron对python支持

Electron本身官网并没有支持Python。

不过网上有人给出了可行的方案：

英文

fyears/electron-python-example: Electron as GUI of Python Applications

中文翻译

用 Electron 作为 Python 的 GUI 界面 - 知乎

下面介绍环境搭建的详细过程：

搭建能运行Python的Electron环境

下载代码

git clone https://github.com/fyears/electron-python-example.git

得到文件：

 tree -CF
.
├── electron-python-example/
│   ├── LICENSE
│   ├── README.md
│   ├── index.html
│   ├── main.js
│   ├── old-post-backup/
│   │   ├── README.md
│   │   ├── hello.py
│   │   ├── main.js
│   │   └── package.json
│   ├── package.json
│   ├── pycalc/
│   │   ├── api.py
│   │   ├── calc.py
│   │   └── requirements.txt
│   └── renderer.js
└── electron-quick-start/
    ├── LICENSE.md
    ├── README.md
    ├── index.html
    ├── main.js
    ├── package-lock.json
    ├── package.json
    ├── preload.js
    └── renderer.js

4 directories, 21 files

用VSCode打开，效果是：

初始化node环境

node要安装版本v8

之前：

brew install node

安装出的node是13.5.0，会导致后续编译electron有问题，所以需要换8.x版本的node：

brew install node@8

安装后使其生效：

把路径加到启动脚本的PATH中：

echo 'export PATH="/usr/local/opt/node@8/bin:$PATH"' >> ~/.zshrc

立刻生效：

source ~/.zshrc

安装后的版本是：

node：8.17.0

npm: 6.13.4

根目录electron-python-example中：

创建文件.npmrc，内容为：

npm_config_target="2.0.18" # electron version
npm_config_runtime="electron" # 为elelctron编译
npm_config_disturl="https://atom.io/download/electron" # 资源下载地址
npm_config_build_from_source="true" # 从源码编译

以及配置文件：electron-python-example/package.json

{
  "name": "pretty-calculator",
  "version": "1.0.0",
  "description": "A minimal Electron and Python - based calculator ",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "https://github.com/fyears/electron-python-example",
  "keywords": [
    "Electron",
    "Python",
    "zerorpc",
    "demo"
  ],
  "author": "fyears",
  "license": "MIT",
  "dependencies": {
    "zeromq": "^6.0.0-beta.6",
    "zerorpc": "^0.9.8"
  },
  "devDependencies": {
    "electron": "^2.0.18",
    "electron-builder": "^21.2.0",
    "electron-rebuild": "^1.8.8"
  }
}

再去安装：

npm install

初始化Python开发环境

虚拟环境

此处Python的虚拟环境选择用：virtualenv

安装：

pip install virtualenv

新建：

virtualenv venv

进入：

source venv/bin/activate

安装依赖的库

安装负责js和python之间的通信的zerorpc：

pip install zerorpc

启动Electron

每次启动前最好清除缓存

根据作者解释，每次启动之前，最好去清除一下各种缓存，防止干扰：

rm -rf ~/.node-gyp
rm -rf ~/.electron-gyp
rm -rf ./node_modules
rm -f package-lock.json

启动electron

./node_modules/.bin/electron

界面效果：

测试zerorpc通信

再去测试zerorpc通信是否正常，Python代码是否可用：

打开某个终端去：

python pycalc/api.py

正常输出是：

start running on tcp://127.0.0.1:4242

再去另外一个终端：

virtualenv venv
source venv/bin/activate

zerorpc tcp://localhost:4242 calc "1 + 1"

正常输出：

connecting to "tcp://localhost:4242"
2.0

即表示zerorpc通信正常，js可以和Python代码交互了。

然后再去启动electron：

./node_modules/.bin/electron .

即可看到计算器的界面：

至此即在mac中跑通此处的electron-python-example了。

剩下的就是自己实现自己需要的逻辑和功能了。

打包和发布

在实现了自己的业务逻辑和功能后，再去打包和发布。

python的打包

先用PyInstaller打包：

cd electron-python-example

rm -rf build
rm -rf *.spec
rm -rf pymitmdumpstartdist
rm -rf pymitmdumpotherdist

pyinstaller pymitmdump/mitmdumpStartApi.py --distpath pymitmdumpstartdist --add-data "pymitmdump/mitmdump_executable:mitmdump_executable" --add-data "pymitmdump/mitmdumpUrlSaver.py:."

pyinstaller pymitmdump/mitmdumpOtherApi.py --distpath pymitmdumpotherdist

此处会生成的2个dist目录：

pymitmdumpstartdist

二进制文件：electron-python-example/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi

以及我们拷贝的：

整个目录：electron-python-example/pymitmdumpstartdist/mitmdumpStartApi/mitmdump_executable

单个py文件：electron-python-example/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpUrlSaver.py

pymitmdumpotherdist

二进制文件：electron-python-example/pymitmdumpotherdist/mitmdumpOtherApi/mitmdumpOtherApi

electron的打包

再去用electron-builder打包：

rm -rf builder_output

npm run dist

生成：

app：electron-python-example/builder_output/mac/mitmdumpUrlSaver.app

app的zip压缩包：electron-python-example/builder_output/mitmdumpUrlSaver-1.0.0-mac.zip

pkg：electron-python-example/builder_output/mitmdumpUrlSaver-1.0.0.pkg

注：Mac中安装后，但在应用程序中找不到对应的app。暂未深究，原因未知，肯能和code sign有关系。

运行

双击打包出来的app即可运行：

其他说明

package.json最新完整配置及说明

此处最后的package.json配置为：

{
  "name": "MitmdumpUrlSaver",
  "version": "1.0.0",
  "description": "Use mitmdump proxy to save out url",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "pack": "electron-builder --dir",
    "dist": "electron-builder",
    "dist_all": "electron-builder -mw"
  },
  "repository": "https://github.com/fyears/electron-python-example",
  "keywords": [
    "Electron",
    "Python",
    "zerorpc",
    "mitmdump"
  ],
  "author": "Crifan Li",
  "license": "MIT",
  "dependencies": {
    "electron-context-menu": "^0.15.2",
    "zeromq": "^6.0.0-beta.6",
    "zerorpc": "^0.9.8"
  },
  "devDependencies": {
    "electron": "^2.0.18",
    "electron-builder": "^21.2.0",
    "electron-rebuild": "^1.8.8"
  },
  "build": {
    "productName": "mitmdumpUrlSaver",
    "appId": "com.crifan.mitmdumpurlsaver",
    "copyright": "Copyright © 2020 ${author} String",
    "directories": {
      "output": "builder_output"
    },
    "files": [
      "**/*",
      "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
      "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
      "!**/node_modules/*.d.ts",
      "!**/node_modules/.bin",
      "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
      "!.editorconfig",
      "!**/._*",
      "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
      "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
      "!**/{appveyor.yml,.travis.yml,circle.yml}",
      "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}",
      "!build",
      "!pymitmdump",
      "!pycalc",
      "!old-post-backup",
      "!*.log",
      "!*.txt",
      "!*.md",
      "!*.spec"
    ],
    "asar": false,
    "mac": {
      "category": "public.app-category.developer-tools",
      "type": "distribution",
      "icon": "assets/icon/icon.icns",
      "target": [
        "pkg",
        "zip"
      ]
    },
    "win": {
      "target": "nsis",
      "icon": "assets/icon/logo.png"
    }
  }
}

(1) file

其中

"**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
"!.editorconfig",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}",

是从官网 Application Contents - electron-builder 拷贝出来的默认的配置；

余下部分：

      "!build",
      "!pymitmdump",
      "!pycalc",
      "!old-post-backup",
      "!*.log",
      "!*.txt",
      "!*.md",
      "!*.spec"

是自己加的，用于排除此处不需要打包的文件。

(2) asar

asar参数默认是开启的，其会导致此处二进制可执行文件：

mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi

无法运行

mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpotherdist/mitmdumpOtherApi/mitmdumpOtherApi

可以运行

根本原因：未知

规避办法：只能去关闭掉asar：

"asar": false,

才能正常运行mitmdumpStartApi和mitmdumpOtherApi

更多细节

详见：

【已解决】Mac中运行跑通electron-python-example的示例

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

心得

Electron心得

此处整理Electron开发期间的一些心得。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

开发

开发

此处整理关于Electron开发期间的一些心得。

Uncaught ReferenceError process is not defined

问题：文件：electron-python-example/index.html 的代码：

  <script>
    require('./renderer.js')
  </script>

报错：Uncaught ReferenceError process is not defined

原因：Electron 5.0之后，把之前默认是开启的nodeIntegration关闭了，导致找不到进程而报错

解决办法：加上参数去开启：nodeIntegration:true

文件：electron_python/electron-python-example/main.js

const createWindow = () => {
  mainWindow = new BrowserWindow(
    {
      width: 800,
      height: 600,
      webPreferences: {
        nodeIntegration: true,
      }
    }
  )

textarea右键复制选中内容

背景：希望textarea区域可以右键显示按钮，带复制选项，用于复制所选内容

解决办法：

安装插件 sindresorhus/electron-context-menu: Context menu for your Electron app，即可实现此功能。

步骤：

npm install electron-context-menu

使用：

const contextMenu = require('electron-context-menu');
contextMenu({
    prepend: (defaultActions, params, browserWindow) => [
        {
            label: 'Rainbow',
            // Only show it when right-clicking images
            visible: params.mediaType === 'image'
        },
        {
            label: 'Search Google for "{selection}"',
            // Only show it when right-clicking text
            visible: params.selectionText.trim().length > 0,
            click: () => {
                shell.openExternal(`https://google.com/search?q=${encodeURIComponent(params.selectionText)}`);
            }
        }
    ]
});

Electron中的textarea，即可支持右键，出现菜单，选择文字后，即可复制：

进一步的需求：禁止右键中除了Copy外其他按钮菜单

去加上配置：

const contextMenu = require('electron-context-menu');
contextMenu({
  showLookUpSelection: false,
  showCopyImage: false,
  showInspectElement: false,
});

效果：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Web技术

Web技术

由于Electron的开发，关于内容显示方面，来说就是Web技术的开发。所以常会涉及到一些Web技术的使用。现整理部分如下：

html的文本区域界面用于显示js中获取到的字符串

文件：electron-python-example/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Mitmdump代理保存URL</title>

    <style>

      h1 {
        text-align: center;
      }

      #mitm {
        text-align: center;
      }

      #startSaver {
        font-size: 14px;
        height: 40px;
        background-color: mediumspringgreen;
        border-radius: 6px;
      }

      #output {
          margin-top: 20px;

          background-color: powderblue;

          border: 1px solid #ccc;
          padding: 10px;
          /* width: 400px; */
          /* min-width: 800px; */
          width: 500px;
          min-height: 300px;
          /* height: auto; */
          /* 可以设置一个最大高度，超出时滚动，否则，高度会被撑开 */
          /* max-height: 300px; */
          overflow: auto;
      }
    </style>

  </head>

  <body>
    <h1>Mitmdump代理保存URL</h1>
    <ul>
      <li>Node.js: <script>document.write(process.versions.node)</script></li>
      <li>Chromium: <script>document.write(process.versions.chrome)</script></li>
      <li>Electron: <script>document.write(process.versions.electron)</script></li>
    </ul>

    <div id="mitm">
      <button id="startSaver">启动代理</button>
      <div>
        <!-- <textarea readonly id="output"  rows="100" cols="50"></textarea> -->
        <textarea readonly id="output"></textarea>
      </div>
    </div>

  </body>

  <script>
    require('./renderer.js')
    // require('electron_python/electron-python-example/renderer.js')
  </script>
</html>

文件：electron-python-example/renderer.js

const zerorpc = require("zerorpc")
// let client = new zerorpc.Client()
const constLargeEnoughHeartbeat = 60 * 60 * 24 * 30 * 12 // 1 Year
clientOptions = {
  "heartbeatInterval": constLargeEnoughHeartbeat,
}
let client = new zerorpc.Client(clientOptions)

client.connect("tcp://127.0.0.1:4242")

client.invoke("echo", "server ready", (error, res) => {
  if(error || res !== 'server ready') {
    console.error(error)
  } else {
    console.log("server is ready")
  }
})

let startElement = document.querySelector('#startSaver')
let outputElement = document.querySelector('#output')

startElement.addEventListener('click', () => {
  console.log("startElement clicked")
  client.invoke("startSaver", (error, res, more) => {
    if(error) {
      console.error("invoke startSaver: error=%s, more=%s", error, more)
    } else {
      console.log("invoke startSaver: res=%s, more=%s", res, more)
      outputElement.textContent += "\n" + res
      // outputElement.textContent += res
      outputElement.scrollTop = outputElement.scrollHeight
    }
  })
})

效果：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Log日志

Log日志

此处整理一些和log日志，print，console.log等相关的内容：

让main.js即js输出log日志文件到当前文件夹

文件：electron-python-example/main.js

const path = require('path')

let logFilename = path.join(__dirname, "main_js.log")
let fs = require('fs');
let util = require('util');

var logFile = fs.createWriteStream(logFilename, { flags: 'a' });
  // Or 'w' to truncate the file every time the process starts.
var logStdout = process.stdout;
console.log = function () {
  logFile.write(util.format.apply(null, arguments) + '\n');
  logStdout.write(util.format.apply(null, arguments) + '\n');
}
console.error = console.log;

实现了之后的console.log：

console.log("main.js: __dirname=%s", __dirname)

可以同时输出到：

console终端

log文件

让main.js中的console.log输出到Electron的devTool中的console的log中

背景：

此处已有：

文件：electron-python-example/main.js

...
const createWindow = () => {
  mainWindow = new BrowserWindow(
    {
      // width: 800,
      // height: 600,
      width: 1024,
      height: 768,
      // transparent: true,
      webPreferences: {
        nodeIntegration: true,
      }
    }
  )
  mainWindow.loadURL(require('url').format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))
  mainWindow.webContents.openDevTools()

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

和 文件：electron-python-example/renderer.js

其中

文件：electron-python-example/index.html

。。。
  <script>
    require('./renderer.js')
  </script>
</html>

可见：index.html中引用了renderer.js

去

./node_modules/.bin/electron .

调试electron时，有2种log：

Electron界面中，右边可以看到html调用的renderer.js输出的log

原因：由于main.js中的mainWindow.webContents.openDevTools()打开了DevTools

调试终端中：可以看到main.js的log

因为是在VSCode的终端中运行的

而mac中打包后的Electron的app中，只能看到 Electron界面中（render.js打印）的log，看不到main.js中的log：

而此处希望，对于production生产模式，即mac中打包electron后得到的app文件 mitmdumpUrlSaver.app在其运行时也可以看到main.js中的log，以便于后续调试，当出错时找到原因。

最终方案：

从界面中启动打包后的app内部的二进制文件，比如我的mitmdumpUrlSaver.app

对应内部的二进制文件

./mitmdumpUrlSaver.app/Contents/MacOS/mitmdumpUrlSaver

即可启动终端，运行此处的APP。终端中可以看到我们要的main.js中的log

进一步：如果想要在当前终端中看到Electron的render.js中的log

则可以启动之前加上环境变量：

export ELECTRON_ENABLE_LOGGING=true
./mitmdumpUrlSaver.app/Contents/MacOS/mitmdumpUrlSaver

注：可以用echo确认变量是否已设置成功

 echo $ELECTRON_ENABLE_LOGGING
true

然后就也能看到render.js中的log了：

相关说明：

此处2个js文件，对应着2个js线程：

main.js -> node.js的主线程

-》main.js中console.log输出到的地方

renderer.js -> 显示界面的渲染线程 = Electron界面所在的线程

-》（调试时）Electron界面开启了DevTools后右边就能看到console.log输出的内容了

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

打包

打包

关于Electron打包方面，总结一些心得和坑。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

制作app的Logo

制作app的Logo

给Electron打包生成可执行文件期间，会涉及到给打包后的app弄个Logo图标。

Mac中可以用工具：

Image2icon

官网

Image2icon - Your Mac. Your Icons

去制作app的logo图标。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

asar

asar

asar简介

是什么：一种压缩文件格式

目的=为何要压缩：

减小文件体积

打包后的应用文件尽量小，不要太大

避免暴露源码

特点

Electron 可以无需解压整个文件，即可从其中读取任意文件内容

如何使用=开启asar

为了使用一个 asar 档案文件代替 app 文件夹，你需要修改这个档案文件的名字为 app.asar，

然后将其放到 Electron 的资源文件夹下，然后 Electron 就会试图读取这个档案文件并从中启动。

asar 目录结构：

Mac

electron/Electron.app/Contents/Resources/
└── app.asar

Window 和 Linux

electron/resources/
└── app.asar

更多细节详见：

应用程序打包 | Electron

asar使用心得

开启asar但不压缩部分文件和目录

有些时候需要实现，虽然整体开启asar，但是不想asar压缩部分内容，则可以用：asarUnpack

举例：

    "asar": true,
    "asarUnpack": [
      "main.js",
      "pymitmdumpstartdist",
      "pymitmdumpotherdist"
    ],

确保预期效果：打包后的mac的app中有了2个dist目录和一个文件：

electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app.asar.unpacked/

pymitmdumpstartdist

pymitmdumpotherdist

main.js

从而确保main.js运行期间去判断对应的PyInstaller打包后的python的二进制文件

xxx/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app.asar/

pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi

pymitmdumpotherdist/mitmdumpOtherApi/mitmdumpOtherApi

都可以被

isPackaged = require('fs').existsSync(fullPath)

识别到，确保后续代码逻辑可以正常运行。

详见：

【已解决】Mac中electron-python用electron-builder打包后app运行失败：Error Lost remote after 10000ms

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

files

files

package.json配置参数中有个files，用来控制要打包的文件。此处整理相关心得。

Application entry file main.js in the .app/Contents/Resources/app.asar does not exist

问题：在改了配置为

    "files": [
      "pymitmdumpstartdist",
      "pymitmdumpotherdist"
    ],

后，打包报错：

⨯ Application entry file "main.js" in the "xxx/mac/mitmdumpUrlSaver.app/Contents/Resources/app.asar" does not exist. Seems like a wrong configuration.

原因：根据官网

files - Application Contents - electron-builder 解释，默认files配置是：

    "files": [
      "**/*",
      "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
      "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
      "!**/node_modules/*.d.ts",
      "!**/node_modules/.bin",
      "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
      "!.editorconfig",
      "!**/._*",
      "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
      "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
      "!**/{appveyor.yml,.travis.yml,circle.yml}",
      "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}",
    ],

其中的 **/* 包含了根目录下的各种核心文件，包括入口文件main.js

而前面的配置：

    "files": [
      "pymitmdumpstartdist",
      "pymitmdumpotherdist"
    ],

表示：去掉其他文件，只包含上述2个文件夹

导致找不到必要的入口文件main.js（等其他文件）

解决办法：把自己的配置 加到 默认配置上：

    "files": [
      "**/*",
      "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
      "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
      "!**/node_modules/*.d.ts",
      "!**/node_modules/.bin",
      "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
      "!.editorconfig",
      "!**/._*",
      "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
      "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
      "!**/{appveyor.yml,.travis.yml,circle.yml}",
      "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}",

      "pymitmdumpstartdist/",
      "pymitmdumpotherdist/"
    ],

即可。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

electron-builder

electron-builder

signing Error Command failed codesign sign

问题：用npm run dist打包报错：

signing         file=release/mac/ElectronDemo.app identityName=Apple Development: xxx xx (xxx) identityHash=xxx provisioningProfile=none
Error: Command failed: codesign --sign xxx —force

原因：code sign方面的错误。

解决办法：暂未深入研究，所以只是去禁止code sign，以规避此问题。

禁止code sign的方法

先去：export CSC_IDENTITY_AUTO_DISCOVERY=false

再继续打包：npm run dist

make-dir.js 86 catch SyntaxError Unexpected token {

问题：打包：

npm run dist

报错：

/Users/limao/dev/src/electron/electron-quick-start/node_modules/electron-builder/node_modules/fs-extra/lib/mkdirs/make-dir.js:86
    } catch {
            ^
SyntaxError: Unexpected token {

原因：之前20200529，用：

npm install -D electron-builder

安装出的是最新版本的electron-builder：

"dependencies": {
  "electron-builder": "^22.6.1"
}

而新版22.6.1有bug。

解决办法：换个别的稍微旧一点的版本，比如20.44.4：

npm uninstall electron-builder
npm install -D electron-builder@20.44.4

Error loading Python lib /private/var/folders .Python dlopen image not found

问题：electron-builder去打包mac的app后运行报错

curPyProc.spawnargs=/var/folders/gt/5868sbcd1jq4rxvryqhy2_1sz8n0s3/T/.com.crifan.mitmdumpurlsaver.bG5MLj,4243
curPyProc.pid=81849
curPyProc stderr data: chunk=[81849] Error loading Python lib '/private/var/folders/gt/5868sbcd1jq4rxvryqhy2_1sz8n0s3/T/.Python': dlopen: dlopen(/private/var/folders/gt/5868sbcd1jq4rxvryqhy2_1sz8n0s3/T/.Python, 10): image not found

curPyProc exit: exitCode=255

原因：此处app中用到了已打包好的.Python，但是却没有被打包进去，所以运行报错找不到。

解决办法：去把缺少的东西打包进来，相关参数：extraFiles。

此处额外的所需要的事 ，项目根目录下的（之前用PyInstaller打包后的）2个dist文件夹：

pymitmdumpotherdist

pymitmdumpstartdist

具体配置是：build中mac的extraFiles

文件：electron-python-example/package.json

  "build": {
...
    "mac": {
...
      "extraFiles": [
        "pymitmdumpstartdist",
        "pymitmdumpotherdist"
      ],
      "target": [ "pkg", "zip"]
    }
  }
}

如此打包后的app，其中就包括了额外的目录：

-》即可正常运行，没了.Python找不到的问题。

打包期间实现拷贝整个目录和其中二进制文件到打包后的特定目录

背景：希望实现把打包前的整个目录electron-python-example/pymitmdump/mitmdump_executable及其中的子文件夹和文件，拷贝到打包后的位置 electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdump_executable，从而希望后续代码能识别到。

解决办法：通过extraResources设置

文件：electron-python-example/package.json

  "build": {
...
    "extraResources": [
      {
        "from": "pymitmdump/mitmdump_executable",
        "to": "app/pymitmdumpstartdist/mitmdumpStartApi/mitmdump_executable",
        "filter": ["**/*"]
      }
    ],

路径中包含中文会无法启动mitmdump代理

问题：Electron打包的app放到别的mac中测试时，路径中有中文，导致无法正常启动mitmdump代理。

原因：路径中包含中文，内部启动mitmdump代理的代码，不支持非中文字符。

解决办法：加上兼容不同编码的字符的路径，即可：

import locale

    curPlatformEncoding = locale.getpreferredencoding()
    logging.info("curPlatformEncoding=%s", curPlatformEncoding)
    # Mac: UTF-8
    # Wind: cp936 ~= GBK ~= GB2312

    curProcess = subprocess.Popen(
        shellCmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
        # encoding="UTF-8",
        encoding=curPlatformEncoding,
        shell=isUseShell,
    )
    logging.debug("curProcess=%s", curProcess)

    while True:
        curLineOutput = curProcess.stdout.readline()

实现了：自动判断当前系统编码

Mac：UTF-8

Win：cp936

=GBK

实现subprocess的readline内部自动解码，而不会出现解码报错问题。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

electron-rebuild

electron-rebuild

build.sh cmake command not found make libzmq Error 127

问题：用electron-rebuild编译报错：build.sh cmake command not found make libzmq Error 127

原因：缺少cmake

解决办法：

brew install cmake

zeromq vendor napi.h error unknown type name napi_callback_scope

问题：用electron-rebuild编译报错：zeromq vendor napi.h error unknown type name napi_callback_scope

原因：未深究

解决办法：无需解决，后续继续：

npm install
./node_modules/.bin/electron-rebuild

是可以正常启动Electron的。

.electron-gyp node v8.h error unterminated conditional directive #ifndef INCLUDEV8_H

问题：

用：

./node_modules/.bin/electron-rebuild

报错：

../binding.cc:1610:23: error: expected '}'
/Users/limao/.electron-gyp/7.1.7/include/node/v8.h:37:14: note: to match this '{'
namespace v8 {
             ^
1 warning and 13 errors generated.
make: *** [Release/obj.target/zmq/binding.o] Error 1
gyp ERR! build error
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/electron_python/electron-python-example/node_modules/node-gyp/lib/build.js:194:23)
gyp ERR! stack     at emitTwo (events.js:126:13)
gyp ERR! stack     at ChildProcess.emit (events.js:214:7)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:198:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command "/usr/local/Cellar/node@8/8.17.0/bin/node" "/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/electron_python/electron-python-example/node_modules/.bin/node-gyp" "rebuild" "--target=7.1.7" "--arch=x64" "--dist-url=https://electronjs.org/headers" "--build-from-source"
gyp ERR! cwd /Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/electron_python/electron-python-example/node_modules/zerorpc/node_modules/zeromq
gyp ERR! node -v v8.17.0
gyp ERR! node-gyp -v v6.0.1
gyp ERR! not ok

原因：

表面上是node的v8.h头文件有问题

实际上：的确也是v8.h文件内容有问题

此处内容不全，只有7000多行，实际上完整的文件大概1万多行

但是根本原因在于：

此处的node版本是v8.17.0

最高支持的electron版本是2.0.18

而当前设置的electron的版本是7.1.2

解决办法：去把electron版本从7.1.2改为2.0.18

修改后：

文件：electron_python/electron-python-example/.npmrc

npm_config_target="2.0.18" # electron version
npm_config_runtime="electron" # 为elelctron编译
npm_config_disturl="https://atom.io/download/electron" # 资源下载地址
npm_config_build_from_source="true" # 从源码编译

文件：electron_python/electron-python-example/package.json

{
...
  "devDependencies": {
    "electron": "^2.0.18",
...
  }
}

即可。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

不同系统

不同系统

Electron支持不同的系统的开发和打包。

常见的有：

Win

Mac

Linux

等，下面分别总结相关经验心得。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Win

Win系统

此处整理关于Windows平台中开发Electron的相关心得。

Win中搭建Electron-Python开发环境

把node 10换成node 8：

【已解决】windows10中重新安装node把node 10换成node 8

npm install

去初始化

运行Electron本身demo：

.\node_modules\.bin\electron

运行当前Electron项目：

.\node_modules\.bin\electron .

打包：

npm run dist

注：无需npm run dist_all

即可打包出exe：

双击运行： D:\dev\DevRoot\mitmdumpurlsaver\electron-python-example\builder_output\win-unpacked\mitmdumpUrlSaver.exe

windows中启动Electron-python报错：Uncaught Error A dynamic link library DLL initialization routine failed zerorpc zeromq zmq.node

问题：

文件：electron-python-example\node_modules\zerorpc\node_modules\zeromq\lib\index.js

var EventEmitter = require('events').EventEmitter
  , zmq = require('../build/Release/zmq.node')
  , util = require('util');

中的：

var zmq = require('../build/Release/zmq.node');

运行报错：

Uncaught Error: A dynamic link library (DLL) initialization routine failed.
\\?\D:\dev\DevRoot\mitmdumpurlsaver\electron-python-example\node_modules\zerorpc\node_modules\zeromq\build\Release\zmq.node

原因：

经过一番网上搜索，没人给出解决办法。

后来自己是从

Uncaught Error: A dynamic link library (DLL) initialization routine failed.

加上输出的错误路径

\\?\D:\dev\DevRoot\mitmdumpurlsaver\electron-python-example\node_modules\zerorpc\node_modules\zeromq\build\Release\zmq.node

中的后面的部分

D:\dev\DevRoot\mitmdumpurlsaver\electron-python-example\node_modules\zerorpc\node_modules\zeromq\build\Release\zmq.node

看出是正确的路径，所以怀疑是否是路径方面的问题。后经研究的确就是路径问题。

解决办法：

之前写法是linux中写法，改为同样适配windows的写法：

let path = require('path')
let zmqNodePath = path.join("..", "build", "Release", "zmq.node")
console.log("zmqNodePath=%s", zmqNodePath)
var zmq = require(zmqNodePath)

或：

let osIsWin = os.platform() === "win32"
let zmqNodePath = undefined
if (osIsWin){
  let zmqNodePath = '..\\build\\Release\\zmq.node'
} else {
  let zmqNodePath = '../build/Release/zmq.node'
}
var zmq = require(zmqNodePath)

即可。

运行时Win的exe所在路径

背景：Win中Electron打包后的可执行文件格式一般是exe。

需求：希望在运行期间，获取到当前的实际的exe所在目录的路径。

心得：双击exe运行后，其内部会解压到临时目录，一般是当前用户的AppData目录，类似于这种：

C:\Users\xxx\AppData\Local\Programs\

-》如果想要获取原本（双击以启动运行的）exe所在目录，则就很困难了。

期间去试过参数 --win portable，对应配置：

    "win": {
      "target": "portable",
      "icon": "assets/icon/logo.png"
    }

结果却是，虽然打包出的exe无需安装，双击即可运行，但是运行期间解压的目录是这种更彻底的临时目录：

C:\Users\xx~1\AppData\Local\Temp\1Wgi1npnbDTNiOqe7pBCQL1FL0Z

更加没法找到exe所在目录。

后来去试了试别人提到的相关参数：

process.env.PORTABLE_EXECUTABLE_DIR

最终是可以获取到当前exe所在目录的：

exe所在目录：

相关代码：

let isWin = process.platform === "win32"

if (isWin){
  let portableExecutableDir = process.env.PORTABLE_EXECUTABLE_DIR
  console.log("portableExecutableDir=%s", portableExecutableDir)
}

输出：

portableExecutableDir=D:\dev\DevRoot\mitmdumpurlsaver\electron-python-example\builder_output

供参考。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Mac

Mac系统

此处整理关于Mac系统中开发Electron的相关心得。

npm安装electron速度太慢

mac中npm install很慢，则可以去全局设置源，换成淘宝的源：

npm config set registry https://registry.npm.taobao.org/
npm config set ELECTRON_MIRROR http://npm.taobao.org/mirrors/electron/

且单独给electron的变量，加到启动脚本中：

vi ~/.zshrc

最后加上：

export ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/

保存退出后，去立刻生效：

source ~/.zshrc

再去用echo验证：

# echo $ELECTRON_MIRROR
http://npm.taobao.org/mirrors/electron/

即可极大地提高 npm install去安装electron相关东西的速度。

比如此处是从<100KB/s 变成 > 1MB/s。

获取不到process.env的值

经过之前折腾：

【未解决】Mac中electron打包后process.env是undefined获取不到有效值

确定是：

Mac中，打包后的app在运行期间，无法获取到env变量：

process.env

或：

const remote = electron.remote;
remote.process.env

的值。-》也就无法获取到

process.env.NODE_ENV

或：

remote.process.env.NODE_ENV

的值了 -》 后续就没法通过别人说的：

const remote = electron.remote
let nodeEnv = remote.process.env.NODE_ENV

const isProd = remote.process.env.NODE_ENV === 'production'

let portableExecutableDir = remote.process.env.PORTABLE_EXECUTABLE_DIR

let initCwd = remote.process.env.INIT_CWD

去判断是否是生产环境，以及获取到有效的app可执行文件所在目录了。

获取Electron的app的根目录

此处是

文件：electron-python-example/main.js

const electron = require('electron')
const app = electron.app
console.log("app=%s", app)

console.log("main.js: __dirname=%s", __dirname)
console.log("getAppPath=%s", app.getAppPath())
// let curAppPath = app.getAppPath()
let curAppPath = __dirname
console.log("curAppPath=%s", curAppPath)

let rootPath = curAppPath
// // for debug:
// let rootPath = "/Users/limao/Downloads/electron_app/mitmdumpUrlSaver.app/Contents/Resources/app"
console.log("rootPath=%s", rootPath)
let removeAppSuffixP = /\/[^\/]+\.app\/Contents\/Resources\/app$/gi
rootPath = rootPath.replace(removeAppSuffixP, "")
console.log("rootPath=%s", rootPath)

通过调用：

const createSinglePyProc = (curScript, curPort, curDistFolder) => {

  if (guessPackaged(curDistFolder)) {
    curPyProc = childProcess.execFile(curScript, [curPort, rootPath])
    console.log('execFile curPyProc=%s, from curScript=%s, curPort=%s, rootPath=%s', curPyProc, curScript, curPort, rootPath)
  } else {
    curPyProc = childProcess.spawn('python', [curScript, curPort, rootPath])
    console.log('spawn python curPyProc=%s, from curScript=%s, curPort=%s, rootPath=%s', curPyProc, curScript, curPort, rootPath)
  }

传入到Python端：

文件：electron-python-example/pymitmdump/mitmdumpStartApi.py

class MitmdumpStartApi(object):
    def __init__(self, rootPath):
        self.appRootPath = rootPath
        logging.debug("self.appRootPath=%s", self.appRootPath)

    @zerorpc.stream
    def startProxy(self):
        """Start mitmdump proxy"""
        logging.debug("startProxy")
        from mitmdumpManage import startMitmdumpSaver
        startResp = startMitmdumpSaver(self.appRootPath)

def main():
    rootPath = sys.argv[2]
    logging.debug("rootPath=%s", rootPath)
    zerorpcServer = zerorpc.Server(MitmdumpStartApi(rootPath))

传递给：

文件：electron-python-example/pymitmdump/mitmdumpManage.py

def startMitmdumpSaver(appRootPath):
    """Start mitmdump saver"""
    logging.debug("startMitmdumpSaver")
    logging.debug("appRootPath=%s", appRootPath)
    # electron not package: /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example
    # electron packaged: /Users/limao/Downloads/electron_app
...
    shellCmdList = ['%s' % gConfig["mitmdumpExecutable"], '-p %s' % gConfig["port"], '-s %s' % gConfig["mitmdumpScript"], appRootPath]

在启动了mitmdump后，是通过：

文件：electron-python-example/pymitmdump/mitmdumpUrlSaver.py

def getOutputFolder():
    print("sys.argv=%s" % sys.argv)
    argvLen = len(sys.argv)
    print("argvLen=%s" % argvLen)

    if argvLen >= 4:
        sysArgv3 = sys.argv[3]
        print("sysArgv3=%s" % sysArgv3)
        appRootPath = sysArgv3
        print("appRootPath=%s" % appRootPath)
        outputFoler = appRootPath
    else:
        outputFoler = os.path.dirname(__file__)

    return outputFoler

outputFoler = getOutputFolder()
print("outputFoler=%s" % outputFoler)

中的sys的argv[3]，最终得到了此处的app的root的path路径：

开发环境=本地调试环境：/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example

生产环境=打包发布app后：

当前app运行路径：/Users/limao/Downloads/electron_app

app的root路径：app.getAppPath = /Users/limao/Downloads/electron_app/mitmdumpUrlSaver.app/Contents/Resources/app

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

不同语言

不同语言

Electron除了支持本身的最基本的 Web语言= HTML+CSS+JS 之外，也可以通过其他方式支持其他编程语言和框架。

现整理如下：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Python

Python

此处总结关于Electron支持Python期间遇到的经验。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

electron-python-example

electron-python-example

在参考electron-python-example给Electron添加Python支持期间，遇到很多问题。整理如下：

npm install时zeromq会报错

解决办法：把node从13.5.0换成8.x的版本，比如此处的

brew install node@8

安装出来后的版本是：8.17.0

详见：

【已解决】mac中npm install报错：make Release/obj.target/zmq/binding.o Error 1

能看到electron界面，但是输入计算表达式后，却看不到输出结果

原因：可能有多种

思路：通过electron的默认打开的console的log输出的错误信息中去找原因

如果找不到合适错误原因，则可以尝试去给python的进程加上stdout和stderr

electron-python-example/main.js

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('child process success on port ' + port)

    pyProc.stdout.on('data',function(data){
      var textChunk = data.toString('utf8') // buffer to string
      console.log("textChunk=%s", textChunk)
    })

    pyProc.stderr.on('data', function(data){
      console.log("pyProc error=%s", data)
    });
  }
}

用于查看到输出的普通信息和错误信息，以便于找到错误现象和原因

详见：

【已解决】electron-python-example运行后输入计算内容但是不显示结果

Uncaught ReferenceError process is not defined

解决办法：加上参数配置nodeIntegration: true

electron-python-example/main.js

const createWindow = () => {
  mainWindow = new BrowserWindow(
    {
      width: 800,
      height: 600,
      webPreferences: {
        nodeIntegration: true,
      }
    }
  )

详见：

【已解决】electron-python-example运行出错：Uncaught ReferenceError process is not defined

renderer.js Error Lost remote after 10000ms

原因：python的zerorpc没有安装，或者安装了zerorpc但是python进程没有正常启动

解决办法：确保已安装zerorpc，且的确启动了对应进程

注：此处在虚拟环境virtualenv中

pip install zerorpc

然后确保

electron-python-example/main.js

pyProc = require('child_process').spawn('python', [script, port])

正常启动了。

此处有2种确认方式：

方式1：系统中查看是否有对应python进程

ps aux | grep 61737

其中的61737是python进程PID

是通过js中加了调试代码

console.log('spawn: pyProc=%s', pyProc)

后，启动

./node_modules/.bin/electron .

时，看到console的log中的

  spawnfile: 'python',
  _handle:
   Process {
     owner: [Circular],
     onexit: { [Function] [length]: 2, [name]: '' },
     pid: 61737

有对应python的pid的.。

如果有进程，说明python进程没问题，否则说明进程没正常启动。

方式2： 加上stdout、stderr等监听

  if (pyProc != null) {
    //console.log(pyProc)
    console.log('child process success on port ' + port)

    pyProc.stdout.on('data',function(data){
      var textChunk = data.toString('utf8') // buffer to string
      console.log("textChunk=%s", textChunk)
    })

    pyProc.stderr.on('data', function(data){
      console.log("pyProc error=%s", data)
    });
  }

确认没有error等异常错误信息

此处自己就是看到有

pyProc error=Traceback (most recent call last):
  File "/xxx/electron_python/electron-python-example/pycalc/api.py", line 4, in <module>
    import zerorpc
ModuleNotFoundError: No module named 'zerorpc'

从而发现：

是没有进入virtualenv的venv中，导致没有找到zerorpc

该确保进入虚拟环境：

source venv/bin/activate

即可找到zerorpc，正常启动python进程了。

zerorpc.exceptions.LostRemote: Lost remote after 10s heartbeat

问题：electron的python，用了stream后，运行了10秒后，报错：

js中：invoke startSaver: error=Error: Lost remote after 10000ms

python中：zerorpc.exceptions.LostRemote: Lost remote after 10s heartbeat

原因：js和python端，底层都有heartbeat心跳机制，每隔一段时间，都要检测是否服务还有效alive，否则就报错。

而此处我的情况特殊：

js端，通过zerorpc通讯到python端

但是python端是while True的无限循环，除非进程被干掉，否则不返回

所以肯定无法满足heartbeat的10秒之内返回的要求

解决办法：故意设置一个足够大的心跳时间

文件：electron-python-example/renderer.js

把：

let client = new zerorpc.Client()

改为：

const constLargeEnoughHeartbeat = 60 * 60 * 24 * 30 * 12
clientOptions = {
  "heartbeatInterval": constLargeEnoughHeartbeat,
}
let client = new zerorpc.Client(clientOptions)

以此规避此问题。

Uncaught Error The module zeromq zmq.node was compiled against a different Node.js version

问题：Electron运行

./node_modules/.bin/electron .

报错：

Uncaught Error: The module node_modules/zeromq/build/Release/zmq.node
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 75. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).

原因：此处安装的zeromq的库是针对于

NODE_MODULE_VERSION 57 = Node.js Node.js 8.x

编译出来的，而当前用的node版本是：

NODE_MODULE_VERSION 75 = Node.js Node.js 12.7.0

所以不匹配，没法使用。

解决办法：去重新编译出对应的匹配的zeromq库即可：

注意事项和准备工作：

需要说明的是，此处当时node版本，已经是为了解决

npm install

期间 zerorpc出错，而去降低了node版本，从之前的 node: 13.5.0降低到了：node：8.17.0

且对于node和electron本身，不同版本之间是有依赖关系的，所以此处node 8.17.0，只能用electron 2.0.18

所以此处配置是：

文件：electron_python/electron-python-example/package.json

  "devDependencies": {
    "electron": "^2.0.18",

文件：electron_python/electron-python-example/.npmrc

npm_config_target="2.0.18" # electron version

解决步骤

安装electron-rebuild：

npm install --save-dev electron-rebuild

重新去rebuild：

./node_modules/.bin/electron-rebuild

即可。

注：

编译之前，需要安装cmake

brew install cmake

详见：

【已解决】Electron报错：Uncaught Error The module zeromq zmq.node was compiled against a different Node.js version

优化：被调用python代码的print为输出日志到log文件

背景：希望被调用的python代码的print的日志输出改为输出到log日志文件中，便于后期调试查看log

解决办法：

文件：electron-python-example/pymitmdump/mitmdumpSaverApi.py

from utils import loggingInit
logFilename = "mitmdumpSaverApi.log"
loggingInit(logFilename)

# from __future__ import print_function
from mitmdumpLauncher import startMitmdumpSaver, getMitmdumpStatus
import sys
import os
import zerorpc
import logging

...

def main():
    logging.debug(__file__)

    addr = 'tcp://127.0.0.1:' + parse_port()

相关的logging的初始化：

文件：electron-python-example/pymitmdump/utils.py

# _*_ coding: utf-8 _*_
# Function: Crifan's utils functions, copy from https://github.com/crifan/crifanLibPython
# Update: 20200103
# Author: Crifan Li

import os
import sys
from datetime import datetime, timedelta
import logging
# print("sys.executable=%s" % sys.executable)
# sys.executable=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/venv/bin/python3.8

...

CURRENT_LIB_FILENAME = "crifanLogging"

LOG_FORMAT_FILE = "%(asctime)s %(filename)s:%(lineno)-4d %(levelname)-7s %(message)s"
LOG_LEVEL_FILE = logging.DEBUG
LOG_FORMAT_CONSOLE = "%(asctime)s %(filename)s:%(lineno)-4d %(levelname)-7s %(message)s"
LOG_LEVEL_CONSOLE = logging.INFO

def loggingInit(filename = None,
                fileLogLevel = LOG_LEVEL_FILE,
                fileLogFormat = LOG_FORMAT_FILE,
                fileLogDateFormat = '%Y/%m/%d %I:%M:%S',
                enableConsole = True,
                consoleLogLevel = LOG_LEVEL_CONSOLE,
                consoleLogFormat = LOG_FORMAT_CONSOLE,
                consoleLogDateFormat = '%Y%m%d %I:%M:%S',
                ):
    """
    init logging for both log to file and console

    :param logFilename: input log file name
        if not passed, use current lib filename
    :return: none
    """
    logFilename = ""
    if filename:
        logFilename = filename
    else:
        # logFilename = __file__ + ".log"
        # '/Users/crifan/dev/dev_root/xxx/crifanLogging.py.log'
        logFilename = CURRENT_LIB_FILENAME + ".log"

    # logging.basicConfig(
    #                 level    = fileLogLevel,
    #                 format   = fileLogFormat,
    #                 datefmt  = fileLogDateFormat,
    #                 filename = logFilename,
    #                 encoding = "utf-8",
    #                 filemode = 'w')

    # rootLogger = logging.getLogger()
    rootLogger = logging.getLogger("")
    rootLogger.setLevel(fileLogLevel)
    fileHandler = logging.FileHandler(
        filename=logFilename,
        mode='w',
        encoding="utf-8")
    fileHandler.setLevel(fileLogLevel)
    fileFormatter = logging.Formatter(
        fmt=fileLogFormat,
        datefmt=fileLogDateFormat
    )
    fileHandler.setFormatter(fileFormatter)
    rootLogger.addHandler(fileHandler)

    if enableConsole :
        # define a Handler which writes INFO messages or higher to the sys.stderr
        console = logging.StreamHandler()
        console.setLevel(consoleLogLevel)
        # set a format which is simpler for console use
        formatter = logging.Formatter(
            fmt=consoleLogFormat,
            datefmt=consoleLogDateFormat)
        # tell the handler to use this format
        console.setFormatter(formatter)
        rootLogger.addHandler(console)
。。。

被调用的python

文件：electron-python-example/pymitmdump/mitmdumpLauncher.py

...
import subprocess
import os
import re
import logging
from utils import osIsMacOS, osIsWinows
...
curFileFolder = os.path.dirname(__file__)
logging.debug("curFileFolder=%s", curFileFolder) # curFileFolder=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller
...

即可正常输出log日志到文件中了：

获取Mac中electron-builder打包出的app通过双击启动时app文件所在的文件夹路径

背景：Mac中带Python的Electron打包后是app文件，双击app运行，希望在运行期间获取app所在目录

文件：electron-python-example/pymitmdump/mitmdumpStartApi.py

import os
import sys
import logging
from utils import getFilenameNoPointSuffix, loggingInit

sysMeipass = None
sysArgv0 = sys.argv[0]
hasMeipass = hasattr(sys, '_MEIPASS')
isBundled = hasMeipass
if isBundled:
    # /xxx/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi -> mitmdumpStartApi
    logFilenameNoSuffix = sysArgv0.split(os.path.sep)[-1]
    logFilename = "%s.log" % logFilenameNoSuffix
    sysMeipass = sys._MEIPASS
    logPath = sysMeipass
else:
    logFilename = "%s.log" % getFilenameNoPointSuffix(__file__)
    logPath = os.path.dirname(__file__)
logFullPath = os.path.join(logPath, logFilename)
# logFullPath = "/Users/limao/Downloads/electron_app/mitmdumpStartApi.log"
loggingInit(logFullPath)

logging.debug("sysArgv0=%s", sysArgv0)
logging.debug("hasMeipass=%s", hasMeipass)
logging.debug("sysMeipass=%s", sysMeipass)

logging.debug("logFilename=%s", logFilename)
logging.debug("logPath=%s", logPath)
logging.debug("logFullPath=%s", logFullPath)

sysExecutable = sys.executable
logging.debug("sysExecutable=%s", sysExecutable)
sysArgv = sys.argv
logging.debug("sysArgv=%s", sysArgv)
hasFrozen = hasattr(sys, 'frozen')
logging.debug("hasFrozen=%s", hasFrozen)
if hasFrozen:
    sysForzen = sys.frozen
    logging.debug("sysForzen=%s", sysForzen)

logging.debug("os.getcwd=%s", os.getcwd())
logging.debug("os.curdir=%s", os.curdir)
logging.debug("__file__=%s", __file__)
logging.debug("__name__=%s", __name__)

可以获取到对应路径：

打包后：bundle的=是二进制 的路径

(PyInstaller)打包后的（且后来再用electron-builder打包在一起的，所生成的）app文件，双击运行后对应输出是：

2020/01/13 06:05:51 mitmdumpStartApi.py:23 DEBUG sysArgv0=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi
2020/01/13 06:05:51 mitmdumpStartApi.py:24 DEBUG hasMeipass=True
2020/01/13 06:05:51 mitmdumpStartApi.py:25 DEBUG sysMeipass=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi
2020/01/13 06:05:51 mitmdumpStartApi.py:27 DEBUG logFilename=mitmdumpStartApi.log
2020/01/13 06:05:51 mitmdumpStartApi.py:28 DEBUG logPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi
2020/01/13 06:05:51 mitmdumpStartApi.py:29 DEBUG logFullPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi.log
2020/01/13 06:05:51 mitmdumpStartApi.py:32 DEBUG sysExecutable=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi
2020/01/13 06:05:51 mitmdumpStartApi.py:34 DEBUG sysArgv=['/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi', '4242']
2020/01/13 06:05:51 mitmdumpStartApi.py:36 DEBUG hasFrozen=True
2020/01/13 06:05:51 mitmdumpStartApi.py:39 DEBUG sysForzen=True
2020/01/13 06:05:51 mitmdumpStartApi.py:41 DEBUG os.getcwd=/
2020/01/13 06:05:51 mitmdumpStartApi.py:42 DEBUG os.curdir=.
2020/01/13 06:05:51 mitmdumpStartApi.py:43 DEBUG __file__=mitmdumpStartApi.py
2020/01/13 06:05:51 mitmdumpStartApi.py:44 DEBUG __name__=__main__
2020/01/13 06:05:51 mitmdumpStartApi.py:101 DEBUG mitmdumpStartApi.py
。。。

其中此处app本身所在位置是：

输出的路径是：

sysArgv0：

/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpStartApi

logPath：

/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/builder_output/mac/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi

其实也就是sysMeipass的值

没打包之前：是源码=是xxx.py=此处是mitmdumpStartApi.py 的路径

注意：此处先要删除2个dist目录（pymitmdumpstartdist和pymitmdumpotherdist）

运行

./node_modules/.bin/electron .

输出：

2020/01/13 06:13:55 mitmdumpStartApi.py:23 DEBUG sysArgv0=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py
2020/01/13 06:13:55 mitmdumpStartApi.py:24 DEBUG hasMeipass=False
2020/01/13 06:13:55 mitmdumpStartApi.py:25 DEBUG sysMeipass=None
2020/01/13 06:13:55 mitmdumpStartApi.py:27 DEBUG logFilename=mitmdumpStartApi.log
2020/01/13 06:13:55 mitmdumpStartApi.py:28 DEBUG logPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump
2020/01/13 06:13:55 mitmdumpStartApi.py:29 DEBUG logFullPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.log
2020/01/13 06:13:55 mitmdumpStartApi.py:32 DEBUG sysExecutable=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/venv/bin/python
2020/01/13 06:13:55 mitmdumpStartApi.py:34 DEBUG sysArgv=['/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py', '4242']
2020/01/13 06:13:55 mitmdumpStartApi.py:36 DEBUG hasFrozen=False
2020/01/13 06:13:55 mitmdumpStartApi.py:41 DEBUG os.getcwd=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example
2020/01/13 06:13:55 mitmdumpStartApi.py:42 DEBUG os.curdir=.
2020/01/13 06:13:55 mitmdumpStartApi.py:43 DEBUG __file__=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py
2020/01/13 06:13:55 mitmdumpStartApi.py:44 DEBUG __name__=__main__
2020/01/13 06:13:55 mitmdumpStartApi.py:101 DEBUG /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py
。。。

可见：sys没有frozen，也没有_MEIPASS值。

打包electron-python的单个主文件报错：OSError Python library not found libpython3.8m.dylib

问题：mac中用PyInstaller打包python报错OSError Python library not found libpython3.8m.dylib

原因：此处PyInstaller打包需要动态库，类似于libpython3.8m.dylib，而默认安装的python是静态库 /Users/limao/.pyenv/versions/3.8.0/lib/libpython3.8.a

解决办法：重新安装带动态库的python 3.8

具体步骤：加上参数重新编译：

最好先去删除之前旧的python：

pyenv uninstall 3.8.0
rm -rf /Users/limao/.pyenv/versions/3.8.0/

然后再去重新安装：

env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.8.0

即可正常使用PyInstaller。

其他说明：

（1）此处安装后对应的动态库是

limao@xxx  ~  ll /Users/limao/.pyenv/versions/3.8.0/Python.framework/Versions/3.8/lib
total 0
lrwxr-xr-x    1 limao  CORP\Domain Users     9B  1  6 10:34 libpython3.8.dylib -> ../Python

软链接对应的文件是：3.4MB的Python

~  ll /Users/limao/.pyenv/versions/3.8.0/Python.framework/Versions/3.8/Python
-rwxr-xr-x  1 limao  CORP\Domain Users   3.4M  1  6 10:34 /Users/limao/.pyenv/versions/3.8.0/Python.framework/Versions/3.8/Python

（2）此处安装后，还需要安装项目所依赖的一些库

比如：

pip install virtualenv

virtualenv venv
source venv/bin/activate

pip install zerorpc

zerorpc的stream持续输出无返回导致后续函数stopProxy无法调用

问题：mitmdumpSaverApi.py中调用启动代理后，代理没有输出。

原因：由于启动代理，内部逻辑是，没有立刻返回，而是通过stream持续返回输出，导致进程被占用，所以stopProxy都没有被执行到

解决办法：新增一个进程，额外单独的去处理关闭代理（以及获取代理状态）

具体步骤：

js中createPyProc创建了2个线程：

文件：electron-python-example/main.js

const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const path = require('path')

/*************************************************************
 * py process
 *************************************************************/

// const PY_DIST_FOLDER = 'pycalcdist'
const PY_DIST_FOLDER = 'pymitmdumpdist'

// const PY_FOLDER = 'pycalc'
// const PY_MODULE = 'api' // without .py suffix

const PY_FOLDER = 'pymitmdump'
// const PY_MODULE = 'mitmdumpSaverApi' // without .py suffix
const PY_MODULE_START = 'mitmdumpStartApi' // without .py suffix
const PY_MODULE_OTHER = 'mitmdumpOtherApi' // without .py suffix

PORT_START = 4242
PORT_OTHER = 4243

// let pyProc = null
let pyProcStart = null
let pyProcOther = null
// let pyPort = null

const guessPackaged = () => {
  // console.log("guessPackaged")
  const fullPath = path.join(__dirname, PY_DIST_FOLDER)
  // console.log("fullPath=%s", fullPath)
  isPackaged = require('fs').existsSync(fullPath)
  // console.log("isPackaged=%s", isPackaged)
  return isPackaged
}

// 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)
// }

const getScriptPath = (pyModule) => {
  if (!guessPackaged()) {
    return path.join(__dirname, PY_FOLDER, pyModule + '.py')
  }
  if (process.platform === 'win32') {
    return path.join(__dirname, PY_DIST_FOLDER, pyModule, pyModule + '.exe')
  }
  return path.join(__dirname, PY_DIST_FOLDER, pyModule, pyModule)
}

const getScriptPathStart = () => {
  return getScriptPath(PY_MODULE_START)
}

const getScriptPathOther = () => {
  return getScriptPath(PY_MODULE_OTHER)
}

// const selectPort = () => {
//   pyPort = 4242
//   return pyPort
// }

const createSinglePyProc = (curScript, curPort) => {
  console.log('createSinglePyProc: curScript=%s, curPort=%s', curScript, curPort)
  let curPyProc = null

  if (guessPackaged()) {
    // curPyProc = require('child_process').execFile(curScript)
    curPyProc = require('child_process').execFile(curScript, [curPort])
  } else {
    // curPyProc = require('child_process').spawn('python', [curScript])
    curPyProc = require('child_process').spawn('python', [curScript, curPort])
  }

  console.log('curPyProc=%s', curPyProc)

  if (curPyProc != null) {
    console.log("success create child proces")
    // console.log('curPyProc=%o', curPyProc)
    console.log('curPyProc.spawnargs=%s', curPyProc.spawnargs)
    console.log('curPyProc.pid=%s', curPyProc.pid)

    curPyProc.stdout.on('data',function(chunk){
      var chunkUtf8Str = chunk.toString('utf8') // buffer to string
      console.log("curPyProc stdout data: chunkUtf8Str=%s", chunkUtf8Str)
    })

    curPyProc.stderr.on('data', function(chunk){
      console.error("curPyProc stderr data: chunk=%s\r\n", chunk)
    })

    curPyProc.on('close', function(closeCode){
      console.log("curPyProc close: closeCode=%s\r\n", closeCode)
    })
    // for Windows:
    curPyProc.on('exit', function(exitCode){
      console.log("curPyProc exit: exitCode=%s\r\nn", exitCode)
    })
  }

  return curPyProc
}

const createPyProc = () => {
  console.log('In createPyProc')
  // let script = getScriptPath()
  // console.log('script=%s', script)
  // let port = '' + selectPort()
  // console.log('port=%s', port)

  let scriptStart = getScriptPathStart()
  console.log('scriptStart=%s', scriptStart)
  pyProcStart = createSinglePyProc(scriptStart, PORT_START)

  let scriptOther = getScriptPathOther()
  console.log('scriptOther=%s', scriptOther)
  pyProcOther = createSinglePyProc(scriptOther, PORT_OTHER)
}

const exitPyProc = () => {
  // pyProc.kill()
  // pyProc = null
  console.log('exitPyProc')

  if (pyProcStart != null) {
    pyProcStart.kill()
    pyProcStart = null
  }

  if (pyProcOther != null) {
    pyProcOther.kill()
    pyProcOther = null
  }

  // pyPort = null
}

app.on('ready', createPyProc)
app.on('will-quit', exitPyProc)

...

文件：electron-python-example/renderer.js

const zerorpc = require("zerorpc")

// let client = new zerorpc.Client()

PORT_START = 4242
PORT_OTHER = 4243

/****************************************
* Client Start Server
****************************************/
const constLargeEnoughHeartbeat = 60 * 60 * 24 * 30 * 12 // 1 Year
clientOptions = {
  "heartbeatInterval": constLargeEnoughHeartbeat,
}
let clientStart = new zerorpc.Client(clientOptions)

clientStart.connect(`tcp://127.0.0.1:${PORT_START}`)
console.log(`clientStart ${PORT_START} connected`)
clientStart.invoke("echo", "server ready", (error, res) => {
  if(error || res !== 'server ready') {
    console.error(error)
  } else {
    console.log("clientStart %d server is ready", PORT_START)
  }
})

let startElement = document.querySelector('#startProxy')
let outputElement = document.querySelector('#output')

startElement.addEventListener('click', () => {
  console.log("startElement clicked")
  outputElement.textContent = "" // clear before start

  clientStart.invoke("startProxy", (error, res, more) => {
    if(error) {
      console.error("invoke startProxy: error=%s, more=%s", error, more)
    } else {
      console.log("invoke startProxy: res=%s, more=%s", res, more)
      if (res != undefined) {
        outputElement.textContent += "\n" + res
        // outputElement.textContent += res
      }
      outputElement.scrollTop = outputElement.scrollHeight
    }
  })
})

/****************************************
* Client Other Server
****************************************/
let clientOther = new zerorpc.Client()

clientOther.connect(`tcp://127.0.0.1:${PORT_OTHER}`)
console.log(`clientOther ${PORT_OTHER} connected`)
clientOther.invoke("echo", "server ready", (error, res) => {
  if(error || res !== 'server ready') {
    console.error(error)
  } else {
    console.log("clientOther %d server is ready", PORT_OTHER)
  }
})

let stopElement = document.querySelector('#stopProxy')

stopElement.addEventListener('click', () => {
  console.log("stopElement clicked")
  clientOther.invoke("stopProxy", (error, res) => {
    if(error) {
      console.error("invoke stopProxy: error=%s", error)
    } else {
      console.log("invoke stopProxy: res=%s", res)
    }
  })
})

let getStatusElement = document.querySelector('#getStatus')
let statusElement = document.querySelector('#serverStatus')

getStatusElement.addEventListener('click', () => {
  console.log("getStatusElement clicked")
  clientOther.invoke("getStatus", (error, res) => {
    if(error) {
      console.error("invoke getStatus: error=%s", error)
    } else {
      console.log("invoke getStatus: res=%s", res)
      statusElement.textContent = res
    }
  })
})

分别调用的python脚本文件是：

文件：electron-python-example/pymitmdump/mitmdumpStartApi.py

from utils import getFilenameNoPointSuffix, loggingInit
loggingInit("%s.log" % getFilenameNoPointSuffix(__file__))

from mitmdumpManage import startMitmdumpSaver
import sys
import zerorpc
import logging

class MitmdumpStartApi(object):
    def echo(self, text):
        """Echo any text from zerorpc client"""
        logging.debug("text=%s", text)
        return text

    @zerorpc.stream
    def startProxy(self):
        """Start mitmdump proxy"""
        logging.debug("startProxy")
        startResp = startMitmdumpSaver()
        logging.debug("startResp=%s", startResp)
        return startResp

def parse_port():
    port = 4242
    try:
        port = int(sys.argv[1])
    except Exception as e:
        pass
    curPortStr = '{}'.format(port)
    return curPortStr

def main():
    logging.debug(__file__)

    addr = 'tcp://127.0.0.1:' + parse_port()
    logging.debug("addr=%s", addr)
    zerorpcServer = zerorpc.Server(MitmdumpStartApi())
    logging.debug("zerorpcServer=%s", zerorpcServer)
    zerorpcServer.bind(addr)
    logging.info('start zerorpc server running on %s', addr)
    zerorpcServer.run()

if __name__ == '__main__':
    main()

和：

文件：electron-python-example/pymitmdump/mitmdumpOtherApi.py

from utils import getFilenameNoPointSuffix, loggingInit
loggingInit("%s.log" % getFilenameNoPointSuffix(__file__))

from mitmdumpManage import getMitmdumpStatus, stopMitmdump
import sys
import zerorpc
import logging

class MitmdumpOtherApi(object):
    def echo(self, text):
        """Echo any text from zerorpc client"""
        logging.debug("text=%s", text)
        return text

    def getStatus(self):
        """Get mitmdump server status"""
        logging.debug("getStatus")
        isRunning, processList = getMitmdumpStatus()
        logging.debug("isRunning=%s, processList=%s", isRunning, processList)
        return isRunning
        # return isRunning, processList

    def stopProxy(self):
        """Stop mitmdump proxy"""
        logging.debug("stopProxy")
        stopResp = stopMitmdump()
        logging.debug("stopResp=%s", stopResp)
        return stopResp

def parse_port():
    port = 4242
    try:
        port = int(sys.argv[1])
    except Exception as e:
        pass
    curPortStr = '{}'.format(port)
    return curPortStr

def main():
    logging.debug(__file__)

    addr = 'tcp://127.0.0.1:' + parse_port()
    logging.debug("addr=%s", addr)
    zerorpcServer = zerorpc.Server(MitmdumpOtherApi())
    logging.debug("zerorpcServer=%s", zerorpcServer)
    zerorpcServer.bind(addr)
    logging.info('start zerorpc server running on %s', addr)
    zerorpcServer.run()

if __name__ == '__main__':
    main()

其中都调用了最底层实现逻辑的代码：

文件：electron-python-example/pymitmdump/mitmdumpManage.py

# _*_ coding: utf-8 _*_
# Function: Launch mitmdump to start script service to proxy to save url
# Update: 20191231
# Author: Crifan Li

import subprocess
import os
import re
import logging
from utils import osIsMacOS, osIsWinows

def startMitmdumpSaver():
    """Start mitmdump saver"""
    logging.debug("startMitmdumpSaver")

    isUseShell = False
    # isUseShell = True

    curFileFolder = os.path.dirname(__file__)
    logging.debug("curFileFolder=%s", curFileFolder) # curFileFolder=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller

    Mitmdump_Mac = "mitmdump_executable/mac/mitmdump"
    Mitmdump_Win = "mitmdump_executable/win/mitmdump.exe"
    mitmdumpExecutablePath = None
    if osIsMacOS():
        mitmdumpExecutablePath = Mitmdump_Mac
    elif osIsWinows():
        mitmdumpExecutablePath = Mitmdump_Win
    logging.debug("mitmdumpExecutablePath=%s", mitmdumpExecutablePath) # mitmdumpExecutablePath=mitmdump_executable/mac/mitmdump

    mitmdumpExecutableFullPath = os.path.join(curFileFolder, mitmdumpExecutablePath)
    logging.debug("mitmdumpExecutableFullPath=%s", mitmdumpExecutableFullPath) # mitmdumpExecutableFullPath=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdump_executable/mac/mitmdump
    # mitmdumpExecutable = "mitmdump"
    # mitmdumpExecutable = "pyinstaller/mitmdump_executable/mac/mitmdump"
    mitmdumpExecutable = mitmdumpExecutableFullPath
    logging.debug("mitmdumpExecutable=%s", mitmdumpExecutable) # mitmdumpExecutable=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdump_executable/mac/mitmdump

    mitmdumpScriptFilename = "mitmdumpUrlSaver.py"
    logging.debug("mitmdumpScriptFilename=%s", mitmdumpScriptFilename)
    mitmdumpScriptFullPath = os.path.join(curFileFolder, mitmdumpScriptFilename)
    logging.debug("mitmdumpScriptFullPath=%s", mitmdumpScriptFullPath) # mitmdumpScriptFullPath=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdumpUrlSaver.py
    mitmdumpScript = mitmdumpScriptFullPath
    logging.debug("mitmdumpScript=%s", mitmdumpScript)

    gConfig = {
        "mitmdumpExecutable": mitmdumpExecutable,
        "port": 8081,
        "mitmdumpScript": mitmdumpScript,
    }
    logging.debug("gConfig=%s", gConfig)

    # shellCmdList = ['ping', '-c 4', 'www.crifan.com']
    shellCmdList = ['%s' % gConfig["mitmdumpExecutable"], '-p %s' % gConfig["port"], '-s %s' % gConfig["mitmdumpScript"]]
    logging.debug("shellCmdList=%s", shellCmdList)
    # shellCmdList=['/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdump_executable/mac/mitmdump', '-p 8081', '-s /Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdumpUrlSaver.py']

    shellCmdStr = " ".join(shellCmdList)
    logging.debug("shellCmdStr=%s", shellCmdStr)
    # shellCmdStr=/Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdump_executable/mac/mitmdump -p 8081 -s /Users/limao/dev/crifan/mitmdump/mitmdumpUrlSaver/pyinstaller/mitmdumpUrlSaver.py

    if isUseShell:
        shellCmd = shellCmdStr
    else:
        shellCmd = shellCmdList
    logging.info("shellCmd=%s", shellCmd)

    curProcess = subprocess.Popen(
        shellCmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
        shell=isUseShell,
    )
    logging.debug("curProcess=%s", curProcess)

    while True:
        curLineOutput = curProcess.stdout.readline()
        curLineOutput = curLineOutput.strip()
        logging.debug("curLineOutput=%s", curLineOutput)
        yield curLineOutput

        # Do something else
        returnCode = curProcess.poll()
        if returnCode is not None:
            logging.info('returnCode=%s', returnCode)

            # Process has finished, read rest of the output
            # for eachLineOutput in curProcess.stdout.readlines():
            #     eachLineOutput = eachLineOutput.strip()
            #     logging.debug("eachLineOutput=%s", eachLineOutput)
            #     yield eachLineOutput
            respLineList = curProcess.stdout.readlines()
            logging.info('respLineList=%s', respLineList)
            yield respLineList

            break

def getMitmdumpStatus():
    """Get current mitmdump status"""
    logging.debug("getMitmdumpStatus")

    isRunning, processInfoList = False, []
    if osIsMacOS():
        shellCmdStr = "ps aux | grep mitmdump"
    elif osIsWinows():
        shellCmdStr = "tasklist | findstr mitmdump"
    logging.debug("shellCmdStr=%s", shellCmdStr)
    shellRespStr = subprocess.check_output(
        shellCmdStr,
        shell=True,
        universal_newlines=True,
    )
    logging.debug("shellRespStr=%s", shellRespStr)
    singleLineList = shellRespStr.split(os.linesep)
    logging.debug("singleLineList=%s", singleLineList)
    for eachLineStr in singleLineList:
        # TODO: add windows support extract mitmdump from output

        logging.debug("eachLineStr=%s", eachLineStr)
        # mitmdumpCmdPattern = "mitmdump\s+-p\s+\d+-s\s+\S+\.py"
        # mitmdumpCmdPattern   = "mitmdump\s+-p\s+\d+\s+-s\s+\S+\.py"
        # mitmdumpCmdPattern   = "mitmdump\s+-p\s+(?P<portStr>\d+)\s+-s\s+(?P<scriptFile>\S+?\.py)"
        # mitmdumpCmdPattern   = "\s+(?P<mitmdumpFile>\S+mitmdump)\s+-p\s+(?P<portStr>\d+)\s+-s\s+(?P<scriptFile>\S+?\.py)"
        # mitmdumpCmdPattern   = "^\w+\s+(?P<pidStr>\d+).+?\s+(?P<mitmdumpFile>\S+mitmdump)\s+-p\s+(?P<portStr>\d+)\s+-s\s+(?P<scriptFile>\S+?\.py)"
        mitmdumpCmdPattern   = "^\w+\s+(?P<pidStr>\d+).+\s+(?P<mitmdumpFile>\S+mitmdump)\s+-p\s+(?P<portStr>\d+)\s+-s\s+(?P<scriptFile>\S+?\.py)"
        # foundMitmdump = re.match(mitmdumpCmdPattern, eachLineStr, re.IGNORECASE)
        foundMitmdump = re.search(mitmdumpCmdPattern, eachLineStr, re.IGNORECASE)
        logging.debug("foundMitmdump=%s", foundMitmdump)
        if foundMitmdump:
            isRunning = True
            matchedMitmdumpStr = foundMitmdump.group(0)
            logging.debug("matchedMitmdumpStr=%s", matchedMitmdumpStr)
            pidStr = foundMitmdump.group("pidStr")
            pidInt = int(pidStr)
            logging.debug("pidInt=%s", pidInt)
            mitmdumpFile = foundMitmdump.group("mitmdumpFile")
            logging.debug("mitmdumpFile=%s", mitmdumpFile)
            portStr = foundMitmdump.group("portStr")
            portInt = int(portStr)
            logging.debug("portInt=%s", portInt)
            scriptFile = foundMitmdump.group("scriptFile")
            logging.debug("scriptFile=%s", scriptFile)
            curProcessDict = {
                "pid": pidInt,
                "mitmdumpFile": mitmdumpFile,
                "port": portInt,
                "scriptFile": scriptFile,
            }
            logging.debug("curProcessDict=%s", curProcessDict)
            processInfoList.append(curProcessDict)

    # logging.info("isRunning=%s, processInfoList=%s", isRunning, processInfoList)
    logging.info("isRunning=%s", isRunning)
    if processInfoList:
        for curIdx, eachProcess in enumerate(processInfoList):
            logging.info("[%d] %s", curIdx, eachProcess)
    return isRunning, processInfoList

def stopMitmdump():
    """Stop mitmdump"""
    # TODO: add windows support
    shellCmdStr = "killall mitmdump"
    logging.debug("shellCmdStr=%s", shellCmdStr)
    try:
        shellRespStr = subprocess.check_output(
            shellCmdStr,
            shell=True,
            universal_newlines=True,
        )
        logging.info("shellCmdStr='%s' -> shellRespStr=%s", shellCmdStr, shellRespStr)
    except subprocess.CalledProcessError as procErr:
        logging.error("shellCmdStr='%s' -> procErr=%s", shellCmdStr, procErr)
        shellRespStr = None

    return shellRespStr

if __name__ == "__main__":
    from utils import loggingInit
    logFilename = "mitmdumpManage.log"
    loggingInit(logFilename)

    # getMitmdumpStatus()
    stopMitmdump()

以及，其中start部分是调用的是：

文件：electron-python-example/pymitmdump/mitmdumpUrlSaver.py

class Saver:
    def request(self, flow):
        ...
addons = [Saver()]

正常启动后：

输出：

 ./node_modules/.bin/electron .
In createPyProc
scriptStart=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py
createSinglePyProc: curScript=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py, curPort=4242
curPyProc=[object Object]
success create child proces
curPyProc.spawnargs=python,/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py,4242
curPyProc.pid=7941
scriptOther=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpOtherApi.py
createSinglePyProc: curScript=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpOtherApi.py, curPort=4243
curPyProc=[object Object]
success create child proces
curPyProc.spawnargs=python,/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpOtherApi.py,4243
curPyProc.pid=7942
curPyProc stderr data: chunk=20200103 06:07:21 mitmdumpOtherApi.py:65   INFO    start zerorpc server running on tcp://127.0.0.1:4243

curPyProc stderr data: chunk=20200103 06:07:21 mitmdumpStartApi.py:65   INFO    start zerorpc server running on tcp://127.0.0.1:4242

以及两个log文件：

文件：electron-python-example/mitmdumpOtherApi.log

2020/01/03 06:07:21 mitmdumpOtherApi.py:58   DEBUG   /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpOtherApi.py
2020/01/03 06:07:21 mitmdumpOtherApi.py:61   DEBUG   addr=tcp://127.0.0.1:4243
2020/01/03 06:07:21 mitmdumpOtherApi.py:63   DEBUG   zerorpcServer=<zerorpc.core.Server object at 0x10daad640>
2020/01/03 06:07:21 events.py:326  DEBUG   bound to tcp://127.0.0.1:4243 (status=None)
2020/01/03 06:07:21 mitmdumpOtherApi.py:65   INFO    start zerorpc server running on tcp://127.0.0.1:4243
2020/01/03 06:07:21 channel.py:125  DEBUG   <-- new channel 31a342f2-a9a7-4181-918e-000000000001
2020/01/03 06:07:21 mitmdumpOtherApi.py:30   DEBUG   text=server ready
2020/01/03 06:07:21 channel.py:139  DEBUG   -x- closed channel 31a342f2-a9a7-4181-918e-000000000001

文件：electron-python-example/mitmdumpStartApi.log

2020/01/03 06:07:21 mitmdumpStartApi.py:58   DEBUG   /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpStartApi.py
2020/01/03 06:07:21 mitmdumpStartApi.py:61   DEBUG   addr=tcp://127.0.0.1:4242
2020/01/03 06:07:21 mitmdumpStartApi.py:63   DEBUG   zerorpcServer=<zerorpc.core.Server object at 0x10d789a30>
2020/01/03 06:07:21 events.py:326  DEBUG   bound to tcp://127.0.0.1:4242 (status=None)
2020/01/03 06:07:21 mitmdumpStartApi.py:65   INFO    start zerorpc server running on tcp://127.0.0.1:4242
2020/01/03 06:07:21 channel.py:125  DEBUG   <-- new channel 31a342f2-a9a7-4181-918e-000000000000
2020/01/03 06:07:21 mitmdumpStartApi.py:16   DEBUG   text=server ready
2020/01/03 06:07:21 channel.py:139  DEBUG   -x- closed channel 31a342f2-a9a7-4181-918e-000000000000

后续启动mitmdump后，再去get status或stop，可以正常调用了：

就不会被之前只有单一进程而阻塞和卡死了。

优化：js端通过zerorpc调用python代码的逻辑优化为支持返回连续输出结果

背景：Electron的js端通过zerorpc调用python代码，之前只能返回单次结果，现象希望支持返回连续的（mitmdump抓包）输出结果

解决办法：

（1）python中

文件：electron-python-example/pymitmdump/mitmdumpLauncher.py

def startMitmdumpSaver():
    """Start mitmdump saver"""
    print("mitmdumpLauncher: startMitmdumpSaver")

    curProcess = subprocess.Popen(
        shellCmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
        shell=isUseShell,
    )
    print("curProcess=%s" % curProcess)

    while True:
        curLineOutput = curProcess.stdout.readline()
        curLineOutput = curLineOutput.strip()
        # print("curLineOutput=%s" % curLineOutput)
        # print(curLineOutput)
        yield curLineOutput

        # Do something else
        returnCode = curProcess.poll()
        if returnCode is not None:
            print('returnCode=%s' % returnCode)

            # Process has finished, read rest of the output
            for eachLineOutput in curProcess.stdout.readlines():
                eachLineOutput = eachLineOutput.strip()
                print("eachLineOutput=%s" % eachLineOutput)

                yield eachLineOutput

            break

核心逻辑：通过yield输出连续内容

对应调用：

文件：electron-python-example/pymitmdump/mitmdumpSaverApi.py

from mitmdumpLauncher import startMitmdumpSaver
import zerorpc

class MitmdumpSaverApi(object):
    @zerorpc.stream
    def startSaver(self):
        """Start mitmdump saver to save url to file"""
        print("MitmdumpSaverApi startSaver")

        respValue = startMitmdumpSaver()
        print("respValue=%s" % respValue)
        return respValue

（2）js中

文件：electron-python-example/renderer.js

const zerorpc = require("zerorpc")
// let client = new zerorpc.Client()
const constLargeEnoughHeartbeat = 60 * 60 * 24 * 30 * 12 // 1 Year
clientOptions = {
  "heartbeatInterval": constLargeEnoughHeartbeat,
}
let client = new zerorpc.Client(clientOptions)

let startElement = document.querySelector('#startSaver')
let outputElement = document.querySelector('#output')

startElement.addEventListener('click', () => {
  console.log("startElement clicked")
  client.invoke("startSaver", (error, res, more) => {
    if(error) {
      console.error("invoke startSaver: error=%s, more=%s", error, more)
    } else {
      console.log("invoke startSaver: res=%s, more=%s", res, more)
      outputElement.textContent += "\n" + res
      // outputElement.textContent += res
      outputElement.scrollTop = outputElement.scrollHeight
    }
  })
})

效果：持续的输出mitmdump代理抓包到的url链接了

Electron打包双击app启动时python用正则处理从sys的argv中解析出app文件所在根目录路径

背景：对于支持Python的Electron打包后的app，希望在app启动运行期间，用Python从sys的argv中解析出app文件所在根目录路径

解决办法：

文件：electron-python-example/pymitmdump/mitmdumpUrlSaver.py

def extractAppFolderFromArgv(curArgvList):
    """Extract app folder from argv list

    Examples:
        input: ['/Users/limao/Downloads/electron_app/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdump_executable/mac/mitmdump', '-p 8081', '-s /Users/limao/Downloads/electron_app/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpUrlSaver.py'],
        output: /Users/limao/Downloads/electron_app
    """
    appFolder = None

    eachArgvStr = " ".join(curArgvList)
    foundScriptPy = re.search("-s\s+(?P<scriptPy>.+?\.py)", eachArgvStr, re.I)
    if foundScriptPy:
        curScripPy = foundScriptPy.group("scriptPy")

        print("curScripPy=%s" % curScripPy)
        # TODO: add windows exe support
        macAppP = "[^\/]+\.app"
        # macAppP = "[^\]+\.app"
        # macAppP = "\\.app"
        # macAppP = "\.app"
        # macAppP = ".app"
        # if re.match(macAppP, curScripPy, re.I):
        # if re.match(macAppP, curScripPy):
        # isMacApp = re.match(macAppP, curScripPy)
        foundMacApp = re.search(macAppP, curScripPy)
        if foundMacApp:
            removeMacAppP = "/?[^/]+\.app(.+)?$"
            # appPath = re.sub(removeMacAppP, "", curScripPy, flags=re.I)
            appFolder = re.sub(removeMacAppP, "", curScripPy)
            print("appFolder=%s" % appFolder)
        else:
            print("not mac app: %s" % curScripPy)

    return appFolder

appFolder = extractAppFolderFromArgv(sys.argv)
print("appFolder=%s" % appFolder)
if appFolder:
    outputFoler = appFolder
else:
    outputFoler = os.path.dirname(__file__)
print("outputFoler=%s" % outputFoler)

打包后双击app运行，即可得到根目录：

curScripPy=/Users/limao/Downloads/electron_scripPy/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpUrlSaver.py
appFolder=/Users/limao/Downloads/electron_scripPy
appFolder=/Users/limao/Downloads/electron_scripPy
outputFoler=/Users/limao/Downloads/electron_scripPy
cfgSaveFileFullPath=/Users/limao/Downloads/electron_scripPy/mitmdump_saved_url.txt
self.fileToSave=/Users/limao/Downloads/electron_scripPy/mitmdump_saved_url_20200114_180412.txt
Loading script  /Users/limao/Downloads/electron_scripPy/mitmdumpUrlSaver.app/Contents/Resources/app/pymitmdumpstartdist/mitmdumpStartApi/mitmdumpUrlSaver.py
Proxy server listening at http://*:8081

2种情况对应的输出：

（1）debug=调试时

curScripPy=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpUrlSaver.py
not mac app: /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpUrlSaver.py
appFolder=None
outputFoler=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump
cfgSaveFileFullPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdump_saved_url.txt
self.fileToSave=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdump_saved_url_20200114_181130.txt
Loading script  /Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdumpUrlSaver.py
Proxy server listening at http://*:8081

（2）普通命令行中调用mitmdump时

✘ limao@xxx  ~/dev/xxx/electron-python-example/pymitmdump   master ●  mitmdump -p 8081 -s mitmdumpUrlSaver.py
__file__=mitmdumpUrlSaver.py
curScripPy=mitmdumpUrlSaver.py
not mac app: mitmdumpUrlSaver.py
appFolder=None
outputFoler=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump
cfgSaveFileFullPath=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdump_saved_url.txt
self.fileToSave=/Users/limao/dev/xxx/crawler/mitmdumpUrlSaver/electron-python-example/pymitmdump/mitmdump_saved_url_20200114_181553.txt
Loading script mitmdumpUrlSaver.py
Proxy server listening at http://*:8081

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

js

js

Electron集成js框架

想要把主流的js框架集成到Electron中，有如下选择：

react

electron-react-boilerplate/electron-react-boilerplate: A Foundation for Scalable Cross-Platform Apps

vue

SimulatedGREG/electron-vue: An Electron & Vue.js quick start boilerplate with vue-cli scaffolding, common Vue plugins, electron-packager/electron-builder, unit/e2e testing, vue-devtools, and webpack

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

Node

Node

Node，全称Node.js，是一个js的服务端框架。

而Electron是基于：

后端：Node

前端：Chromium

所以Electron开发期间会涉及到一些和Node有关的内容，整理如下。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

electron和node版本对应关系

Electron和Node版本对应关系

开发Electron期间，往往会涉及到，需要特定的版本的Node。

此处整理出Electron和Node版本对应关系：

常见版本对应关系

Electron 2.0.18 -> Node 8.17.0

Electron 3.0.0 -> Node 10.2.0

完全列表

详见官网

electron-releases - npm

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

NODE_MODULE_VERSION和node版本对应关系

NODE_MODULE_VERSION和node版本对应关系

折腾Electron期间，往往会遇到类似于

【未解决】Electron报错：Uncaught Error The module zeromq zmq.node was compiled against a different Node.js version

的问题，就会涉及到：NODE_MODULE_VERSION和node之间的版本对应关系。

此处总结如下：

| NodeJS版本 | 说明
NODE_MODULE_VERSION 79 | Node.js 13.x | Node.js 13.0.0 ~ Node.js 13.5.0
NODE_MODULE_VERSION 72 | Node.js 12.x | Node.js 12.0.0 ~ Node.js 12.14.0
NODE_MODULE_VERSION 75 | （据说是）Node.js 12.7.0 |
NODE_MODULE_VERSION 67 | Node.js 11.x | Node.js 11.0.0 ~ Node.js 11.15.0
NODE_MODULE_VERSION 64 | Node.js 10.x | Node.js 10.0.0 ~ Node.js 10.18.0
NODE_MODULE_VERSION 59 | Node.js 9.x | Node.js 9.0.0 ~ Node.js 9.11.2
NODE_MODULE_VERSION 57 | Node.js 8.x | Node.js 8.0.0 ~ Node.js 8.17.0
NODE_MODULE_VERSION 51 | Node.js 7.x | Node.js 7.0.0 ~ Node.js 7.10.1
NODE_MODULE_VERSION 48 | Node.js 6.x | Node.js 6.0.0 ~ Node.js 6.17.1
NODE_MODULE_VERSION 47 | Node.js 5.x | Node.js 5.0.0 ~ Node.js 5.12.0
NODE_MODULE_VERSION 46 | Node.js 4.x | Node.js 4.0.0 ~ Node.js 4.9.1

更多细节详见：

以往的版本 | Node.js

Node v12.7.0 (Current) | Node.js

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

附录

附录

下面列出相关参考资料。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

文档和教程

Electron文档和教程

此处整理Electron的相关资料和教程：

Electron官网有非常详尽的教程，需要查找某些技术细节时，推荐优先参考官网教程：

官网文档主入口

文档 | Electron

截图预览

所有文档 单页面 版本

所有的 Electron 的文档 | Electron

快速了解Electron所有API及效果

electron/electron-api-demos: Explore the Electron APIs

快速上手模板

electron/electron-quick-start: Clone to try a simple Electron app

Python打包

PyInstaller

如果涉及到Electron支持Python，则Electron打包期间也会涉及到Python的打包。

最常用的Python打包工具是：PyInstaller

其打包常用参数可以参考：

What to bundle, where to search

和运行时的路径有关的资料：

Run-time Information — PyInstaller 3.6 documentation

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44

参考资料

参考资料

【已解决】npm的electron的最新版本是多少

【已解决】electron-python中去打包Electron成独立的可执行程序

【已解决】electron打包npm run dist报错：signing Error Command failed codesign sign

【已解决】electron-builder打包报错：make-dir.js 86 catch SyntaxError Unexpected token {

【已解决】给Electron-Python打包出win的exe

【已解决】windows中用PyInstaller打包mitmdump的Python脚本为exe

【已解决】Windows10中重建NodeJS开发环境以运行和打包Electron-Python的项目为exe

【已解决】Windows中如何获取到Electron-builder打包后exe双击运行时的目录

【已解决】Electron的打包工具选择：electron-packager vs electron-builder vs electron-rebuild

【未解决】electron-builder打包Mac期间报错：skipped macOS application code signing reason cannot find valid Developer ID Application identity

【已解决】windows中electron-builder打包出免安装的可移动的单个exe

【已解决】js中用正则处理替换Electron打包后app的路径

【已解决】mac中用Image2icon制作要打包的app的图标

【无需解决】Mac中electron-rebuild失败：zeromq vendor napi.h error unknown type name napi_callback_scope

【已解决】Electron报错：Uncaught Error The module zeromq zmq.node was compiled against a different Node.js version

【已解决】electron-python-example运行后输入计算内容但是不显示结果

【已解决】mac中重新安装以降低node和npm版本

【已解决】electron和node版本对应关系

【未解决】Mac中electron打包后process.env是undefined获取不到有效值

【已解决】mac中npm安装electron速度太慢

【已解决】electron中js启动python调用mitmdump报错：mitmdump No such script

【已解决】electron-python中去打包Electron成独立的可执行程序

【记录】把Electron打包的app放到别的mac中测试

【已解决】打包后Electron路径中包含中文会无法启动mitmdump代理

【已解决】Mac中electron-python用electron-builder打包后app运行失败：Error Lost remote after 10000ms

【已解决】Mac中electron-builder打包的app运行Contents/MacOS下二进制没问题但是双击app运行却报错

【已解决】Mac中electron-builder打包后app运行报错：PermissionError Errno 13 Permission denied txt

【已解决】mitmdump被PyInstaller和electron-builder打包后功能不正常无法抓报到有效url链接地址

【已解决】mac中如何获取electron的app的根目录

【未解决】Electron打包双击app启动时python用正则处理从sys的argv中解析出app文件所在根目录路径

【已解决】electron中如何复制textarea中的内容

【已解决】Windows中写bat脚本方便清理内容和编译运行PyInstaller打包和Electron打包

【已解决】windows中一个bat脚本文件如何包含另一个bat脚本文件且即时报错也继续运行

【已解决】electron-python-example运行出错：Uncaught ReferenceError process is not defined

【未解决】mac中给Electron打包Windows的exe

【未解决】mac中用wine运行electron-builder打包的win的exe

【已解决】mac中wine中安装Windows的python 3

【已解决】Mac中运行跑通electron-python-example的示例

【已解决】Electron的打包工具选择：electron-packager vs electron-builder vs electron-rebuild

【已解决】mac中安装node和npm

【已解决】mac中npm install报错：make Release/obj.target/zmq/binding.o Error 1

【已解决】mac中electron-python用PyInstaller去打包可执行文件

【已解决】Electron-python中优化被调用python代码的print为输出日志到log文件

【未解决】js中child_process启动Python子进程无法获取到stderr的出错详细信息

【已解决】electron-python中zerorpc的stream持续输出无返回导致后续函数stopProxy无法调用

【已解决】Mac中把Electron的Python代码换成启动mitmdump的Python脚本

【已解决】electron通过js启动python但是实际上python代码print没输出后台mitmdump服务也没运行

【已解决】把electron的js端通过zerorpc调用python代码的逻辑优化为支持返回连续输出结果

【已解决】把electron的python中优化逻辑：增加检测和关闭mitmdump

【未解决】Mac中用Electron打包集成Python代码为GUI独立程序

【未解决】electron-python中去打包Electron成独立的可执行程序

【已解决】electron-builder打包配置中main.js的asar的asarUnpack不起作用

【已解决】electron的python的stream输出报错：zerorpc.exceptions.LostRemote Lost remote after 10s heartbeat

【已解决】Electron-python中优化被调用python代码的print为输出日志到log文件

【已解决】Mac中打包后的electron中如何让main.js中的console.log输出到Electron的devTool中的console的log中

【已解决】】Mac中electron-builder打包app运行报错：cant open file app/Contents/Resources app.asar Errno 20 Not a directory

【已解决】Python中如何获取Mac中electron-builder打包出的app通过双击启动时app文件所在的文件夹路径

【已解决】electron中实现html的文本区域界面用于显示js中获取到的字符串

【已解决】mac打包Electron出app运行报错：Error loading Python lib /private/var/folders .Python dlopen image not found

【已解决】electron-builder打包时如何设置参数把把额外的文件夹添加和打包进来

【已解决】mac中PyInstaller打包electron-python的单个主文件报错：OSError Python library not found libpython3.8m.dylib

【未解决】mac中PyInstaller打包python报错：TypeError an integer is required got type bytes

【已解决】Electron中main.js即js输出log日志文件到当前文件夹

【已解决】windows中启动Electron-python报错：Uncaught Error A dynamic link library DLL initialization routine failed zerorpc zeromq zmq.node

【无需解决】win10中调试运行本地electron期间如何清除缓存让最新代码生效

【已解决】Mac中electron-builder打包后app运行报错：PermissionError Errno 13 Permission denied txt

【已解决】mac中如何获取electron的app的根目录

【已解决】windows中打包Electron的mitmdumpUrlSaver为exe但运行没有在当前目录下生成txt文件

【已解决】electron通过js启动python但是实际上python代码print没输出后台mitmdump服务也没运行

【已解决】mac中Python的subprocess.Popen启动mitmdump报错：No such script

【已解决】electron-builder打包时如何设置参数把把额外的文件夹添加和打包进来

【已解决】electron-builder打包期间实现拷贝整个目录和其中二进制文件到打包后的特定目录

【已解决】打包后Electron路径中包含中文会无法启动mitmdump代理

【已解决】Electron打包双击app启动时python用正则处理从sys的argv中解析出app文件所在根目录路径

【已解决】PyInstaller打包electron-python后运行报错：路径问题和subprocess.Popen依赖调用python文件没被打包进来的问题

【已解决】mac中PyInstaller打包后的二进制文件在electron-builder打包后app中有些无法通过child_process的execFile运行

【已解决】NODE_MODULE_VERSION和node版本号对应关系

【已解决】node中是否有根据当前系统类型不同而不同的路径分隔符

【已解决】js中child_process的spawn启动python脚本无效实际上没运行

开发环境 | Electron

小公司小项目开发跨平台的桌面应用用什么编程语言比较好？ - 知乎

在 Visual Studio 2019 - .NET 框架中创建您的第一个 WPF 应用 | Microsoft Docs

C# WPF应用程序基础开发入门 | 小奋斗

This free brand-new digital design app wants to take on Adobe and Sketch - News - Digital Arts

[译] 零基础 macOS 应用开发（一） - 个人文章 - SegmentFault 思否

Mac开发——基础篇（swift桌面App）swift刘成利的博客-CSDN博客

Xcode - Apple Developer

macOS - Apple Developer

Electron 和当下其他的桌面开发方法相比如何？ - 知乎

打造你的第一个 Electron 应用 | Electron

Electron 应用架构 | Electron

electron/electron-api-demos: Explore the Electron APIs

Electron - 维基百科，自由的百科全书

python - How to configure ZeroRPC and timeouts - Stack Overflow

Path | Node.js v13.6.0 Documentation

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-08-09 10:12:44