eel开发环境启动的服务器默认端口是8000,如果前端界面的开发也是直接在EEL开发环境中进行,一切好办。但如果前端用vue,则需要另外启动专用的vue开发环境的服务器(Vue CLI (npm run serve
)默认端口是8080,Vite (npm run dev
)默认端口是5173)。
那怎么同步联调开发呢?
核心操作有两点:
1、python代码中的eel.start() 参数配置指定启动页为vue环境的入口页
2、vue页面中引入eel.js的时候,引用路径为eel环境的eel.js , 以及把websocket的host设为eel环境的host。
# main.pyimport eel@eel.expose
def say_hello_py(x):print("Hello from %s" % x)"""开发环境:
1、python开发环境的eel.start()参数:设置启动页面为vue开发环境的服务端口5173,
2、vue开发环境中的public/index.html里引用eel.js时,路径是引用python eel环境的eel.js
3、vue开发环境中的public/index.html里设置websocket的服务器为python eel所启动的那个服务器。生产环境:
和正常的一样使用
"""
def start_eel(environment):"""判断当前是开发环境还是生产环境,选择不同的eel.start()参数配置"""if environment == 'develop': # 开发环境directory = 'src' # 注意!这个值对应的是EEL服务器的文件夹,不是VUE服务器的文件夹app ='chrome'start_page = {'port': 5173} # 指向:http://localhost:5173/eel_kwargs = dict( # 设置 http://localhost:9000 为eel服务器mode=app,host="localhost",port=9000,)else: # 生产环境directory = 'web'app = 'chrome'start_page = 'index.html'eel_kwargs = dict(mode=app,port=0,size=(1280, 800),)eel.init(directory)eel.start(start_page, **eel_kwargs)if __name__ == "__main__":print("启动python...")start_eel('develop')
// vue 的 public/index.html<%if(process.env.NODE_ENV === 'production'){ %>
<script type="text/javascript" src="/eel.js"></script>
<%}else{%>
<script type=text/javascript src="http://localhost:9000/eel.js"></script>
<script>window.eel.set_host("ws://localhost:9000");
</script>
<%}%>
<!-- vue 中 public/index.html--><!DOCTYPE html>
<html><head><title>Hello, World!</title><script type=text/javascript src="http://localhost:9000/eel.js"></script>
<script>window.eel.set_host("ws://localhost:9000");
</script><script type="text/javascript">eel.expose(say_hello_js); // Expose this function to Pythonfunction say_hello_js(x) {const msg = "Hello from " + xdocument.getElementById("msgbox").innerHTML=msg;}eel.say_hello_py("Javascript World!"); // Call a Python function</script></head><body>Hello, World!<button onclick="eel.say_hello_py('Javascript Button!')">调用Python函数</button><p id="msgbox"></p><button onclick="say_hello_js('Javascript Button!')">调用JS函数</button></body>
</html>
==========
踩坑小记:
eel.init(directory)
当使用5173端口作前端服务时, eel.init(directory) 的directory 这个配置项对应的文件夹应该是VUE开发环境的本地文件夹。如果VUE开发环境不在本机上,你可以在本地构建一个文件夹,把需要用到的js函数的函数名放入这个文件夹中即可。
我一开始没有留意,结果是界面可以成功启动,界面启动过程没有报错,网页端调用python函数也成功,但python端调用js函数就报错提示:[AttributeError: module 'eel' has no attribute 'say_hello_js'] ,把eel.init(directory)的directory配置为vue服务的本地目录就成功了。
甚至你可以专门建一个目录,这个目录只存放一个文本文件,把所有暴露的js函数名以eel.expose(js_function_name) 的形式记录到一个文件中,并以.js为扩展名命名,也可以。
//expose_js_function_name.jseel.expose(say_hello_js);
eel.expose(my_js_function_1);
eel.expose(my_js_function_2);
eel.expose(my_js_function_3);
eel.expose(my_js_function_4);
跟踪了一下源代码,发现确实是通过遍历该文件夹及其子目录的全部指定扩展名的文件,并通过语法解析器 EXPOSED_JS_FUNCTIONS (基于PyParsing构建)进行匹配。
EXPOSED_JS_FUNCTIONS的解释规则是:用正则表达式匹配,解析得到函数名,这些函数名被存储在js_functions这个集合中。
得到这些js函数名后,通过_mock_js_function() 构建同名函数,构建的这个函数对于eel这个类来说是全局函数,所以对于main.py来说,就是【eel.同名函数】,就可以通过eel.js_function_name() 调用了。
# 如果程序未被PyInstaller打包成exe,则返回path的绝对路径,否则exe创建的临时资源目录_MEIPASS
def _get_real_path(path: str) -> str:if getattr(sys, 'frozen', False):return os.path.join(sys._MEIPASS, path) # type: ignore # sys._MEIPASS is dynamically added by PyInstallerelse:return os.path.abspath(path)'''
当你使用 PyInstaller 将脚本+资源打包成一个exe后。运行exe时,会动态创建一个临时目录(通常是在系统的临时文件夹中),并将可执行文件内部的所有资源解压到这个临时目录。sys._MEIPASS 就是这个临时目录的路径。
'''
def init(path: str, allowed_extensions: List[str] = ['.js', '.html', '.txt', '.htm','.xhtml', '.vue'], js_result_timeout: int = 10000) -> None:global root_path, _js_functions, _js_result_timeoutroot_path = _get_real_path(path)js_functions = set()for root, _, files in os.walk(root_path): # 遍历它的子目录for name in files:if not any(name.endswith(ext) for ext in allowed_extensions):continuetry:with open(os.path.join(root, name), encoding='utf-8') as file:contents = file.read()expose_calls = set()matches = EXPOSED_JS_FUNCTIONS.parseString(contents).asList() # 对文件进行解释,把【暴露给python的js函数】匹配出来。for expose_call in matches:# Verify that function name is validmsg = "eel.expose() call contains '(' or '='"assert rgx.findall(r'[\(=]', expose_call) == [], msgexpose_calls.add(expose_call) # 收集此文件的暴露函数js_functions.update(expose_calls) # 收集全部文件的暴露函数except UnicodeDecodeError:pass # Malformed file probably_js_functions = list(js_functions)for js_function in _js_functions:_mock_js_function(js_function) # 将找到的JS函数名称保存起来,并准备在 websocket 连接时使用_js_result_timeout = js_result_timeout
===============================================
对于eel.start() 参数配置中的start_page参数。
根据作者官方github上的资料,eel.start()的第一个参数是启动页的html文件名(入口页面),是字符串。为什么可以接收一个dict变量{'port':5173}呢?
追踪了一下源代码,发现其值为dict类型时,可以支持的参数包含了协议scheme 、域host 、端口port 、路径path 这几个参数
其值为字符串时,字符串应该为base_url 之后的访问路径。
代码追踪:def start(*start_urls: str, **kwargs: Any) --> show(*start_urls) -->brw.open(list(start_urls), _start_args) --> open(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) --> _build_urls(start_pages: Iterable[Union[str, Dict[str, str]]], options: OptionsDictT) -->_build_url_from_dict(page, options)
def _build_url_from_dict(page: Dict[str, str], options: OptionsDictT) -> str:scheme = page.get('scheme', 'http')host = page.get('host', 'localhost')port = page.get('port', options["port"])path = page.get('path', '')if not isinstance(port, (int, str)):raise TypeError("'port' option must be an integer")return '%s://%s:%d/%s' % (scheme, host, int(port), path)def _build_url_from_string(page: str, options: OptionsDictT) -> str:if not isinstance(options['port'], (int, str)):raise TypeError("'port' option must be an integer")base_url = 'http://%s:%d/' % (options['host'], int(options['port']))return base_url + page