☰
ReactPy 组件开发指南:从 @component 装饰器到条件渲染的完整实战
2026/9/26 3:10:47 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】reactpy

It's React, but in Python

项目地址:https://gitcode.com/gh_mirrors/re/reactpy
点击查看免费下载

本篇技术指南围绕 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

项目地址:https://gitcode.com/gh_mirrors/re/reactpy
点击查看免费下载
上一篇:seomachine /research-serp 命令深度解析:从 SERP 深度分析到可执行内容简报的完整工作流
下一篇:免费在 Windows 上运行 iOS 应用:ipasim 模拟器 5 步上手指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询