官方 Github: ChrisKnott/Eel。
Eel 是一个轻量的 Python 库,用于制作简单的类似于 Electron(但是比它更轻量) 的离线 HTML/JS GUI 应用程序,并具有对 Python 功能(capabilities)和库的完全访问权限。
Eel 托管一个本地 Web 服务器,然后允许您使用 Python 注释函数(annotate functions),以便可以从 JavaScript 调用它们,反之亦然。
Eel 旨在消除编写简短的 GUI 应用程序的麻烦。如果您熟悉 Python 和 Web 开发,则可以跳到 示例,该示例从给定文件夹中选择随机文件名(这在浏览器中是不可能的)。
1 使用方法
一个 Eel 应用程序将分为由各种Web技术文件(.html
, .js
, .css
)组成的前端和由各种 Python 脚本组成的后端。
所有前端文件都应放在一个目录中(如果需要,可以将它们进一步分成多个文件夹)。
my_python_script.py <-- Python scripts
other_python_module.py
static_web_folder/ <-- Web folder
main_page.html
css/
style.css
img/
logo.png
假设您将所有前端文件都放在一个名为 web
的目录中,包括起始页 index.html
,则该应用程序将按以下方式启动:
import eel
eel.init('web')
eel.start('index.html')
这将在默认设置(http://localhost:8000
)的地址上启动网络服务器,并打开浏览器到 http://localhost:8000/index.html。
如果安装了 Chrome 或 Chromium,则默认情况下,无论操作系统的默认浏览器设置为什么,它都将在“应用程序模式”(App Mode)下打开(带有 --app
的命令行标志)(可以覆写此行为)。
2 App 选项
可以将其他选项作为关键字参数传递给 eel.start()
。一些选项包括应用程序所处的模式(例如'chrome'),应用程序运行的端口,应用程序的主机名以及添加其他命令行标志。
从 Eel v0.12.0开始,以下选项可用于 start()
:
- mode, a string specifying what browser to use (e.g.
'chrome'
,'electron'
,'edge'
,'custom'
). Can also beNone
orFalse
to not open a window. Default:'chrome'
- host, a string specifying what hostname to use for the Bottle server. Default:
'localhost'
) - port, an int specifying what port to use for the Bottle server. Use
0
for port to be picked automatically. Default:8000
. - block, a bool saying whether or not the call to
start()
should block the calling thread. Default:True
- jinja_templates, a string specifying a folder to use for Jinja2 templates, e.g.
my_templates
. Default:None
- cmdline_args, a list of strings to pass to the command to start the browser. For example, we might add extra flags for Chrome;
eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog'])
. Default:[]
- size, a tuple of ints specifying the (width, height) of the main window in pixels Default:
None
- position, a tuple of ints specifying the (left, top) of the main window in pixels Default:
None
- geometry, a dictionary specifying the size and position for all windows. The keys should be the relative path of the page, and the values should be a dictionary of the form
{'size': (200, 100), 'position': (300, 50)}
. Default: {} - close_callback, a lambda or function that is called when a websocket to a window closes (i.e. when the user closes the window). It should take two arguments; a string which is the relative path of the page that just closed, and a list of other websockets that are still open. Default:
None
- app, an instance of Bottle which will be used rather than creating a fresh one. This can be used to install middleware on the
instance before starting eel, e.g. for session management, authentication, etc.
3 暴露函数
除了前端文件夹中的文件之外,还将在 /eel.js
中提供 JavaScript 库。您应该在任何页面中包括以下内容:
Python 代码中的任何函数可以用 @eel.expose
装饰:
@eel.expose
def my_python_function(a, b):
print(a, b, a + b)
而 Python 中的代码可以直接在 JavaScript 中像下面的方式调用:
console.log("Calling Python...");
eel.my_python_function(1, 2); // This calls the Python function that was decorated
类似的任何 JavaScript 函数也可以这样暴露:
eel.expose(my_javascript_function);
function my_javascript_function(a, b, c, d) {
if (a < b) {
console.log(c * d);
}
}
然后在 Python 中调用:
print('Calling Javascript...')
eel.my_javascript_function(1, 2, 3, 4) # This calls the Javascript function
exposed 的名称也可以通过传入第二个参数来覆盖。如果您的应用在构建过程中缩小了 JavaScript,则可能有必要确保在 Python 端可以解析函数:
eel.expose(someFunction, "my_javascript_function");
当将复杂对象作为参数传递时,请记住,在内部将它们转换为 JSON 并通过websocket 发送(该过程可能会丢失信息)。
下面看一个实例:
4 Hello World!
创建一个 HTML 文件 web/hello.html
:
Hello, World!
Hello, World!
和一个简单的 Python 脚本 hello.py
:
import eel
# Set web files folder and optionally specify which file types to check for eel.expose()
# *Default allowed_extensions are: ['.js', '.html', '.txt', '.htm', '.xhtml']
eel.init('web', allowed_extensions=['.js', '.html'])
@eel.expose # Expose this function to Javascript
def say_hello_py(x):
print('Hello from %s' % x)
say_hello_py('Python World!')
eel.say_hello_js('Python World!') # Call a Javascript function
eel.start('hello.html') # Start (this blocks and enters loop)
如果我们运行 Python 脚本(python hello.py
),则会打开一个浏览器窗口,显示hello.html
:
并且在终端将看到:
Hello from Python World!
Hello from Javascript World!
5 返回值
尽管我们想将代码视为由单个应用程序组成,但 Python 解释器和浏览器窗口在单独的进程中运行。这可能会使它们之间来回通信变得一团糟,尤其是当我们总是必须显式地将值从一侧发送到另一侧时。
Eel 支持从应用程序另一端检索返回值的两种方法,这有助于使代码简洁。
为了防止在 Python 端永久挂起,已设定尝试从 JavaScript 端检索值的超时时间,默认为 10000 毫秒(10秒)。可以通过 _js_result_timeout
参数更改为 eel.init
。
JavaScript 端没有相应的超时。
5.1 回调
调用 exposed 函数时,可以立即传递回调函数。当函数在另一侧执行完毕时,将自动使用返回值自动调用此回调。
例如,如果我们在 JavaScript 中定义并暴露了以下函数:
eel.expose(js_random);
function js_random() {
return Math.random();
}
然后在 Python 中,我们可以像这样从 JavaScript 端检索随机值:
def print_num(n):
print('Got this from Javascript:', n)
# Call Javascript function, and pass explicit callback function
eel.js_random()(print_num)
# Do the same with an inline lambda as callback
eel.js_random()(lambda n: print('Got this from Javascript:', n))
反过来也一样。
5.2 同步返回
在大多数情况下,对另一端的调用是为了快速检索某些数据,例如小部件的状态或输入字段的内容。在这些情况下,同步几毫秒然后继续执行代码比将整个过程分解成回调更方便。
要同步获取返回值,只需将任何内容都传递给第二组括号即可。因此,在 Python 中,我们将编写:
n = eel.js_random()() # This immediately returns the value
print('Got this from Javascript:', n)
您只能在浏览器窗口启动后(调用 eel.start()
之后)执行同步返回,否则显然会挂起调用。
在 JavaScript 中,该语言不允许我们在等待回调时进行阻塞,除非通过从 async
函数内部使用 await
。 因此,JavaScript 方面的等效代码为:
async function run() {
// Inside a function marked 'async' we can use the 'await' keyword.
// Must prefix call with 'await', otherwise it's the same syntax
let n = await eel.py_random()();
console.log("Got this from Python: " + n);
}
run();
6 异步 Python
Eel基于 Bottle 和 Gevent 构建,它们提供了类似于 JavaScript 的异步事件循环。许多Python的标准库都隐式地假设只有一个执行线程-为了处理这个问题,Gevent 使用“monkey patch”的许多标准模块,例如 time
。如果您需要猴子补丁,则应该 import gevent.monkey
并调用gevent.monkey.patch_all()
之后,再 import eel
。猴子修补会干扰调试器之类的东西,因此除非有必要,否则应避免。
在大多数情况下,应该避免使用 time.sleep()
而使用 gevent
提供的版本。为了方便起见,直接从 Eel 提供了两个最常用的 gevent 方法sleep()
和 spawn()
(同样也节省了导入 time
和/或 gevent
的时间)。
例如:
import eel
eel.init('web')
def my_other_thread():
while True:
print("I'm a thread")
eel.sleep(1.0) # Use eel.sleep(), not time.sleep()
eel.spawn(my_other_thread)
eel.start('main.html', block=False) # Don't block on this call
while True:
print("I'm a main loop")
eel.sleep(1.0) # Use eel.sleep(), not time.sleep()
然后,我们将运行三个“线程”(greenlets):
- Eel 的内部线程用于服务Web文件夹
-
my_other_thread
方法,重复打印 "I'm a thread" - 主 Python 线程(将停留在最终的
while
循环中)重复打印 "I'm a main loop"
7 打包二进制文件
如果你想让用户下载你的软件使用, 而用户没有安装python, 你最好将你的程序打包成二进制可执行文件, 那么最好使用 PyInstaller。
在你的 app 根目录下执行下面的命令:
python -m eel [your_main_script] [your_web_folder]
这将创建文件夹 dist/
,如果你想要创建单文件程序,你需要使用 --onefile
参数, 如果不想程序运行的时候有一个黑色命令窗口, 你可以使用 --noconsole
参数。也可以运行像 python -m eel file_access.py web --exclude win32com --exclude numpy --exclude cryptography
,排除一些包。更多信息见 PyInstaller 文档。
8 Microsoft Edge
对于 Windows 10 用户,默认情况下会安装 Microsoft Edge(eel.start(.., mode='edge')
),如果未安装首选浏览器,则会进行有用的回退。请参阅示例:
- A Hello World example using Microsoft Edge: examples/01 - hello_world-Edge/
- Example implementing browser-fallbacks: examples/07 - CreateReactApp/eel_CRA.py