?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