- 前端
- UI组件
【免费下载链接】reactpy
It's React, but in Python
本篇技术指南围绕 ReactPy(Python 中的 React)组件体系的入门核心展开:如何用@component装饰器把普通的 Python 函数变成可复用的 UI 组件、如何嵌套组合组件、为什么组件必须返回"单一根元素"以及如何用 fragment 优雅绕开多余的div,最后通过参数化与条件渲染完成第一个真正可维护的 Todo 应用。读完本文,你将掌握 ReactPy 组件从定义、组合到逻辑分支的完整写法,并理解其底层 VDOM 约束(源码依据见 component.py 与 _html.py)。
为什么需要组件:从 HTML 元素到可复用单元
在 ReactPy 中,你可以用html模块里的函数(如html.h1、html.ul、html.li)直接构造出结构化的 HTML 文档,相关入门内容可参见 HTML With ReactPy。但当页面越来越大、越来越复杂时,直接操纵这些细粒度的小元素会变得难以维护——比如同一个图片卡片要在多个地方重复出现,每次都把src、style、alt写一遍显然不可取。
ReactPy 给出的答案是组件(Component):把若干元素分组封装成一个单元,然后在应用的任何地方反复复用。这与 React 前端的组件思想一脉相承——组件是"带名字的 UI 片段",是构建复杂界面时进行抽象和复用的基本单位。
定义组件:@component 装饰器与 render function
组件本质上就是返回 HTML 的普通 Python 函数。要把它变成组件,只需给函数加上@component装饰器:
from reactpy import component, html, run @component def Photo(): return html.img( { "src": "https://picsum.photos/id/237/500/300", "style": {"width": "50%"}, "alt": "Puppy", } ) run(Photo)(完整可运行示例见 simple_photo.py)
这里有几个要点:
- 被
@component装饰的函数称为render function(渲染函数),它描述了组件在任意时刻应该长什么样; - 按惯例,render function 用CamelCase(驼峰式)命名,即像类名一样,如
Photo、Gallery、TodoList,以区别于普通函数; - 组件内部通过
html.img({...})返回元素——注意属性的写法是字典,且键名使用snake_case(如class_name、margin_left),这是 ReactPy 与原生 HTML 的重要差异,完整属性对照见 HTML Attributes。
忘加装饰器的后果
如果Photo的渲染函数忘了加@component装饰器,会发生什么?服务器仍能正常启动,但一旦访问页面就会显示空白,服务端日志会报出如下错误:
TypeError: Expected a ComponentType, not dict.原因在于:不加装饰器时Photo()直接返回的是一个 VDOM 字典(dict),而 ReactPy 的布局系统期望接收的是"组件类型"(ComponentType)。装饰器正是把普通函数包装成组件构造器的关键。
源码视角:装饰器内部做了什么
从源码看,@component的实现位于 component.py:
def component(function): sig = inspect.signature(function) if "key" in sig.parameters and sig.parameters["key"].kind in ( inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, ): msg = f"Component render function {function} uses reserved parameter 'key'" raise TypeError(msg) @wraps(function) def constructor(*args, key=None, **kwargs): return Component(function, key, args, kwargs, sig) return constructor可以看出:
- 装饰器保留了原函数的签名(
inspect.signature),并返回一个新的constructor,调用时把参数打包进Component实例; key是保留参数:如果渲染函数把key声明为普通参数,会直接抛出TypeError,因为key专门用于列表渲染时的元素标识(详见 ReactPy 的 key 机制说明);- 这也解释了为什么"组件可以像普通函数一样接收位置参数和关键字参数"——因为包装层只是原样透传了
args与kwargs。
使用组件:嵌套与组合
定义好Photo后,它就能被嵌套进其他组件里。比如定义一个"父组件"Gallery,一次性返回多个Profile(此处用Photo复用三次)组件:
from reactpy import component, html, run @component def Photo(): return html.img( { "src": "https://picsum.photos/id/274/500/300", "style": {"width": "30%"}, "alt": "Ray Charles", } ) @component def Gallery(): return html.section( html.h1("Famous Musicians"), Photo(), Photo(), Photo(), ) run(Gallery)(完整示例见 nested_photos.py)
这正是组件的核心价值所在:定义一次,随处可用。Gallery内部把标准 HTML 元素(section、h1)与自定义组件(Photo)混排在一起,二者在调用语法上几乎无差别——这是组件抽象能够无痛组合的关键。
返回单一根元素:div 还是 fragment
组件必须返回"单一根元素"(single root element)。这个根元素可以带任意多的子元素,但不能直接返回一个元素列表并期望它被正确渲染。
下面这种写法就是错误的——返回了多个并列元素:
@component def MyTodoList(): return ( html.h1("My Todo List"), html.img({"src": "https://picsum.photos/id/0/500/300"}), html.ul(html.li("The first thing I need to do is...")), )方案一:用 div 包裹
想返回多个元素时,把它们包进一个html.div即可:
from reactpy import component, html, run @component def MyTodoList(): return html.div( html.h1("My Todo List"), html.img({"src": "https://picsum.photos/id/0/500/300"}), html.ul(html.li("The first thing I need to do is...")), ) run(MyTodoList)(完整示例见 wrap_in_div.py)
方案二:用 fragment(html._)不留下痕迹
如果你不想为了分组而引入一个多余的div(它会在最终 DOM 中留下一个无意义的容器节点),可以使用fragment——即html._函数:
from reactpy import component, html, run @component def MyTodoList(): return html._( html.h1("My Todo List"), html.img({"src": "https://picsum.photos/id/0/500/200"}), html.ul(html.li("The first thing I need to do is...")), ) run(MyTodoList)(完整示例见 wrap_in_fragment.py)
fragment 允许你把元素分组,却在 UI 上不留任何痕迹。例如下面这段 ReactPy 代码:
from reactpy import html html.ul( html._( html.li("Group 1 Item 1"), html.li("Group 1 Item 2"), html.li("Group 1 Item 3"), ), html._( html.li("Group 2 Item 1"), html.li("Group 2 Item 2"), html.li("Group 2 Item 3"), ) )渲染出的 HTML 与直接写六个li完全等价,两个 fragment 都不会产生任何额外节点:
<ul> <li>Group 1 Item 1</li> <li>Group 1 Item 2</li> <li>Group 1 Item 3</li> <li>Group 2 Item 1</li> <li>Group 2 Item 2</li> <li>Group 2 Item 3</li> </ul>源码视角:fragment 的底层约束
从源码看,fragment 的构造器实现在 _html.py:
def _fragment(attributes, children, event_handlers): """An HTML fragment - this element will not appear in the DOM""" if any(k != "key" for k in attributes) or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) model = VdomDict(tagName="") ...两个值得注意的事实:
- fragment 生成的 VDOM 其
tagName为空字符串,因此渲染时不产生任何 DOM 标签; - fragment不允许携带除
key之外的任何属性,也不能绑定事件处理器,否则直接抛TypeError——它只负责"分组"这一件事。
html._这个名字来自HtmlConstructor.__call__,它被显式指向 fragment 构造器(见 _html.py 中"fragment": Vdom("", custom_constructor=_fragment)与__call__ = __cache__["fragment"].__call__),同时HtmlConstructor.__getattr__会动态生成所有标准 HTML 元素(div、ul、li等),并把下划线转换为连字符,因此html._既是一个"调用即 fragment"的特殊入口,也是整个html命名空间的分发枢纽。
参数化组件:让父组件向子组件传数据
由于组件就是普通函数,你可以给它添加参数,让父组件把信息传给子组件。注意与标准 HTML 元素的区别:HTML 元素用字典传属性,而自定义组件像普通函数一样用位置参数和关键字参数传值:
from reactpy import component, html, run @component def Photo(alt_text, image_id): return html.img( { "src": f"https://picsum.photos/id/{image_id}/500/200", "style": {"width": "50%"}, "alt": alt_text, } ) @component def Gallery(): return html.section( html.h1("Photo Gallery"), Photo("Landscape", image_id=830), Photo("City", image_id=274), Photo("Puppy", image_id=237), ) run(Gallery)(完整示例见 parametrized_photos.py)
这里Photo("Landscape", image_id=830)同时演示了两种传参方式:
"Landscape"是位置参数,对应alt_text;image_id=830是关键字参数,对应image_id。
同一个Photo组件通过不同的参数组合,渲染出三张完全不同的图片——这正是"组件是函数"这一设计带来的灵活性。回顾 component.py 的constructor(*args, key=None, **kwargs),可以发现参数透传正是由装饰器包装层完成的:任何普通函数的参数约定(默认值、关键字传参、*args/**kwargs)在组件上都同样适用。
条件渲染:if/else 与内联 if
实际组件经常需要根据条件显示不同的内容。考虑一个基本的 Todo 列表,其中部分条目已完成:
from reactpy import component, html, run @component def Item(name, done): return html.li(name) @component def TodoList(): return html.section( html.h1("My Todo List"), html.ul( Item("Find a cool problem to solve", done=True), Item("Build an app to solve it", done=True), Item("Share that app with the world!", done=False), ), ) run(TodoList)(完整示例见 todo_list.py)
注意这里的Item组件并不根据done改变外观——它总是渲染成一样的li。如果我们想给done=True的条目加上一个 ✔ 标记呢?
方式一:if/else 分支返回不同元素
最直接的想法是写一个if语句,根据done返回不同的li:
@component def Item(name, done): if done: return html.li(name, " ✔") else: return html.li(name)(完整示例见 bad_conditional_todo_list.py)
这样确实达成了目标!但仔细观察会发现html.li(name, " ✔")与html.li(name)高度相似——虽然在这个例子里危害不大,但当分支逻辑变多时,代码会迅速变得臃肿、难以阅读和维护。
方式二:内联 if(三元表达式)
更优雅的做法是用内联 if(inlineif,即 Python 三元表达式)来收敛差异:
@component def Item(name, done): return html.li(name, " ✔" if done else "")(完整示例见 good_conditional_todo_list.py)
两种写法渲染结果完全一致,但后者把"两个几乎相同的分支"压缩成一行表达式:done为真时追加" ✔",为假时追加空字符串。对于这种"仅在局部细节上不同"的条件渲染,内联 if 是更易读、更易维护的选择;而当分支之间差异较大(返回完全不同的元素结构)时,使用完整的if/else反而更清晰。二者都是 ReactPy 支持的标准写法,没有性能差异——选择标准只有一个:可读性。
运行组件
所有示例末尾都调用了run(ComponentName),它会以当前组件为根启动一个开发服务器并在浏览器中打开页面。具体的启动方式、端口配置以及如何与 FastAPI、Flask、Sanic、Starlette、Tornado 等框架集成,参见 Running ReactPy;run、component、html等 API 均由 reactpy 包入口 统一导出,可直接from reactpy import component, html, run导入使用。
小结与延伸阅读
至此你已经走完 ReactPy 组件的完整入门闭环:
| 主题 | 关键结论 |
|---|---|
| 定义组件 | 给返回 HTML 的函数加@component,命名用 CamelCase |
| 组件约束 | 必须返回单一根元素;忘记装饰器会报TypeError: Expected a ComponentType, not dict. |
| 多元素返回 | 用html.div包裹,或用html._fragment 不留 DOM 痕迹 |
| 参数化 | 组件像普通函数一样支持位置/关键字参数,父组件可自由传值 |
| 条件渲染 | if/else与内联 if 皆可,按可读性取舍 |
下一步可以继续探索:
- 组件内部的状态管理:如何在组件里维护可变状态,参见 components-with-state;
- 事件处理:如何让组件响应用户点击等交互,参见 responding-to-events;
- 理解 ReactPy 如何用 VDOM 字典表示 HTML,参见 representing-html 与 The Rendering Process。
- 前端
- UI组件
【免费下载链接】reactpy
It's React, but in Python
相关推荐
FTXUI 组件(component)模块完全指南:从交互组件、装饰器到容器布局
FTXUI 组件(component)模块完全指南:从交互组件、装饰器到容器布局 ftxui::component 模块是 FTXUI(C++ Function
UI组件OptiScaler 实战手册:按玩家场景配 DLSS 替代方案,上采样与帧率提升一次搞懂
OptiScaler 实战手册:按玩家场景配 DLSS 替代方案,上采样与帧率提升一次搞懂 装了 OptiScaler 之后,我做得最多的动作就是在游戏里按 I
图形学游戏开发Gutenberg SlotFill 扩展开发指南:从 Plugin 注册到条件渲染的完整实践
Gutenberg SlotFill 扩展开发指南:从 Plugin 注册到条件渲染的完整实践 SlotFill 是 Gutenberg(WordPress 块
后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考