Flet-Python全栈框架剖析

Xsens动作捕捉 2023-04-13 5658


Flet-Python全栈框架剖析  第1张


前后端分离架构

在Flet架构中,前端可以是基于浏览器的Web,或者windows桌面端exe程序,或者移动端的apk等等,由于前后端代码都写到一起的,要去理解哪些是前端代码,哪些是后端代码就很费解了。

我们先分析一个简单的页面如下,页面显示了服务端的ini配置文件的值。左边是ini文件列表,下一级菜单是sections,右边是每个section下面的配置名称和值,可以修改保存。

Flet-Python全栈框架剖析  第2张


暂时不涉及复杂的交互,按照目前的前后端分离的架构,前端进行页面渲染,然后向后端去请求数据并动态响应展示。保存也通过接口传递给后端,修改ini文件。


Flet-Python全栈框架剖析  第3张

前后端分离

如上图所示,分离架构里面,服务端主要提供RestFul接口服务,只需要提供数据查询和更新即可,不用关注用户动作和呈现,前端去定义数据展示形态,实现用户查询动作和更新动作的捕获,并将请求发送到后端,根据后端范围的数据动态渲染。

Flet的前后端代码合一

Flet比较神奇,比方说前面例子中的Web服务,页面的展示、动作定义和数据获取都在Python中实现的,感觉好像是Python的代码Run起来之后,Server端也把前端的事情也干了。

我贴一下代码来看看,首先是主入口代码,这里基本上就是定义主页面,启动一个flet服务。

import logging

from GTUtility.config.manager_ini import Inis

import flet as ft

from manager_flet.parse.parse_ini import IniControls

def main(page: ft.Page):

# page.title = f"base mode测试"

page.vertical_alignment = ft.MainAxisAlignment.START

page.theme_mode = ft.ThemeMode.LIGHT

logging.warning("页面启动开始")

config_data = Inis(conf=Params.conf_dir)

config_data.load()

columns = ft.Column(expand=True)

controls = IniControls(expand=8)

controls.load(config_data=config_data)

columns.controls.append(ft.Container(expand=1, content=ft.Tabs(expand=10, tabs=[

ft.Tab(text="INI配置", content=controls), ft.Tab(text="基础数据配置", content=ft.Text("第二个菜单预留"))

])))

page.controls.append(columns)

page.update()

logging.warning("页面启动完成")

if __name__ == __main__:

# upload_dir=open_ai_dir,

ft.app(name="index", port=38090, target=main, view=ft.WEB_BROWSER, web_renderer="html")


Inis类是我自己封装的加载和管理ini的数据处理对象; 而iniControls是定义的展示视图,是一个自定义Controls类,用来定义页面展示元素和事件响应。Inis类可以理解为纯后端数据处理的类,由于前后端开发时间比较长,用Flet的时候还是习惯将数据处理也页面处理分开

另外比较有意思的是main入库,需要有一个page,在flet中,不管是Web还是APP,page可以看做前端的页面主容器,页面的最终展示可以看成是通过page.update()来渲染的。官网与介绍,每个flet都有一个page,下面可以包含多个view,通过view的切换来完成视图的切换,呈现不同的前端页面。


Flet-Python全栈框架剖析  第4张


Flet定义前端展示效果和事件处理

不管是BS还是CS,用户层面上最重要的两个动作就是展示和事件响应。参见前面范例的代码,controls是自定义的ini展示组件,将这个组件append到系统的columns中,最终将columns添加到主页面page里面,在page里面展示,我们这个范例里面只有一个view。有前端基础的很容易理解这个处理。

columns = ft.Column(expand=True)

controls = IniControls(expand=8)

controls.load(config_data=config_data)

columns.controls.append(ft.Container(expand=1, content=ft.Tabs(expand=10, tabs=[

ft.Tab(text="INI配置", content=controls), ft.Tab(text="基础数据配置", content=ft.Text("第二个菜单预留"))

])))

page.controls.append(columns)

page.update()


再来深入看下自定义的控件实现,build函数是重写了父类的函数,看实现就知道主要是完成前端显示元素构建和布局设置。

class IniControls(ft.UserControl):

def get_on_change(self, key: str, cfg, save_button):

"""

生成事件处理函数

:param key:

:return:

"""

section, option = key.split(".")

# 定义根据事件更新的处理函数

def update(e):

cfg.set(section=section, option=option, value=e.data)

# 有数据更新,保存按钮生效

save_button.disabled = False

save_button.update()

def _save_cfg(self, e, file_name):

try:

self.config_data.cfg_ini[file_name].save()

except Exception as esave:

logging.error(f"保存异常:{esave}")

else:

logging.warning(f"{file_name}更新保存")

e.control.disabled = True

e.control.update()

def _build_section(self, columns_tree, columns_content, file_name, section, cfg: ConfigParser):

def click_on_section(e):

columns_content.controls.clear()

save_button = ft.TextButton(text="保存", on_click=lambda e: self._save_cfg(e, file_name), disabled=True)

# 增加保存按钮

columns_content.controls.append(ft.Container(content=save_button))

# 展开

for option in cfg.options(section=section):

self._build_cfg_param(columns_content, section, option, cfg, save_button)

columns_content.update()

section_control = get_list_title_for_expand(

label=section,

on_click=click_on_section,

init_expand_status=False,

text_css={"width": 180}, level=2

)

section_control.visible = False

columns_tree.controls.append(section_control)

return section_control

def build(self):

rows = ft.Row(vertical_alignment=ft.CrossAxisAlignment.START, expand=True)

columns_tree = ft.Column(scroll=ft.ScrollMode.AUTO)

columns_content = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True)

# 按照文件遍历构造

for file_name, cfg in self.config_data.cfg_ini.items():

self._build_cfg(columns_tree, columns_content, file_name, cfg.cfg)

columns_tree.scroll = ft.ScrollMode.ALWAYS

columns_content.scroll = ft.ScrollMode.ALWAYS

rows.controls.append(ft.Container(content=columns_tree))

# 间隔

rows.controls.append(ft.VerticalDivider(width=5, color=ft.colors.BLUE, thickness=3))

rows.controls.append(ft.Container(content=columns_content))

return ft.Container(content=rows)

其他的内容我们暂时不关注,先重点看看保存按钮,为每个保存按钮添加self._save_cfg点击处理函数,有点像一个回调函数,这就是一个前端页面动作响应。

save_button = ft.TextButton(text="保存", on_click=lambda e: self._save_cfg(e, file_name), disabled=True)

# 增加保存按钮

columns_content.controls.append(ft.Container(content=save_button))

# 展开

for option in cfg.options(section=section):

self._build_cfg_param(columns_content, section, option, cfg, save_button)

而保存动作是保存到后端,看下self._save_config函数。我们save到后端,同时对控件状态进行了更新,e.control.disabled的目的是保存之后设置按钮状态并更新页面。

    def _save_cfg(self, e, file_name):

try:

self.config_data.cfg_ini[file_name].save()

except Exception as esave:

logging.error(f"保存异常:{esave}")

else:

logging.warning(f"{file_name}更新保存")

e.control.disabled = True

e.control.update()

按照flet的思路,我们根据web的一个案例进行了分析,这个写法确实没有前后端的概念了这是因为Flet是SDUI架构,官网也进行了阐述,我们下一小节来描述。

Flet的SDUI架构


参见官网
https://flet.dev/docs/guides/python/mobile-support

SDUI架构全称是Server-driven UI,简单理解就是服务端驱动UI,如下图,在我们的Web应用中,User program和Flet server都运行在Server端,Flet client可以是Web、exe或者apk应用。

我们的所有Python程序构成User program模块,完成业务流程, 与Flet client的交互通过Flet框架中的Flet server,这部分是由go实现。


Flet-Python全栈框架剖析  第5张


每个模块详细说明如下:

  • User program:用户面程序,支持后端数据处理、Flet前端页面控件定义、事件定义以及页面刷新等等所有业务相关处理
  • Flet server:传递前端(web或者apk等)事件以及后端处理结果,直接驱动用户页面展示,通过websockets与连接通信
  • Flet client:浏览器或者apk等等,完成用户界面展示和动作捕获,并与Flet server交互

可以看出来SDUI架构,动作响应和页面刷新都需要由Server端驱动,Server端需要干很多以前前端干的是事情,websockets连接对资源消耗也更大,所以Flet引入了Flet Server并用go来实现作为底层通道,解决Python性能较差的问题,但Flet仍然存在两个弊端

  • 前后端除了数据交互,还有前端行为和响应的交互,会增大交互时延,可能影响用户操作体验
  • 每个客户端都会开辟Websockets通道,增大长连接的资源占用
  • 对于本机场景,或者使用Flet做客户端,对接其他HTTP Server场景,也需要部署Flet Server?


Flet 纯客户端场景

在官方博客<Flet mobile update>
https://flet.dev/blog/flet-mobile-update 中,官方介绍了Flet新的桌面架构,去掉了Go的Flet Server(Fletd),这部分封装到Python里面,与Flet Client通信也去掉websocket,windows上采用tcp, linux和mac等系统采用unix pipes。

因为是单客户端场景,并发少,缩短交互路径能够减少时延,提升响应速度。


Flet-Python全栈框架剖析  第6张


如上图,该场景下我称之为纯客户端场景,用Python+Flutter去驱动Flet Client,如果要跟服务端通信,可以像前端一样,通过requests等库与第三方服务端通信,基于HTTP。

移动端APP也是类似的,只是按照官方所述,需要注意Python程序需要引入比较纯粹的python,或者必须支持在ARM64架构上编译。


Flet-Python全栈框架剖析  第7张

The End