python GUI库 EEL + VUE.js 开发环境配置 联调

 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.py


import 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 Python
      function say_hello_js(x) {
        const msg = "Hello from " + x
        document.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.js

eel.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 PyInstaller
    else:
        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_timeout
    root_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):
                continue

            try:
                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 valid
                        msg = "eel.expose() call contains '(' or '='"
                        assert rgx.findall(r'[\(=]', expose_call) == [], msg
                        expose_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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值