跳转到主要内容

Django组件,知道如何渲染自身。

项目描述

Laces

License: BSD-3-Clause PyPI version laces CI codecov


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_htmldjango.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_htmlget_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 %}标签类似的withonlyas关键字。

使用with提供额外的父上下文变量

您可以使用关键字with将额外的父上下文变量传递给组件

{% component welcome with name=request.user.first_name %}

注意:这些额外变量将被添加到传递给组件的render_htmlget_context_data方法的parent_context中。默认的get_context_data实现忽略了parent_context参数,因此您需要重写它来使用这些额外变量。更多信息请参阅父上下文部分。

使用only限制父上下文变量

要限制传递给组件的父上下文变量仅限于with关键字提供的变量(而不是调用模板上下文中的其他变量),请使用only

{% component welcome with name=request.user.first_name only %}

注意withonly都只影响传递给组件的render_htmlget_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 (26.9 kB 查看散列)

上传时间

构建分发

laces-0.1.1-py3-none-any.whl (21.3 kB 查看散列)

上传时间 Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面