Django组件,知道如何渲染自身。
项目描述
Laces
Django组件,知道如何渲染自身。
Laces组件提供了一种简单的方法来组合数据(以Python对象的形式)与旨在渲染数据的Django模板。然后可以使用{% component %}
模板标签在任意模板中简单渲染组件。父模板不需要了解组件的模板或数据。无需接收、过滤、重新结构化或传递任何数据到组件的模板。只需让组件自行渲染即可。
模板和数据在组件中绑定(抱歉,但就是这样 😅),并且可以一起传递。当组件嵌套时,这尤其有用——它允许我们避免在数据中再次构建相同的嵌套结构(一次在数据中,一次在模板中)。
在复杂的Django应用程序中,与能够将自身渲染为HTML元素的对象一起工作是常见的模式,例如在Wagtail管理界面中。Wagtail管理界面也是之前发现、开发和巩固此包中提供的API的地方。此包的目的是将这些工具提供给Wagtail生态系统之外的其它Django项目。
链接
入门
安装
首先,使用pip安装
$ python -m pip install laces
然后,将其添加到已安装的应用程序中
# settings.py
INSTALLED_APPS = ["laces", ...]
就是这样。
创建组件
创建组件的最简单方法是定义一个laces.components.Component
的子类,并在此类上指定一个template_name
属性。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
{# my_app/templates/my_app/components/welcome.html #}
<h1>Hello World!</h1>
有了上述内容,然后您就可以实例化组件(例如,在视图中),并将其传递到另一个模板以进行渲染。
# my_app/views.py
from django.shortcuts import render
from my_app.components import WelcomePanel
def home(request):
welcome = WelcomePanel() # <-- Instantiates the component
return render(
request,
"my_app/home.html",
{"welcome": welcome}, # <-- Passes the component to the view template
)
在视图模板中,我们加载laces
标签库,并使用{% component %}
标签来渲染组件。
{# my_app/templates/my_app/home.html #}
{% load laces %}
{% component welcome %} {# <-- Renders the component #}
就是这样!组件的模板将在视图模板中直接渲染。
当然,这是一个非常简单的例子,并不比使用简单的include
更有用。我们将在下面介绍一些更有用的用例。
无模板
在我们深入研究组件用例之前,先简单说明一下,组件不必有模板。对于不需要模板的简单情况,可以覆盖render_html
方法。如果返回值包含HTML,应使用django.utils.html.format_html
或django.utils.safestring.mark_safe
将其标记为安全。
# my_app/components.py
from django.utils.html import format_html
from laces.components import Component
class WelcomePanel(Component):
def render_html(self, parent_context):
return format_html("<h1>Hello World!</h1>")
将上下文传递到组件模板
现在回到带有模板的组件。
上面显示的示例中,模板中的静态欢迎消息当然并不很有用。这似乎更像是一种复杂的替换简单include
的方法。
但,我们很少想要渲染具有静态内容的模板。通常,我们希望将一些上下文变量传递给要渲染的模板。这正是组件开始变得有趣的地方。
render_html
的默认实现调用组件的get_context_data
方法以获取传递给模板的上下文变量。默认的get_context_data
实现返回一个空字典。要自定义传递给模板的上下文变量,可以覆盖get_context_data
。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
def get_context_data(self, parent_context):
return {"name": "Alice"}
{# my_app/templates/my_app/components/welcome.html #}
<h1>Hello {{ name }}</h1>
有了上述内容,我们现在正在渲染一个带有来自组件的get_context_data
方法的名称的欢迎消息。不错。但,仍然不太有用,因为名称仍然是硬编码的——在组件方法中而不是在模板中,但仍然是硬编码的。
使用类属性
在考虑如何使组件的上下文更有用时,记住组件只是普通的Python类和对象是很有帮助的。因此,您基本上可以以任何您喜欢的方式将上下文数据放入组件中。
例如,我们可以向构造函数传递参数,并在组件的方法中使用它们,如get_context_data
。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
def __init__(self, name):
self.name = name
def get_context_data(self, parent_context):
return {"name": self.name}
不错,这变得越来越好了。现在我们可以将名称传递给组件,当实例化它时,并将准备好的组件传递到视图模板中。
# my_app/views.py
from django.shortcuts import render
from my_app.components import WelcomePanel
def home(request):
welcome = WelcomePanel(name="Alice")
return render(
request,
"my_app/home.html",
{"welcome": welcome},
)
所以,正如之前提到的,我们可以使用Python类和对象的全功能来为我们的组件提供上下文数据。下面可以找到一些组件如何使用的更多示例。
使用父上下文
在上面的示例中,您可能已经注意到render_html
和get_context_data
方法都接收一个parent_context
参数。这是调用组件的模板的上下文。通过{% component %}
模板标签,parent_context
被传递给render_html
方法。在render_html
方法的默认实现中,parent_context
随后被传递给get_context_data
方法。但是,get_context_data
方法的默认实现忽略了parent_context
参数,并返回一个空字典。为了使用它,您需要重写get_context_data
方法。
依赖于父上下文中的数据在一定程度上牺牲了组件的一些好处,即将数据和模板绑定在一起。特别是对于嵌套使用组件的情况,您现在需要将正确格式的数据再次传递给模板的所有层。通常,直接将组件所需的所有数据提供给组件本身会更干净。
然而,可能存在一些情况下这是不可能或不希望的。对于这些情况,您可以在组件的get_context_data
方法中访问父上下文。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
def get_context_data(self, parent_context):
return {"name": parent_context["request"].user.first_name}
(当然,这也可以通过在视图中将请求或用户对象传递给组件来实现,但这只是一个示例。)
在其他模板中使用组件
如第一个示例中所述,组件是通过laces
标签库中的{% component %}
标签在其他模板中渲染的。
以下是上面的示例,其中视图将WelcomePanel
的实例传递给my_app/home.html
的上下文。
# my_app/views.py
from django.shortcuts import render
from my_app.components import WelcomePanel
def home(request):
welcome = WelcomePanel()
return render(
request,
"my_app/home.html",
{
"welcome": welcome,
},
)
然后,在my_app/templates/my_app/home.html
模板中,我们按照以下方式渲染欢迎面板组件
{# my_app/templates/my_app/home.html #}
{% load laces %}
{% component welcome %}
这是组件的基本用法,应该涵盖了大多数情况。
然而,{% component %}
标签也支持一些附加功能。具体来说,支持与{% include %}
标签类似的with
、only
和as
关键字。
使用with
提供额外的父上下文变量
您可以使用关键字with
将额外的父上下文变量传递给组件
{% component welcome with name=request.user.first_name %}
注意:这些额外变量将被添加到传递给组件的render_html
和get_context_data
方法的parent_context
中。默认的get_context_data
实现忽略了parent_context
参数,因此您需要重写它来使用这些额外变量。更多信息请参阅父上下文部分。
使用only
限制父上下文变量
要限制传递给组件的父上下文变量仅限于with
关键字提供的变量(而不是调用模板上下文中的其他变量),请使用only
{% component welcome with name=request.user.first_name only %}
注意:with
和only
都只影响传递给组件的render_html
和get_context_data
方法的parent_context
。它们对传递给组件模板的实际上下文没有直接影响。例如,如果组件的get_context_data
方法返回一个始终包含键foo
的字典,那么该键将在组件的模板中可用,无论是否使用了only
。
使用as
将渲染的输出存储在变量中
要存储组件的渲染输出而不是立即输出,请使用as
后跟变量名
{% component welcome as welcome_html %}
{{ welcome_html }}
向组件添加JavaScript和CSS资源
与Django表单小部件类似,组件可以指定相关的JavaScript和CSS资源。组件的资源可以通过与定义Django表单资源相同的方式指定。这可以通过内部Media
类或动态media
属性来实现。
内部 Media
类定义如下
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
class Media:
css = {"all": ("my_app/css/welcome-panel.css",)}
通过 media
属性实现的更动态的定义如下
# my_app/components.py
from django.forms import Media
from laces.components import Component
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
@property
def media(self):
return Media(css={"all": ("my_app/css/welcome-panel.css",)})
注意:输出组件上定义的任何媒体声明是模板的责任。
在模板中输出组件媒体
一旦通过上述两种方式中的一种在组件上定义了资产,您就可以在模板中输出它们。这同样与 Django 表单小部件的方式相同。组件实例将具有一个 media
属性,该属性返回一个 django.forms.Media
类的实例。即使您使用嵌套的 Media
类来定义资产,也是如此。Media
对象的字符串表示形式 是包含资产的 HTML 声明。
在上面的示例首页模板中,我们可以这样输出组件的媒体声明
{# my_app/templates/my_app/home.html #}
{% load laces %}
<head>
{{ welcome.media }}
<head>
<body>
{% component welcome %}
</body>
将媒体与 MediaContainer
结合使用
当页面上有多个组件时,分别输出每个组件的媒体声明可能会很麻烦。为了使这个过程更容易一些,Laces 提供了一个 MediaContainer
类。该 MediaContainer
类是 Python 内置的 list
类的子类,它将所有成员的 media
合并。
在视图中,我们可以创建一个包含多个定义媒体的组件的 MediaContainer
实例,并将其传递给视图模板。
# my_app/views.py
from django.shortcuts import render
from laces.components import MediaContainer
from my_app.components import (
Dashboard,
Footer,
Header,
Sidebar,
WelcomePanel,
)
def home(request):
components = MediaContainer(
[
Header(),
Sidebar(),
WelcomePanel(),
Dashboard(),
Footer(),
]
)
return render(
request,
"my_app/home.html",
{
"components": components,
},
)
然后,在视图模板中,我们可以一次性输出容器中所有组件的媒体声明。
{# my_app/templates/my_app/home.html #}
{% load laces %}
<head>
{{ components.media }}
<head>
<body>
{% for component in components %}
{% component component %}
{% endfor %}
</body>
这将输出容器中所有组件的合并媒体声明。媒体声明的组合遵循 Django 文档中概述的行为。
注意:MediaContainer
的使用不仅限于包含组件。它可以用来合并具有 media
属性的任何类型对象的 media
属性。
使用组件的模式
下面,我们将展示一些更多关于如何使用组件的示例,这些示例在上面的 “入门”部分 中没有涉及。
嵌套组件
当组件嵌套时,组件提供的数据和模板的组合变得特别有用。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component): ...
class Dashboard(Component):
template_name = "my_app/components/dashboard.html"
def __init__(self, user):
self.welcome = WelcomePanel(name=user.first_name)
...
def get_context_data(self, parent_context):
return {"welcome": self.welcome}
"父" 组件的模板不需要了解 "子" 组件的任何信息,除了哪个模板变量是组件。子组件已经包含了它需要的数据,并且知道使用哪个模板来渲染这些数据。
{# my_app/templates/my_app/components/dashboard.html #}
{% load laces %}
<div class="dashboard">
{% component welcome %}
...
</div>
嵌套还为我们提供了一个很好的数据结构,我们可以对其进行测试。
dashboard = Dashboard(user=request.user)
assert dashboard.welcome.name == request.user.first_name
嵌套组件组
组件的嵌套不仅限于单个实例。我们也可以嵌套组件组。
# my_app/components.py
from laces.components import Component
class WelcomePanel(Component): ...
class UsagePanel(Component): ...
class TeamPanel(Component): ...
class Dashboard(Component):
template_name = "my_app/components/dashboard.html"
def __init__(self, user):
self.panels = [
WelcomePanel(name=user.first_name),
UsagePanel(user=user),
TeamPanel(groups=user.groups.all()),
]
...
def get_context_data(self, parent_context):
return {"panels": self.panels}
{# my_app/templates/my_app/components/dashboard.html #}
{% load laces %}
<div class="dashboard">
{% for panel in panels %}
{% component panel %}
{% endfor %}
...
</div>
容器组件
上面的示例相对静态。该 Dashboard
组件始终包含相同的面板。
您也可以想象通过构造函数传递子组件。这将使您的组件成为动态容器组件。
# my_app/components.py
from laces.components import Component
class Section(Component):
template_name = "my_app/components/section.html"
def __init__(self, children: list[Component]):
self.children = children
...
def get_context_data(self, parent_context):
return {"children": self.children}
class Heading(Component): ...
class Paragraph(Component): ...
class Image(Component): ...
{# my_app/templates/my_app/components/section.html #}
{% load laces %}
<section>
{% for child in children %}
{% component child %}
{% endfor %}
</section>
上面的 Section
组件可以接受任何类型的子组件。唯一的要求是,子组件可以使用 {% component %}
标签进行渲染(所有组件都这样做)。
在视图中,我们现在可以使用任何我们想要的子组件实例化 Section
组件。
# my_app/views.py
from django.shortcuts import render
from my_app.components import (
Heading,
Image,
Paragraph,
Section,
)
def home(request):
content = Section(
children=[
Heading(...),
Paragraph(...),
Image(...),
]
)
return render(
request,
"my_app/home.html",
{"content": content},
)
{# my_app/templates/my_app/home.html #}
{% load laces %}
<body>
{% component content %}
...
</body>
使用数据类
上面,我们展示了如何使用 类属性 向组件上下文中添加数据。这是一个非常有用且常见的模式。然而,当您有多个属性并且直接将这些属性传递到模板上下文中时,它有点冗长。
为了使这个过程更方便,我们可以使用 dataclasses
。
# my_app/components.py
from dataclasses import dataclass, asdict
from laces.components import Component
@dataclass
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
name: str
def get_context_data(self, parent_context):
return asdict(self)
使用 dataclasses,我们可以在类定义中定义要传递给组件的属性名称和类型。然后,我们可以使用 asdict
函数将数据类实例转换为可以直接作为模板上下文的字典。
asdict
函数只为字典添加了在数据类中定义的属性键。在上面的例子中,asdict
返回的字典将只包含 name
键。它不会包含 template_name
键,因为这是在类中设置的值,但没有类型注解。如果您添加类型注解,那么 template_name
键也会包含在 asdict
返回的字典中。
自定义构造函数方法
当组件有许多属性时,单独传递每个属性给构造函数可能会很痛苦。当组件被用在很多地方,并且数据准备需要在每个用例中重复时,这一点尤其如此。自定义构造函数方法可以帮助解决这个问题。
在我们的 WelcomePanel
示例中,我们可能想显示更多的用户信息,包括用户头像和用户个人资料页面的链接。我们可以添加一个 classmethod
,它接受用户对象并返回组件实例,该实例包含渲染组件所需的所有数据。我们还可以使用此方法来封装生成附加数据(如个人资料URL)的逻辑。
# my_app/components.py
from django import urls
from dataclasses import dataclass, asdict
from laces.components import Component
@dataclass
class WelcomePanel(Component):
template_name = "my_app/components/welcome.html"
first_name: str
last_name: str
profile_url: str
profile_image_url: str
@classmethod
def from_user(cls, user):
profile_url = urls.reverse("profile", kwargs={"pk": user.pk})
return cls(
first_name=user.first_name,
last_name=user.last_name,
profile_url=profile_url,
profile_image_url=user.profile.image.url,
)
def get_context_data(self, parent_context):
return asdict(self)
现在,我们可以在视图中这样实例化组件
# my_app/views.py
from django.shortcuts import render
from my_app.components import WelcomePanel
def home(request):
welcome = WelcomePanel.from_user(request.user)
return render(
request,
"my_app/home.html",
{"welcome": welcome},
)
构造函数方法允许我们保持视图非常简单和整洁,因为所有数据准备都封装在组件中。
就像上面的例子一样,自定义构造函数方法与 dataclasses 的使用非常匹配,但当然也可以在不使用它们的情况下使用。
关于Laces和组件
为什么叫“Laces”?
“Laces”多少是对将数据和模板关联起来的功能的引用。组件也是“自渲染”的,这可以看作是“自给自足”,这又与“自举”相关。而且,“bootstraps”不就是长一点的“(鞋)带”吗?
最后,这也是对 @mixxorz 的出色 Slippers 包 的致敬,该包也以组件为中心的方法来改善使用 Django 模板时的体验,但方式相当不同。
支持的版本
- Python >= 3.8
- Django >= 3.2
贡献
安装
要修改此项目,首先克隆此存储库
$ git clone https://github.com/tbrlpld/laces.git
$ cd laces
激活您首选的虚拟环境后,安装开发依赖项
使用 pip
$ python -m pip install --upgrade pip>=21.3
$ python -m pip install -e '.[dev]' -U
使用 flit
$ python -m pip install flit
$ flit install
pre-commit
注意,此项目使用 pre-commit。它包含在项目测试需求中。要本地设置
# initialize pre-commit
$ pre-commit install
# Optional, run all checks once for this, then the checks will run only on the changed files
$ git ls-files --others --cached --exclude-standard | xargs pre-commit run --files
如何运行测试
现在您可以通过以下方式运行所有测试
$ tox
或者,您可以为特定环境运行测试
$ tox -e python3.11-django4.2
或者,只运行特定的测试
$ tox -e python3.11-django4.2 laces.tests.test_file.TestClass.test_method
要交互式地运行测试应用程序,使用
$ tox -e interactive
现在您可以通过 http://localhost:8020/
访问。
使用覆盖率测试
tox
已配置为使用覆盖率运行测试。覆盖报告是针对所有环境合并的。这是通过在 tox
中运行覆盖率时使用 --append
标志来实现的。这意味着它还会包括以前的结果。
您可以通过运行以下命令查看覆盖报告
$ coverage report
要获取干净的报告,您可以在运行 tox
之前运行 coverage erase
。
不使用 tox
运行测试
如果您想不使用 tox
运行测试,可以使用 testmanage.py
脚本。这个脚本是对 Django 的 manage.py
的包装,并将使用正确的设置运行测试。
为了使此工作,您需要安装 testing
依赖项。
$ python -m pip install -e '.[testing]' -U
然后您可以通过以下方式运行测试
$ ./testmanage.py test
要使用覆盖率运行测试,使用
$ coverage run ./testmanage.py test
Python版本管理
Tox将尝试在您的机器上查找已安装的Python版本。
如果您使用pyenv
来管理多个版本,您可以告诉tox
使用这些版本。为了确保tox
可以找到使用pyenv
安装的Python版本,您需要virtualenv-pyenv
(注意:这不同于pyenv-virtualenv
)。virtualenv-pyenv
是开发依赖项的一部分(就像tox
本身一样)。此外,您必须设置环境变量VIRTUALENV_DISCOVERY=pyenv
。
发布
本项目使用可信发布者模型进行PyPI发布。这意味着发布是通过GitHub Actions在GitHub上创建新版本时进行的。
要创建版本,您需要一个Git标签。该标签可以是命令行中创建并推送的,也可以在GitHub上的“创建版本”界面中创建。标签名称应该是版本号,前面加上一个v
(例如v0.1.0
)。
在发布新版本之前,请确保更新
CHANGELOG.md
中的更改日志,以及laces/__init__.py
中的版本号。
要手动测试发布包,您可以使用flit
。确保根据Flit的文档配置您的~/.pypirc
文件中的testpypi
存储库。如果您的PyPI账户使用2FA,您需要创建一个PyPI API令牌,并将其用作密码,将__token__
用作用户名。
准备好测试发布时,运行
$ flit build
$ flit publish --repository testpypi
项目详情
下载文件
下载适合您平台的文件。如果您不确定该选择哪个,请了解有关安装包的更多信息。
源代码分发
构建分发
laces-0.1.1.tar.gz的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | e45159c46f6adca33010d34e9af869e57201b70675c6dc088e919b16c89456a4 |
|
MD5 | e75e453948f7a0caeb40141d54ede959 |
|
BLAKE2b-256 | a2cbd69f2cbb248bd6baceb7597d8c6c229797689b546ac2a446fac15b8f455f |
laces-0.1.1-py3-none-any.whl的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | ae2c575b9aaa46154e5518c61c9f86f5a9478f753a51e9c5547c7d275d361242 |
|
MD5 | b92194c9dff29f295ca1509e2a327205 |
|
BLAKE2b-256 | e0dcceadbdc5e14aec7bd01bdd6ef8d5bf704d6a12d1b5a25bcaaa066bdb820d |