跳转到主要内容

直接在Python中编写html组件,你将拥有一个美丽但颇具争议的MIXTure

项目描述

直接在Python中编写html组件,你将拥有一个美丽但颇具争议的MIXTure。

是的,颇具争议

如果你不喜欢,可以忽略它(但你可以使用它,而不需要html-in-python部分,见下文 ;))

基于 pyxl. Python 3.6+,并使用typing进行数据验证。

一旦你有了html,你可以用它做任何你想做的事情。把它看作是你经典模板引擎的替代品。

源代码: https://github.com/twidi/mixt/

文档: https://twidi.github.io/mixt/

PyPI: https://pypi.ac.cn/project/mixt/

CI (CircleCi): https://circleci.com/gh/twidi/workflows/mixt/

基本用法

让我们创建一个文件 example.py

# coding: mixt

from mixt import html, Element, Required

class Hello(Element):
    class PropTypes:
        name: Required[str]

    def render(self, context):
        return <div>Hello, {self.name}</div>

print(<Hello name="World"/>)

然后执行它

$ python example.py
<div>Hello, World</div>

如果你不想在Python中编写html,你仍然可以使用它

from mixt import html, Element, Required

class Hello(Element):
    class PropTypes:
        name: Required[str]

    def render(self, context):
        return html.Div()("Hello, ", self.name)

print(Hello(name="World"))

特性

是的,它受到了React(实际上是JSX)的启发,并且我们借鉴了一些概念

  • props和PropTypes以及验证

  • 开发模式用于在开发环境中验证props,而在生产环境中不验证,以提高效率(你应该已经测试过一切正常)

  • 上下文

  • 类组件或简单的函数组件

  • 高阶组件

我们增加了

  • 使用Python编写CSS

  • css/js收集器

  • 代理组件

安装

运行这两个命令。第二个命令将告诉Python如何理解包含HTML的文件。

pip install mixt
mixt-post-install

为了检查是否一切准备就绪,请运行

python -m mixt.examples.simple

你应该得到以下输出

<div title="Greeting">Hello, World</div>

如果你不想使用html-in-python的功能,不要运行mixt-post-install。然后使用(以获得相同的输出)进行测试

python -m mixt.examples.simple_pure_python

贡献

克隆git项目,然后

make dev

为了检查是否一切准备就绪,请运行

python -m mixt.examples.simple

你应该得到以下输出

<div title="Greeting">Hello, World</div>

在编写了一些代码之后

make tests
make lint

如果你触摸了codec目录中的内容,你必须运行make dev(或者至少make full-clean)来清除pyc Python文件。

注意,我们的CI将检查每个提交是否通过了make lintmake testsmake check-doc。因此,不要忘记在每个提交时运行这些。

在推送之前这样做的一种方法

git rebase develop --exec 'git log -n 1; make checks'

用户指南

注意:您可以在src/mixt/examples/user_guide中找到此用户指南的最终代码(您将找到mixt.pypure_python.py)。

使用以下命令运行

python -m mixt.examples.user_guide

开始

让我们创建一个...待办事项列表,是的!

但在此之前,请记住。这不是React,它不在浏览器上,这里也没有涉及JavaScript。我们只谈论渲染一些HTML。

但你可以用它来做你想要的事情。添加javascript处理器,简单的表单...

说到表单...

在待办事项列表中,我们希望能够添加待办事项。这是一个简单的文本输入。

所以让我们创建我们的第一个组件,TodoForm。我们想要一个包含输入文本和按钮的表单。

组件是Element类的子类,你必须编写一个名为render的方法。

# coding: mixt

from mixt import Element, html  # html is mandatory to resolve html tags

class TodoForm(Element):

    def render(self, context):  # Ignore the ``context`` argument for now.
        return \  # The ``\`` is only for a better indentation below
            <form method="post" action="???">
                <label>New Todo: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>

注意,这可以写成简单的函数

# coding: mixt

from mixt import Element, html

def TodoForm():
    return \
        <form method="post" action="???">
            <label>New Todo: </label><itext name="todo" />
            <button type="submit">Add</button>
        </form>

当打印组件时,这两个将给出相同的结果

print(<TodoForm />)
<form method="post" action="???"><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

间距

注意它的格式化方式:标签之间没有空格。实际上,它就像JSX一样

JSX移除了行首和行尾的空白。它还移除了空白行。相邻于标签的新行被移除;在字符串字面量中间出现的新行被压缩成一个空格

要添加空格或换行,你可以传递一些Python。让我们以添加标签前的换行为例

#...
            <form method="post" action="???">
                {'\n'}<label>New Todo: </label><itext name="todo" />
#...

现在我们有了这个输出

<form method="post" action="/todo/add">
<label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

Props

现在让我们更进一步。

注意表单的action属性。我们需要传递一些东西。但是硬编码它听起来并不合适。我们需要将它传递给组件。

MixtReact一样,有属性的概念,也就是“props”。

PropTypes类

Mixt中,我们使用类型在组件内部的类中定义它们,该类命名为PropTypes

class TodoForm(Element):

    class PropTypes:
        add_url: str

    def render(self, context):
        return \
            <form method="post" action={self.add_url}>
                <label>New Todo: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>

在这里,我们定义了一个名为add_url的属性,它必须是一个字符串(str)。这使用了Python类型语法

请注意,我们如何更改了

标签的属性。现在它是{self.add_url},而不是"???"。

当属性在花括号之间传递时,它们在运行时被解释为纯Python。事实上,由于解析器会在让Python解释器运行之前将整个文件转换为纯Python,所以它保持不变,只有HTML会被转换。因此,这样做没有任何惩罚。

属性和子组件

看看如果我们的组件是用纯Python编写的,它会是什么样子

from mixt import Element, html

class TodoForm(Element):

    class PropTypes:
        add_url: str

    def render(self, context):
        return html.Form(method='post', action=self.add_url )(
            html.Label()(
                html.Raw("New Todo: ")
            ),
            html.InputText(name='todo'),
            html.Button(type='submit')(
                html.Raw("Add")  # or html.Rawhtml(text="Add")
            ),
        )

请注意,属性如何作为命名参数传递给组件,以及如何传递

这个纯Python组件也展示了它的工作方式:属性作为命名参数传递给组件类,然后调用该组件,将子组件作为位置参数传递给调用

ComponentClass(prop1="val1", prop2="val2")(
    Children1(),
    Children2(),
)

子组件是什么?子组件是其他标签内的标签。

foo

中,我们有

  • 一个带有属性id和两个子组件的html.Div组件

    • 一个不带子组件的html.Span组件

    • 一个带有子组件的html.P组件

      • 一个带有文本“foo”的html.RawHtml组件

注意,您可以玩转属性和子组件。首先,这是纯Python版本,以展示其工作原理

def render(self, context):
    props = {"prop1": "val1", "prop2": "val2"}
    children = [Children1(), Children2()]

    return ComponentClass(**props)(*children)
    # You can pass a list of children to to the call, so this would produce the same result:
    # ComponentClass(**props)(children)

然后是版本

def render(self, context):
    props = {"prop1": "val1", "prop2": "val2"}
    children = [<Children1/>, <Children2/>]

    return <ComponentClass {**props}>{*children}</ComponentClass>
    # or, the same, passing the children as a list:
    # return <ComponentClass {**props}>{children}</ComponentClass>

传递属性

现在让我们回到我们的属性

如何将其传递给组件?

这正是我们传递给HTML标签属性的方式:它们实际上是HTML组件(在中定义)中定义的属性。我们支持在撰写本文时有效的(未废弃的)HTML5中的每个HTML标签及其属性(不包括废弃的属性)。

所以让我们这样做

print(<TodoForm add_url="/todo/add"/>)
<form method="post" action="/todo/add"><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

好的,我们的属性现在出现在渲染的HTML中。

验证

如果我们不传递一个字符串怎么办?我们在中说过我们想要一个字符串...

数字

让我们试试

print(<TodoForm add_url=1/>)
<form method="post" action="1"><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

它工作!但是……它不是一个字符串!!事实上,对于数字,有一个特殊情况,您可以将它们作为数字而不是字符串传递,如果需要,它们会被转换...

布尔值和其他特殊情况

让我们试试其他东西。

print(<TodoForm add_url=True/>)
mixt.exceptions.InvalidPropValueError:
<TodoForm>.add_url: `True` is not a valid value for this prop (type: <class 'bool'>, expected: <class 'str'>)

如果我们以Python中的True传递,结果是一样的

print(<TodoForm add_url={True}/>)
mixt.exceptions.InvalidPropValueError:
<TodoForm>.add_url: `True` is not a valid value for this prop (type: <class 'bool'>, expected: <class 'str'>)

好,让我们欺骗系统,传递"True",作为一个字符串。

print(<TodoForm add_url="True"/>)
mixt.exceptions.InvalidPropValueError:
<TodoForm>.add_url: `True` is not a valid value for this prop (type: <class 'bool'>, expected: <class 'str'>)

仍然是一样的,但是我们这里传递了一个字符串!是的,但是有4个值始终被评估为它们看起来像的值

  • True

  • False

  • None

  • NotProvided(一个特殊值,表示“未设置”,与None不同)

将这些值之一作为字符串传递的唯一方法是使用Python,将其作为字符串传递

print(<TodoForm add_url={"True"}/>)
<form method="post" action="True"><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

除了这4个值和数字之外,传递给属性的所有值都被视为字符串。即使在HTML5中,如果没有一些字符(没有空格,没有/...),引号对于没有引号的字符串也不是强制的。

要传递其他内容,必须在花括号内(在这种情况下,不需要在花括号周围加引号)包围该值。

好,现在我们确定我们只接受字符串……但是,如果我什么都不传递怎么办?那么,“什么也没有”是什么意思?

让我们从Python中的空字符串开始

print(<TodoForm add_url={""}/>)
<form method="post" action=""><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

好的,它工作了,我们想要一个字符串,我们现在有一个字符串。

现在让我们直接传递这个空字符串

print(<TodoForm add_url=""/>)
<form method="post" action=""><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

它仍然可以工作,因为它仍然是一个字符串。让我们去掉引号看看。

print(<TodoForm add_url=/>)
mixt.exceptions.GeneralParserError: <mixt parser> Unclosed Tags: <TodoForm>

嗯,这并不是有效的HTML。所以让我们去掉=

print(<TodoForm add_url/>)
mixt.exceptions.InvalidPropValueError:
<TodoForm>.add_url: `True` is not a valid value for this prop (type: <class 'bool'>, expected: <class 'str'>)

什么?是的,想想HTML5属性如requiredchecked……它们只需要作为属性存在,没有值,就被认为是True。所以当一个属性没有值时,它是一个布尔值,并且它是True

除了不传递值之外,HTML5中还有两种方式可以使布尔值为True

  • 传递一个空字符串:required=""

  • 传递属性的名称:required="required"

为了方便起见,我们添加了另一种方式

  • 传递True(大小写无关),作为python或字符串:required=Truerequired={True}required="true"

及其对立面,传递False

  • 传递False(大小写无关),作为python或字符串:required=Falserequired={False}required="false"

必需属性

对于布尔属性来说,这是可以接受的。但这不是我们的情况。我们能做的最后一件事就是根本不设置属性

print(<TodoForm/>)
# this is the same: ``print(<TodoForm add_url=NotProvided />)```
# (``NotProvided`` must be imported from ``mixt``)
mixt.exceptions.UnsetPropError: <TodoForm>.add_url: prop is not set

这是可以理解的:我们尝试访问一个未设置的属性,当然我们无法使用它。

但如果我们不访问它怎么办?如果我们不打印组件,它就不会被渲染

<TodoForm/>
<TodoForm at 0x7fbd18ea5630>

因此,我们可以创建一个实例,但它将在渲染时失败。但是有一个方法可以防止这种情况。

默认情况下,所有属性都是可选的。而且你不必为每个属性使用python typing 模块中的 Optional 类型,这会变得很繁琐。

相反,mixt 提供了一个名为 Required 的类型,你可以像使用 Optionnal 一样使用它。

from mixt import Element, Required, html

class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]

    def render(self, context):
        # ...

所以我们刚刚说过我们想要一个字符串,并且它是必需的。

让我们再次尝试创建它而不传递属性

<TodoForm/>
mixt.exceptions.RequiredPropError: <TodoForm>.add_url: is a required prop but is not set

现在我们在程序中引发了早先的异常。

默认属性

为了查看属性的其他可能性,让我们添加一个新的属性来更改文本标签。但我们不希望它成为必需的,而是提供一个默认值。

为此,只需在 PropTypes 类中为属性添加一个值即可

class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]
        label: str = 'New Todo'

    def render(self, context):
        return \
            <form method="post" action={self.add_url}>
                <label>{self.label}: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>

现在让我们尝试不传递属性

print(<TodoForm add_url="/todo/add"/>)
<form method="post" action=""><label>New Todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

如果我们传递一个

print(<TodoForm add_url="/todo/add" label="Thing to do" />)
<form method="post" action="/todo/add"><label>Thing to do: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

它按预期工作。

请注意,你不能在属性 Required 的同时提供一个默认值。这没有意义,所以它会在类构造时尽可能早地进行检查。

class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]
        label: Required[str] = 'New Todo'
mixt.exceptions.PropTypeRequiredError: <TodoForm>.label: a prop with a default value cannot be required

当然,默认值必须与类型匹配!

class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]
        label: str = {'label': 'foo'}
mixt.exceptions.InvalidPropValueError:
<TodoForm>.label: `{'label': 'foo'}` is not a valid value for this prop (type: <class 'dict'>, expected: <class 'str'>)

选择

在我们的组件中,我们还想让它根据传递给它的“类型”来构造标签,但限制选择。为此,我们可以使用 Choices 类型。

from mixt import Choices, Element, Required, html


class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]
        type: Choices = ['todo', 'thing']

    def render(self, context):

        return \
            <form method="post" action={self.add_url}>
                <label>New {self.type}: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>

让我们试试

print(<TodoForm add_url="/todo/add" type="todo" />)
print(<TodoForm add_url="/todo/add" type="thing" />)
<form method="post" action="/todo/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>
<form method="post" action="/todo/add"><label>New thing: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

如果我们尝试传递一个不在可用选择范围内的值,它会失败,这也是意料之中的。

print(<TodoForm add_url="/todo/add" type="stuff" />)
mixt.exceptions.InvalidPropChoiceError: <TodoForm>.type: `stuff` is not a valid choice for this prop (must be in ['todo', 'thing'])

默认选择

但也许我们不想传递它,并使用默认值。结果会是什么?

print(<TodoForm add_url="/todo/add" />)
mixt.exceptions.UnsetPropError: <TodoForm>.type: prop is not set

因此,我们必须将 type 属性标记为必需的。

class PropTypes:
    add_url: Required[str]
    type: Required[Choices] = ['todo', 'thing']

因此,如果我们不传递它,它会在更早的时候失败。

print(<TodoForm add_url="/todo/add" />)
mixt.exceptions.RequiredPropError: <TodoForm>.type: is a required prop but is not set

但这不是我们想要的,我们想要一个默认值。

事实上,你注意到对于除了Choices类型之外,在PropTypes中设置一个值会给我们一个默认值。但对于Choices来说,情况不同,因为值是选择列表。

为此,我们有了DefaultChoices:它的工作方式与Choices相同,但使用列表中的第一个条目作为默认值。当然,与其他具有默认值的类型一样,它不能是Required

让我们试试

from mixt import DefaultChoices, Element, Required, html


class TodoForm(Element):

    class PropTypes:
        add_url: Required[str]
        type: DefaultChoices = ['todo', 'thing']
print(<TodoForm add_url="/todo/add" />)
<form method="post" action="/todo/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

它按预期工作。

高级类型

在此之前,我们使用了简单类型,但你也可以使用更复杂的类型。

例如,我们将使add_url prop 接受一个函数,该函数将基于type prop 计算出 URL。但我们还想允许字符串,并带有默认值。

我们可以通过typing来实现这一点。我们的函数将接受一个字符串,type,并返回一个字符串,即 URL。

因此,可调用对象的语法是Callable[[str], str],我们使用Union来接受类型为Callablestr的值。

from typing import Union, Callable
from mixt import DefaultChoices, Element, Required, html


class TodoForm(Element):

    class PropTypes:
        add_url: Union[Callable[[str], str], str] = "/todo/add"
        type: DefaultChoices = ['todo', 'thing']

    def render(self, context):

        if callable(self.add_url):
            add_url = self.add_url(self.type)
        else:
            add_url = self.add_url

        return \
            <form method="post" action={add_url}>
                <label>New {self.type}: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>

首先,让我们在不使用add_url prop 的情况下尝试,因为我们有一个默认值。

print(<TodoForm  />)
<form method="post" action="/todo/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

如果我们传递一个字符串,也应该可以工作。

print(<TodoForm add_url="/todolist/add" />)
<form method="post" action="/todolist/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

现在我们可以传递一个函数。

def make_url(type):
    return f"/{type}/add"

print(<TodoForm add_url={make_url} />)
mixt.exceptions.InvalidPropValueError: <TodoForm>.add_url:
`<function make_url at 0x7fe2ae87be18>` is not a valid value for this prop (type: <class 'function'>, expected: Union[Callable[[str], str], str])

哦?为什么?我传递了一个接受字符串作为参数并返回字符串的函数。是的,但不要忘记类型是会被检查的!所以我们必须给我们的函数添加类型。

def make_url(type: str) -> str:
    return f"/{type}/add"

print(<TodoForm add_url={make_url} />)
<form method="post" action="/todo/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

如果我们传递另一种类型,URL应该相应地更改。

print(<TodoForm add_url={make_url} type="thing" />)
<form method="post" action="/thing/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

我们甚至可以将这个函数作为我们 prop 的默认值。

from typing import Union, Callable
from mixt import DefaultChoices, Element, Required, html


def make_url(type: str) -> str:
    return f"/{type}/add"


class TodoForm(Element):

    class PropTypes:
        add_url: Union[Callable[[str], str], str] = make_url
        type: DefaultChoices = ['todo', 'thing']

    def render(self, context):

        if callable(self.add_url):
            add_url = self.add_url(self.type)
        else:
            add_url = self.add_url

        return \
            <form method="post" action={add_url}>
                <label>New {self.type}: </label><itext name="todo" />
                <button type="submit">Add</button>
            </form>
print(<TodoForm />)
<form method="post" action="/todo/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

dev-mode

现在你可能开始想知道……Python typing很繁琐,验证可能会消耗我们宝贵的时间。

让我来回答这个问题。

  1. 不,typing并不繁琐。它非常有用,可以帮助我们发现错误并添加一些自述文档。

  2. 是的,它会消耗我们宝贵的时间。但我们有解决办法。

默认情况下,mixt 在“dev-mode”下运行。在 dev-mode 下,当 prop 传递给组件时,会进行验证。当你不在“dev-mode”下时,验证将被跳过。所以在生产环境中,你可以禁用 dev-mode(我们一会儿会看到如何做到这一点)并快速传递 props。

  • 我们不检查必需的 props(但如果你尝试在你的组件中使用它,这将会失败)。

  • 我们不检查Choices prop 是否确实在选择列表中。

  • 我们根本不检查类型,所以例如,如果你想传递一个列表作为字符串,它会工作,但你的 render 方法会发生一些可以理解的奇怪事情。

但你可能认为在生产环境中验证很重要。确实如此。但当然,你的代码完全由测试覆盖,你在 dev-mode 下运行这些测试,所以在生产环境中,你不需要这种验证!顺便说一下,这就是 React 的工作方式,通过设置NODE_ENV=production

如何更改 dev-mode?我们不会强制任何环境变量,但我们提出了一些函数。是否调用它们取决于你。

from mixt import set_dev_mode, unset_dev_mode, override_dev_mode, in_dev_mode

# by default, dev-mode is active
assert in_dev_mode()

# you can unset the dev-mode
unset_dev_mode()
assert not in_dev_mode()

# and set it back
set_dev_mode()
assert in_dev_mode()

# set_dev_mode can take a boolean
set_dev_mode(False)
assert not in_dev_mode()

set_dev_mode(True)
assert in_dev_mode()

# and we have a context manager to override for a block
with override_dev_mode(False):
    assert not in_dev_mode()
    with override_dev_mode(True):
        assert in_dev_mode()
    assert not in_dev_mode()
assert in_dev_mode()

所以让我们用 type prop 来试一试。记住,它看起来像这样。

type: DefaultChoices = ['todo', 'thing']

我们尝试传递另一个选择,首先在 dev-mode 下。

with override_dev_mode(True):
    print(<TodoForm type="stuff" />)
mixt.exceptions.InvalidPropChoiceError: <TodoForm>.type: `stuff` is not a valid choice for this prop (must be in ['todo', 'thing'])

正如预期的那样失败了。

现在,通过禁用 dev-mode。

with override_dev_mode(False):
    print(<TodoForm type="stuff" />)
<form method="post" action="/stuff/add"><label>New stuff: </label><input type="text" name="todo" /><button type="submit">Add</button></form>

它正常工作,我们有一个不在选择中的待办类型被使用,并且也在操作中。确保你永远不会传递无效属性是你的测试工作,这样你就可以在生产环境中放心使用并禁用开发模式。

组件层叠

现在我们有了我们的表单。我们的待办事项列表应用还需要哪些组件呢?

当然,我们需要一种显示待办条目的方式。

但是,待办条目是什么?让我们创建一个基本的TodoObject

class TodoObject:
    def __init__(self, text):
        self.text = text

这是一个非常简单的类,但你当然可以使用你想要的任何东西。它可以是Django模型等...

因此,我们可以创建我们的Todo组件,使其接受一个必需的TodoObject属性

class Todo(Element):
    class PropTypes:
        todo: Required[TodoObject]

    def render(self, context):
        return <li>{self.todo.text}</li>

并且我们可以使用它

todo = TodoObject("foo")
print(<Todo todo={todo} />)
<li>foo</li>

现在我们想要有一个待办事项列表。让我们创建一个接受TodoObject列表作为属性的TodoList组件。

但是,与之前两个只使用HTML标签的render方法的组件不同的是,现在我们将在一个组件中封装另一个组件。让我们看看怎么做。

class TodoList(Element):

    class PropTypes:
        todos: Required[List[TodoObject]]

    def render(self, context):
        return <ul>{[<Todo todo={todo} /> for todo in self.todos]}</ul>

是的,就这么简单:你就像使用HTML标签一样使用<Todo...>组件。唯一的区别是,对于HTML标签,你不需要直接导入它们(简单导入html from mixt),并且按照惯例,我们将它们写成小写。对于常规组件,你必须导入它们(你仍然可以这样做:from mylib import components<components.MyComponent ...>)并使用正确的命名。

注意我们如何要求一个列表,并通过花括号中的列表推导式将其传递给<ul>

如果你愿意,可以有不同的做法。

例如,将列表推导式与HTML分开

def render(self, context):
    todos = [
        <Todo todo={todo} />
        for todo
        in self.todos
    ]
    return <ul>{todos}</ul>

或在专用方法中(这对于测试会有用)

def render_todos(self, todos):
    return [
        <Todo todo={todo} />
        for todo
        in todos
    ]

def render(self, context):
    return <ul>{self.render_todos(self.todos)}</ul>

由你决定:最终这只是Python。

让我们看看这个组件渲染了什么

todos = [TodoObject("foo"), TodoObject("bar"), TodoObject("baz")]
print(<TodoList todos={todos} />)
<ul><li>foo</li><li>bar</li><li>baz</li></ul>

最后,我们有封装表单和列表的TodoApp组件

class TodoApp(Element):

    class PropTypes:
        todos: Required[List[TodoObject]]
        type: DefaultChoices = ['todo', 'thing']

    def render(self, context):
        return \
            <div>
                <h1>The "{self.type}" list</h1>
                <TodoForm type={self.type} />
                <TodoList todos={self.todos} />
            </div>
todos = [TodoObject("foo"), TodoObject("bar"), TodoObject("baz")]
print(<TodoList todos={todos} type="thing" />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>foo</li><li>bar</li><li>baz</li></ul></div>

让我们将这个HTML传递给一个HTML美化器

<div>
    <h1>The "thing" list</h1>
    <form method="post" action="/thing/add">
        <label>New thing: </label>
        <input type="text" name="todo" />
        <button type="submit">Add</button>
    </form>
    <ul>
        <li>foo</li>
        <li>bar</li>
        <li>baz</li>
    </ul>
</div>

就这样,我们有了待办事项列表应用!要在页面上使用它,只需创建一个将渲染HTML基本标记并集成TodoApp组件的组件。你甚至不需要组件

todos = [TodoObject("foo"), TodoObject("bar"), TodoObject("baz")]

print(
    <html>
        <body>
            <TodoApp todos={todos} type="thing" />
        </body>
    </html>
)

美化后的输出将是

<html>

<body>
    <div>
        <h1>The "thing" list</h1>
        <form method="post" action="/thing/add">
            <label>New thing: </label>
            <input type="text" name="todo" />
            <button type="submit">Add</button>
        </form>
        <ul>
            <li>foo</li>
            <li>bar</li>
            <li>baz</li>
        </ul>
    </div>
</body>

</html>

覆盖组件

我们有一个通用的待办事项列表,但是根据可用的待办事项类型,我们可能想要有一个“待办事项列表”和一个“事项列表”。

我们已经有了待办事项列表,因为我们的TodoApp默认有一个todo类型。

因此,让我们创建一个ThingApp

继承

实现这个功能的第一种方式是从我们的TodoApp继承。但是通过继承,我们无法从父组件中删除属性(这并不是真的,我们稍后会看到),所以我们仍然默认有type属性。但是,我们不想接受除“thing”之外的其他任何内容。因此,我们可以像这样重新定义type属性

class ThingApp(TodoApp):
    class PropTypes:
        type: DefaultChoices = ['thing']

让我们使用这个组件

print(<ThingApp todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form method="post" action="/thing/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form><ul><li>foo</li></ul></div>

如果我们尝试为type属性传递“todo”,它将不起作用

print(<ThingApp todos={[TodoObject("foo")]} type="todo" />)
mixt.exceptions.InvalidPropChoiceError:
<ThingApp>.type: `todo` is not a valid choice for this prop (must be in ['thing'])

但是,仍然,能够传递类型很奇怪。

父组件

让我们尝试另一种方法:父组件。一个只做与子组件相关的事情并返回它们的组件。我们在这里想要的,是一个返回带有 type 属性强制为“thing”的 TodoApp 组件的组件。

让我们试试这个

class ThingApp(Element):
    class PropTypes:
        todos: Required[List[TodoObject]]

    def render(self, context):
        return <TodoApp todos={self.todos} type="thing" />
print(<ThingApp todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form method="post" action="/thing/add"><label>New todo: </label><input type="text" name="todo" /><button type="submit">Add</button></form><ul><li>foo</li></ul></div>

它工作了,这次,我们不能传递 type 属性

print(<ThingApp todos={[TodoObject("foo")]} />)
mixt.exceptions.InvalidPropNameError: <ThingApp>.type: is not an allowed prop

PropTypes 的 DRY原则

注意我们不得不在 TodoAppTodoThing 中定义 todos 属性的类型。

有许多处理这种类型的方法。

第一种方法是在 ThingApp 中忽略类型,因为它将在 TodoApp 中进行检查。所以我们将使用类型 Any

from typing import Any

#...

class ThingApp(Element):
    class PropTypes:
        todos: Any

 #...

让我们用一个有效的 todos 列表试一下

print(<ThingApp todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>foo</li></ul></div>

但如果我们传递其他东西呢?

print(<ThingApp todos="foo, bar" />)
mixt.exceptions.InvalidPropValueError:
<TodoApp>.todos: `foo, bar` is not a valid value for this prop (type: <class 'str'>, expected: List[TodoObject])

它按预期工作,但错误是在 TodoApp 层级报告的,这是完全正常的。

另一种方法是在更高层级定义类型

TodoObjects = Required[List[TodoObject]]

class TodoApp(Element):
    class PropTypes:
        todos: TodoObjects
 # ...

class ThingApp(Element):
    class PropTypes:
        todos: TodoObjects
 # ...

现在如果我们传递其他东西,错误将在正确的层级报告

print(<ThingApp todos="foo, bar" />)
mixt.exceptions.InvalidPropValueError:
<TodoThing>.todos: `foo, bar` is not a valid value for this prop (type: <class 'str'>, expected: List[TodoObject])

但如果你不能或不想这样做,你可以在 TodoApp 中保留类型定义,并使用组件的 prop_type 类方法来获取属性的类型

class ThingApp(Element):
    class PropTypes:
        todos: TodoApp.prop_type("todos")
 # ...

但对于 ThingAppTodoApp 报错真的很重要吗?因为最终,确实是 TodoApp 必须进行检查。

所以这应该是一个更通用的做法...

函数

我们之前看到,一个组件可以是一个单独的函数来渲染组件。它只需返回一个组件,一个 HTML 标签。与类组件的不同之处在于,没有 PropTypes,所以没有验证。但是...这正是我们所需要的。

我们希望我们的 ThingApp 接受一些属性(例如 todos 属性),并返回一个具有特定 type 属性的 TodoApp

所以我们可以这样做

def ThingApp(todos):
    return <TodoApp type="thing" todos={todos} />

这里我们可以看到,不能将 type 传递给 ThingsApp,这不是一个有效的参数。

让我们试试

print(<ThingApp todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>foo</li></ul></div>

这里只有一个属性要传递,所以很容易。但想象一下,如果我们有很多属性。我们可以使用 {**props} 语法

def ThingApp(**props):
    return <TodoApp type="thing" {**props} />

并且你可以用更少的字符来做(如果这很重要的话)

ThingApp = lambda **props: <TodoApp type="thing" {**props} />

这两个函数行为完全相同。

你不能传递一个 type 属性,因为这会是一个 Python 错误,因为它将被两次传递给 TodoApp

print(<ThingApp todos={[TodoObject("foo")]} type="thing" />)
TypeError: BaseMetaclass object got multiple values for keyword argument 'type'

(是的,它提到了 BaseMetaclass,这是创建我们组件类的元类)

其他任何错误的属性都将由 TodoApp 验证

print(<ThingApp todos={[TodoObject("foo")]} foo="bar" />)
mixt.exceptions.InvalidPropNameError: <TodoApp>.foo: is not an allowed prop

考虑到这一点,我们可能已经创建了一个通用函数,该函数可以强制接受 type 属性的任何组件的类型

Thingify = lambda component, **props: <component type="thing" {**props} />
print(<Thingify component={TodoApp} todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>foo</li></ul></div>

渲染的组件是 TodoApptype 属性是“thing”,其他属性(这里仅指 todos)都正确传递。

高阶组件

现在将这个概念扩展到更通用的情况:“高阶组件”。在 React 中,“高阶组件” 是“一个接收组件并返回新组件的函数。”

想法是

EnhancedComponent = higherOrderComponent(WrappedComponent)

一种经典的方法是返回一个新的组件类

def higherOrderComponent(WrappedComponent):

    class HOC(Element):
        __display_name__ = f"higherOrderComponent({WrappedComponent.__display_name__})"

        class PropTypes(WrappedComponent.PropTypes):
            pass

        def render(self, context):
            return <WrappedComponent {**self.props}>{self.childre()}</WrappedComponent>

    return HOC

注意我们如何将 PropTypes 类设置为从包装组件继承,以及如何将所有属性传递给包装组件,包括子组件。返回的组件将接受与包装组件相同的属性,具有相同的类型。

还要注意 __display_name__。它将在异常中使用,以便您知道引发异常的组件。在这里,没有强制设置,它将被设置为 HOC,这没有帮助。相反,我们表明这是一个传入组件的转换版本。

这里是一个没有任何有用功能的函数。

在我们的例子中,我们可以这样做

def thingify(WrappedComponent):

    class HOC(Element):
        __display_name__ = f"thingify({WrappedComponent.__display_name__})"

        class PropTypes(WrappedComponent.PropTypes):
            __exclude__ = {'type'}

        def render(self, context):
            return <WrappedComponent type="thing" {**self.props}>{self.children()}</WrappedComponent>

    return HOC

这里有两个重要的事情

  • 注意我们如何使用 __exclude__ = {'type'}WrappedComponent.PropTypes 继承的属性中删除 type 属性。因此,返回的组件将期望与包装组件相同的属性,除了 type

  • 我们在渲染的包装组件中添加了 {self.children()},因为即使我们实际上知道我们将要包装的组件 TodoApp 不接受子组件(它可能接受但对其没有作用),我们也不能预先说它始终是这样,而且这个高阶组件不会用来包装除了 TodoApp 以外的组件。所以最好总是这样做。

现在我们可以创建我们的 ThingApp

ThingApp = thingify(TodoApp)

并使用它

print(<ThingApp todos={[TodoObject("foo")]} />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>foo</li></ul></div>

如果我们尝试传递类型

print(<ThingApp todos={[TodoObject("foo")]} type="thing" />)
mixt.exceptions.InvalidPropNameError: <thingify(TodoApp)>.type: is not an allowed prop

所以如计划,我们不能传递类型。并且注意 __display_name__ 的使用。

让我们思考一下它的强大。

假设我们想让 TodoApp 接受一个 TodoObject 列表。但我们想从“源”获取它们。

我们甚至可以直接以通用方式编写这个新的高阶组件

def from_data_source(WrappedComponent, prop_name, get_source):

    class HOC(Element):
        __display_name__ = f"from_data_source({WrappedComponent.__display_name__})"

        class PropTypes(WrappedComponent.PropTypes):
            __exclude__ = {prop_name}

        def render(self, context):
            props = self.props.copy()
            props[prop_name] = get_source(props, context)
            return <WrappedComponent {**props}>{self.children()}</WrappedComponent>

    return HOC

这次,函数 from_data_source 除了 WrappedComponent 之外还接受两个参数

  • prop_name:这是要填充数据的包装组件属性的名称

  • get_source:这是一个将被调用来获取数据的函数

看看我们如何从包装组件继承了 PropTypes 并排除了 prop_name。因此,我们(并且不能)将数据传递给我们的新组件。

然后在 render 中,我们设置一个属性,将 get_source 调用的结果传递给 WrappedComponent

所以让我们编写一个非常简单的函数(这可能是具有缓存、过滤等复杂性的函数),它接受属性和上下文,并返回一些数据

def get_todos(props, context):
    # here it could be a call to a database
    return [
        TodoObject("fooooo"),
        TodoObject("baaaar"),
    ]

我们可以组合我们的组件

SourcedTodoApp = from_data_source(TodoApp, 'todos', get_todos)
ThingApp = thingify(SourcedTodoApp)

并运行它

print(<ThingApp />)
<div><h1>The "thing" list</h1><form>...</form><ul><li>fooooo</li><li>baaaar</li></ul></div>

它按预期工作,并且仅在组件需要渲染时才会获取数据。

上下文

因此,我们有一个可以从外部源获取数据的待办事项列表。但我们可能希望数据根据用户而有所不同。

我们可以在主级别做的是,获取我们的用户并将它传递给每个组件,以确保每个组件都能获取当前登录用户。

这不是很麻烦吗?

解决这个用例正是由 mixt 提供的 Context 概念的目的所在。当然,它也是受到了 React 中的上下文概念 的启发。

正如他们所说

上下文的设计是为了共享那些可以被认为是 React 组件树中“全局”的数据,例如当前认证的用户、主题或首选语言。

创建一个上下文就像创建一个组件一样简单,只是它会继承自 BaseContext,并且不需要 render 方法(它将渲染其子元素)。

它接受一个 PropTypes 类,该类定义了上下文将接受和传递给树的数据类型。

所以让我们创建一个将保存认证用户 id 的上下文。

from mixt import BaseContext

class UserContext(BaseContext):
    class PropTypes:
        authenticated_user_id: Required[int]

现在,我们想更新我们的 get_todos 方法,使其考虑到 authenticated_user_id

记住,我们传入了 props 和上下文。在这里上下文将很有用

def get_todos(props, context):
    return {
        1:[
            TodoObject("1-1"),
            TodoObject("1-2"),
        ],
        2: [
            TodoObject("2-1"),
            TodoObject("2-2"),
        ]
    }[context.authenticated_user_id]

现在我们可以使用上下文来渲染我们的应用

print(
    <UserContext authenticated_user_id=1>
        <ThingApp />
    </UserContext>
)
<div><h1>The "thing" list</h1><form>...</form><ul><li>1-1</li><li>1-2</li></ul></div>

我们可以看到用户 1 的待办事项条目。

让我们试一下用户 2

print(
    <UserContext authenticated_user_id=2>
        <ThingApp />
    </UserContext>
)
<div><h1>The "thing" list</h1><form>...</form><ul><li>2-1</li><li>2-2</li></ul></div>

我们可以看到用户 2 的待办事项条目。

在这种情况下,当然我们可以直接通过 prop 传递用户 id。但是想象一下待办事项应用深埋在组件树中,这样做要容易得多。

但正如 React 文档所说

不要仅仅为了避免向下传递 props 几层而使用上下文。坚持使用那些需要在多个层级和多个组件中访问相同数据的场景。

在没有上下文的情况下,render 方法的 context 参数被设置为 EmptyContext,而不是 None。因此,你可以直接使用 has_prop 方法来检查是否可以通过上下文访问某个 prop。

让我们更新 get_todos 函数,以便在没有认证用户的情况下返回一个空的待办事项对象列表。

def get_todos(props, context):
    if not context.has_prop('authenticated_user_id') or not context.authenticated_user_id:
        return []
    return {
        1:[
            TodoObject("1-1"),
            TodoObject("1-2"),
        ],
        2: [
            TodoObject("2-1"),
            TodoObject("2-2"),
        ]
    }[context.authenticated_user_id]

让我们试一下

print(<ThingApp />)
<div><h1>The "thing" list</h1><form>...</form><ul></ul></div>

即使有上下文中的用户,它仍然可以工作

print(
    <UserContext authenticated_user_id=1>
        <ThingApp />
    </UserContext>
)
<div><h1>The "thing" list</h1><form>...</form><ul><li>1-1</li><li>1-2</li></ul></div>

关于上下文的重要注意事项:你可以有多个上下文!但是,在多个上下文中定义相同的 prop 可能会导致未定义的行为。

样式和 JavaScript

每个人都喜欢美好的设计,也许还有一些交互。

这很容易做到:我们生成 HTML,HTML 可以包含一些 CSS 和 JS。

让我们先添加一些交互:当在 TodoForm 中添加一个项时,我们将其添加到列表中。

首先,我们在 TodoForm 组件中添加一个 render_javascript 方法,该方法将托管我们的 (很差,我们可以做得更好,但这不是重点) JavaScript

class TodoForm(Element):
    # ...

    def render_javascript(self, context):
        return html.Raw("""
function on_todo_add_submit(form) {
    var text = form.todo.value;
    alert(text);
}
        """)

首先,我们只显示新的待办事项文本。

现在更新我们的 render 方法以返回此 JavaScript (注意,使用 render_javascript 方法只是为了分离关注点,它可以直接放在 render 方法中)。

class TodoForm(Element):
    # ...

    def render(self, context):
        # ...

        return \
            <Fragment>
                <script>{self.render_javascript(context)}</script>
                <form method="post" action={add_url} onsubmit="return on_todo_add_submit(this);">
                    <label>New {self.type}: </label><itext name="todo" />
                    <button type="submit">Add</button>
                </form>
            </Fragment>

注意 Fragment 标签。它是一种封装多个元素以便返回的方式,就像在 React 中一样。它本可以是一个简单的列表,但结尾有逗号。

return [
    <script>...</script>,
    <form>
        ...
    </form>
]

现在我们想要向列表中添加一个项。这不是 TodoForm 的角色,而是列表的角色。因此,我们将在 TodoList 组件中添加一些 JS:一个接受一些文本并创建新条目的函数。

至于 TodoForm,我们添加一个 render_javascript 方法,其中包含 (仍然很差) JavaScript

class TodoList(Element):
    # ...

    def render_javascript(self, context):

        todo_placeholder = <Todo todo={TodoObject(text='placeholder')} />

        return html.Raw("""
TODO_TEMPLATE = "%s";
function add_todo(text) {
    var html = TODO_TEMPLATE.replace("placeholder", text);
    var ul = document.querySelector('#todo-items');
    ul.innerHTML = html + ul.innerHTML;
}
        """ % (todo_placeholder))

并且我们更新了我们的 render 方法,向 ul 标签中添加了 <script> 标签和一个 id,该标签在 JavaScript 中使用。

class TodoList(Element):
    # ...

    def render(self, context):
        return \
            <Fragment>
                <script>{self.render_javascript(context)}</script>
                <ul id="todo-items">{[<Todo todo={todo} /> for todo in self.todos]}</ul>
            </Fragment>

现在我们可以更新 TodoForm 组件的 render_javascript 方法,以使用我们新的 add_toto JavaScript 函数。

class TodoForm(Element):
    # ...

    def render_javascript(self, context):
        return html.Raw("""
function on_todo_add_submit(form) {
    var text = form.todo.value;
    add_todo(text);
}
        """)

就是这样。实际上没有什么特别的。

但是让我们来看看我们的 TodoApp 的输出。

print(
    <UserContext authenticated_user_id=1>
        <ThingApp />
    </User>
)

美化后的输出是:

<div>
    <h1>The "thing" list</h1>
    <script>
        function on_todo_add_submit(form) {
            var text = form.todo.value;
            add_todo(text);
        }
    </script>
    <form method="post" action="/thing/add" onsubmit="return on_todo_add_submit(this);">
        <label>New thing: </label>
        <input type="text" name="todo" />
        <button type="submit">Add</button>
    </form>
    <script>
        TODO_TEMPLATE = "<li>placeholder</li>";

        function add_todo(text) {
            var html = TODO_TEMPLATE.replace("placeholder", text);
            var ul = document.querySelector('#todo-items');
            ul.innerHTML = html + ul.innerHTML;
        }
    </script>
    <ul id="todo-items">
        <li>1-1</li>
        <li>1-2</li>
    </ul>
</div>

所以我们有很多 script 标签。只有一个可能更好。

收集器

mixt 提供了一种“收集”渲染部分内容以便将其放在其他地方的方法。我们有两个简单的收集器可供使用,用作组件:JSCollectorCSSCollector

这些组件收集其子树的部分内容。

Collector.Collect

第一种方式是通过使用收集器 Collect 标签。

首先让我们改变我们的主要调用

from mixt import JSCollector

print(
    <JSCollector render_position="after">
        <UserContext authenticated_user_id=1>
            <ThingApp />
        </User>
    </JSCollector>
)

这将收集所有 JSCollector.Collect 标签的内容。

让我们更新我们的 TodoForm,并用 JSCollector.Collect 标签替换我们的 script 标签

class TodoForm(Element):
    # ...

    def render(self, context):

        if callable(self.add_url):
            add_url = self.add_url(self.type)
        else:
            add_url = self.add_url

        return \
                <JSCollector.Collect>{self.render_javascript(context)}</JSCollector.Collect>
                <form method="post" action={add_url} onsubmit="return on_todo_add_submit(this);">
                    <label>New {self.type}: </label><itext name="todo" />
                    <button type="submit">Add</button>
                </form>
            </Fragment>

我们也可以用相同的方式处理 TodoList

class TodoList(Element):
    # ...

    def render(self, context):
        return \
            <Fragment>
                <JSCollector.Collect>{self.render_javascript(context)}</JSCollector.Collect>
                <ul id="todo-items">{[<Todo todo={todo} /> for todo in self.todos]}</ul>
            </Fragment>

现在让我们运行我们的更新代码

print(
    <JSCollector render_position="after">
        <UserContext authenticated_user_id=1>
            <ThingApp />
        </User>
    </JSCollector>
)

美化后的输出是:

<div>
    <h1>The "thing" list</h1>
    <form method="post" action="/thing/add" onsubmit="return on_todo_add_submit(this);">
        <label>New thing: </label>
        <input type="text" name="todo" />
        <button type="submit">Add</button>
    </form>
    <ul id="todo-items">
        <li>1-1</li>
        <li>1-2</li>
    </ul>
</div>
<script type="text/javascript">
    function on_todo_add_submit(form) {
        var text = form.todo.value;
        add_todo(text);
    }

    TODO_TEMPLATE = "<li>placeholder</li>";

    function add_todo(text) {
        var html = TODO_TEMPLATE.replace("placeholder", text);
        var ul = document.querySelector('#todo-items');
        ul.innerHTML = html + ul.innerHTML;
    }
</script>

如您所见,所有的脚本都放在单个 script 标签中,在最后。更确切地说,在 JSCollector 标签所在的最后,因为我们使用了 render_position="after"。另一种可能是 render_position="before",将内容放在 JSCollector 标签开始的地方。

所有这些工作方式与 CSSCollector 标签完全相同,其中内容放在一个 <style type="text/css"> 标签中。

render_[js|css] 方法

由于在 HTML 世界中,使用 JS/CSS 非常普遍,我们添加了一些糖来使所有这些操作更加容易。

如果您有一个 render_js 方法,JSCollector 将自动收集该方法的输出。对于 CSSSelectorrender_css 方法也是如此。

有了这个,就不再需要 JSCollector.Collect 标签了。

为了让这在我们示例中工作,在 TodoFormTodoList

  • 移除 JSCollector.Collect 标签

  • 移除现在不再需要的 Fragment 标签

  • render_javascript 方法重命名为 render_js

  • render_js 中移除对 html.Raw 的调用,因为它在收集器调用 render_js 时不再需要:如果输出是一个字符串,它被视为“原始”字符串

这样我们就得到了相同的结果。

render_[js|css]_global 方法

现在它之所以工作,是因为我们只有一个具有 render_js 方法的子实例。

但是如果我们有多个子实例,这个方法将为每个子实例调用。实际上,它应该只包含对特定实例非常具体的代码。

为了只为 Component 类收集一次 JS/CSS,我们必须使用 render_js_globalrender_css_global(预期为 classmethod)。

它将在找到第一个实例时收集,并且只收集一次,在收集 render_js 方法之前。

在这里,我们可以将我们的 render_js 改为 render_js_global,用 @classmethod 装饰它们,它仍然会按预期工作。

参考资料

我们现在能够重新组合 JavaScript 或样式。但如果我们想将其放置在其他位置,比如在 head 标签内或 body 标签的末尾呢?

通过引用,也就是“refs”,这是可能的。与 React 中的上下文相同,当然不包括 DOM 部分。

您创建一个引用,将其传递给一个组件,然后您可以在任何地方使用它。

让我们更新我们的主要代码来实现这一点。

首先我们创建一个引用。

from mixt import Ref

js_ref = Ref()

这会创建一个新的对象,该对象将保存对组件的引用。在一个组件中,您不需要导入 Ref,可以使用 js_ref = self.add_ref(),但在这里我们不在组件中。

要保存引用,我们只需将其传递给 ref 属性

<JSCollector ref={js_ref} >...</JSCollector>

请注意,我们已经移除了 render_position 属性,因为我们现在不希望将 JS 放在标签之前或之后,而是放在其他地方。

要访问由引用引用的组件,请使用 current 属性

js_collector = js_ref.current

当然,这只能在渲染之后完成。

我们如何使用它来在我们的 head 中添加一个 script 标签。

首先更新我们的 html 以包含经典的 htmlheadbody 标签

return str(
    <html>
        <head>
        </head>
        <body>
            <JSCollector ref={js_ref} >
                <UserContext authenticated_user_id=1>
                    <ThingApp />
                </UserContext>
            </JSCollector>
        </body>
    </html>
)

到目前为止,输出中还没有任何 script 标签

<html>

<head></head>

<body>
    <div>
        <h1>The "thing" list</h1>
        <form method="post" action="/thing/add" onsubmit="return on_todo_add_submit(this);">
            <label>New thing: </label>
            <input type="text" name="todo" />
            <button type="submit">Add</button>
        </form>
        <ul id="todo-items">
            <li>1-1</li>
            <li>1-2</li>
        </ul>
    </div>
</body>

</html>

首先要知道:收集器能够通过调用其 render_collected 方法来渲染它收集的所有内容。

而且记得它已经包含了 script 标签,我们可能想要这样做

# ...
<head>
    {js_ref.current.render_collected()}
</head>
# ...

但这不起作用

AttributeError: 'NoneType' object has no attribute 'render_collected'

这是因为我们尝试在渲染时访问当前值。必须在之后完成。

为此,我们可以使用 mixt 的一个特性:如果添加到树中的某个东西是可调用的,它将在转换为字符串时调用它。

所以我们可以使用一个 lambda

# ...
<head>
    {lambda: js_ref.current.render_collected()}
</head>
# ...

现在它工作了

<html>

<head>
    <script type="text/javascript">
        function on_todo_add_submit(form) {
            var text = form.todo.value;
            add_todo(text);
        }

        TODO_TEMPLATE = "<li>placeholder</li>";

        function add_todo(text) {
            var html = TODO_TEMPLATE.replace("placeholder", text);
            var ul = document.querySelector('#todo-items');
            ul.innerHTML = html + ul.innerHTML;
        }
    </script>
</head>

<body>
    <div>
        <h1>The "thing" list</h1>
        <form method="post" action="/thing/add" onsubmit="return on_todo_add_submit(this);">
            <label>New thing: </label>
            <input type="text" name="todo" />
            <button type="submit">Add</button>
        </form>
        <ul id="todo-items">
            <li>1-1</li>
            <li>1-2</li>
        </ul>
    </div>
</body>

</html>

用户指南结论

太好了!我们成功了!已经解释了 mixt 的所有主要功能。您现在可以在自己的项目中使用 mixt

API

作为下一步,您可能想阅读 API 文档

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分发

mixt-1.2.3.tar.gz (204.7 kB 查看哈希值)

上传时间

构建分发

mixt-1.2.3-py2.py3-none-any.whl (194.0 kB 查看哈希值)

上传于 Python 2 Python 3

由以下提供支持