跳转到主要内容

Meteorish Python响应式前端

项目描述

Ryzom:用Python组件替换HTML模板

为什么?

因为虽然像Django这样的框架声称“模板包含一种受限制的语言,以避免HTML编码员自己伤害自己”,但另一方面,GoF(设计模式)声称装饰器模式是设计GUI最有效的模式,这实际上是React等框架成功的一个重要部分。

是什么?

Ryzom基本上提供Python组件,如果您启用WebSockets,还将提供“将Python代码编译为JS”和“数据绑定”(当数据库中的数据变化时,DOM会自动刷新)等前沿特性。

状态

目前处于Beta阶段,我们正在为一家为捍卫民主而战的非政府组织开源项目进行生产发布前的准备,该项目包含一个使用同态加密的在线投票平台,基本上是在microsoft/electionguard-python之上构建的Django项目。

演示

虽然Django不是Ryzom的要求,但我们目前只有一个Django演示应用程序

git clone https://yourlabs.io/oss/ryzom.git
sudo -u postgres createdb -O $UTF -E UTF8 ryzom_django_example
cd ryzom
pip install -e .[project]
./manage.py migrate
./manage.py runserver
# open localhost:8000 for a basic form
# open localhost:8000/reactive for databinding with django channels

# to run tests:
py.test

用法

HTML

内容

组件是负责渲染HTML标签的Python类。因此,它们可能包含内容(子元素)。

from ryzom.html import *

yourdiv = Div('some', P('content'))
yourdiv.render() == '<div>some <p>content</p></div>'

大多数组件应以*content作为第一个参数进行实例化,并且您可以在其中传递所需的所有子元素。这些内容将存入self.content,您也可以在实例化后更改它。

您还可以将组件作为关键字参数传递,在这种情况下,它们将具有“slot”属性,并将分配给self。

yourdiv = Div(main=P('content'))
yourdiv.main == P('content', slot='main')

特殊内容

任何未定义to_html方法的内联内容将被转换为字符串,并包裹在Text()组件内。

任何为None的内容将被从self.content中删除。

属性

HTML标签也有属性,我们提供了Pythonic API来访问它们。

Div('hi', cls='x', data_y='z').render() == '<div class="x" data-y="z">hi</div>'

如果您不希望在元素内容后包含属性,请注意您也可以将内容组件作为关键字参数传递。

支持声明式和继承

class Something(Div):
    attrs = dict(cls='something', data_something='foo')


class SomethingNew(Something):
    attrs = dict(addcls='new')  # how to add a class without re-defining


yourdiv = SomethingNew('hi')
yourdiv.render() == '<div class="something new" data-something="foo">hi</div>'

样式

样式可以声明在属性中,也可以单独声明。

class Foo(Div):
    style = dict(margin_top='1px')

# is the same as:

class Foo(Div):
    style = 'margin-top: 1px'

# is the same as:

class Foo(Div):
    attrs = dict(style='margin-top: 1px')
  • 类样式属性将被提取到CSS包中。
  • 实例样式属性将内联渲染。
  • 任何具有样式的组件也将渲染一个类属性。

SASS也适用,但Ryzom不会对其进行解析,而是由libsass直接渲染

class FormContainer(Container):
    sass = '''
    .FormContainer
        max-width: 580px
        .mdc-text-field, .mdc-form-field, .mdc-select, form
            width: 100%
    '''

JavaScript

此存储库提供了一个py2js分支,您可以使用它用Python编写JavaScript。您有三种方式可以在Python中编写JavaScript:“HTML方式”、“jQuery方式”和“WebComponent方式”。

但是,您必须理解,我们的目的是在Python中编写JavaScript,而不是像Transcrypt项目那样在JavaScript中支持Python。在我们的情况下,我们将限制自己使用JS和Python语言的一个子集,因此像Python __mro__或甚至是多重继承这样的功能将完全不受支持。

但是,您仍然可以在Python中编写JavaScript并生成一个JS包。

HTML方式

onclickonsubmitonchange等可以在Python中定义。它们将以目标元素作为第一个参数接收

class YourComponent(A):
    def onclick(element):
        alert(self.injected_dependency(element))

    def injected_dependency(element):
        return element.attributes.href.value

上面的代码将捆绑一个YourComponent_onclick函数、YourComponent_dependency函数以及递归的。

并且YourComponent将使用onclick="YourComponent_onclick(this)"进行渲染。

WebComponent: HTMLElement

以下定义了一个带有JS HTMLElement类的自定义HTMLElement,它将生成一个基本的Web组件。

class DeleteButton(Component):
    class HTMLElement:
        def connectedCallback(self):
            this.addEventListener('click', this.delete.bind(this))

        async def delete(self, event):
            csrf = document.querySelector('[name="csrfmiddlewaretoken"]')
            await fetch(this.attributes['delete-url'].value, {
                method: 'delete',
                headers: {'X-CSRFTOKEN': csrf.value},
                redirect: 'manual',
            }).then(lambda response: print(response))

这将生成以下JS,这将让浏览器负责组件的生命周期,请查阅window.customElement.define文档以获取详细信息。

class DeleteButton extends HTMLElement {
    connectedCallback() {
        this.addEventListener('click',this.delete.bind(this));
    }
    async delete() {
        var csrf = document.querySelector('[name="csrfmiddlewaretoken"]');
        await fetch(this.attributes['delete-url'].value,{
            method: 'delete',
            headers: {'X-CSRFTOKEN': csrf.value},
            redirect: 'manual'
        }).then(
            (response) => {return console.log(response)}
        );
    }
}

window.customElements.define("delete-button", DeleteButton);

在我看来,这真是太酷了。

但是有一个问题:目前,您必须像在Python中那样将第一个参数设置为self,这样编译器才知道这个函数是一个类方法,并且它不应该带有在ES6类中不起作用的function 前缀。

jQuery方式

您可以通过在组件中定义一个py2js方法来“jQuery方式”进行操作

class YourComponent(Div):
    def nested_injection():
        alert('submit!')

    def on_form_submit():
        self.nested_injection()

    def py2js(self):
        getElementByUuid(self.id).addEventListener('submit', self.on_form_submit)

这将使您的组件也渲染一个在script标签中的addEventListener语句,并且捆绑将包含on_form_submit函数。

捆绑

组件将依赖于它们的CSS和JS捆绑。在没有Django的情况下,您可以手动这样做

from ryzom import bundle

your_components_modules = [
    'ryzom_mdc.html',
    'your.html',
]

css_bundle = bundle.css(*your_components_modules)
js_bundle = bundle.js(*your_components_modules)

Django

INSTALLED_APPS

添加到settings.INSTALLED_APPS

'ryzom',            # add py-builtins.js static file
'ryzom_django',     # enable autodiscover of app_name.html
'ryzom_django_mdc', # enable MDC form rendering

TEMPLATES

虽然ryzom提供了将组件注册到模板名称的功能,但ryzom_django提供了模板后端,以便在Django中使用这些功能,在settings.py中添加模板后端如下所示

TEMPLATES = [
    {
        'BACKEND': 'ryzom_django.template_backend.Ryzom',
    },
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

此模板后端将允许两种用法

  • 用组件覆盖HTML模板名称
  • 使用点分方式为template_name指定组件导入路径,即template_name = 'yourapp.html.SomeThing'

为视图注册模板

目前,ryzom_django会自动发现(导入)任何应用中的html.py文件。因此,这是您定义所有使用ryzom.html.template的视图模板替换的地方。例如,为具有YourModel模型的django.views.generic.ListView设置默认模板

from ryzom_mdc import *

class BaseTemplate(Html):
    title = 'Your site title'


@template('yourapp/yourmodel_list.html', BaseTemplate)
class YourModelList(Ul)
    def __init__(self, **context):
        super().__init(*[Li(f'{obj}') for obj in context['object_list'])])

根据您想要的版本,从ryzom_mdc或从ryzom导入html模块。在注册模板时,您可以动态嵌套组件,以替换{% extends %}

您可以链尽可能多的父级,例如,您可以有一个“卡片”布局,设置小内容宽度

class CardLayout(Div):
    style='max-width: 20em; margin: auto'

@html.template('yourapp/yourmodel_form.html', BaseTemplate, CardLayout)
class YourModelForm(Form):
    def __init__(self, **context):
        super().__init__(
            CSRFInput(context['view'].request),
            context['form'],
            method="post",
        )

捆绑

ryzom_django应用提供了3个命令

  • ryzom_css:输出CSS包
  • ryzom_js:输出JS包
  • ryzom_bundle:在ryzom_bundle/static中写入bundle.jsbundle.css

此外,还有2个视图,JSBundleViewCSSBundleView,您可以在开发中使用,在urls.py中如下包含它们

from django.conf import settings

if settings.DEBUG:
    urlpatterns.append(
        path('bundles/', include('ryzom_django.bundle')),
    )

对于生产环境,您可以在运行collectstatic之前编写包

./manage.py ryzom_bundle
./manage.py collectstatic

然后,确保您使用ryzom_django中的Html组件或任何ryzom_django_*应用,它将自动包含它们。

表单

API

ryzom_django.forms修补了django.forms.BaseForm,增加了两个新方法

  • BaseForm.to_html():渲染HTML,使BaseForm对象像组件一样“吱吱作响”,也可以在非ryzom模板中使用,通过{{ form.to_html }}获取渲染

  • BaseForm.to_component():由to_html()调用,这是生成默认布局的地方,您可以重写它以自定义表单对象的渲染。它将返回每个boundfield的to_component()结果的CList(无标签组件列表)。

ryzom_django.forms修补了django.forms.BoundField,增加了两个新方法

  • BoundField.to_component():如果为字段小部件模板名称注册了组件模板,则将返回该模板的to_component(),在这种情况下,它将使用该模板的from_boundfield(boundfield)

  • BoundField.to_html():渲染HTML,使BoundField对象像组件一样“吱吱作响”。

因此,您可以通过重写to_component()方法来配置表单对象的渲染方式,并且也可以像组件一样使用BoundField对象

def to_component(self):
    return Div(
        H3('Example form!'),
        self['some_field'],  # BoundField quacks like a Component!
        Div(
            Input(type='submit'),
        )
    )

演示

示例Django项目在src/ryzom_django_example/中可用,示例代码在urls.py文件中。

支持的API

本节中记录的低级别文档在v1之前可能受到不友好的更改,因为我们仍在研究用例,请负责任地使用。

我们正在尝试保护以下Component方法,您可以在重构代码时覆盖它们

  • Component.context(*content, **context):在渲染之前更改上下文,以从内部组件向上冒泡新的上下文数据到父组件,旨在解决我们在jinja模板中遇到的相同问题。
  • Component.content_html(*content, **context):渲染内部HTML
  • Component.to_html(*content, **context):渲染外部和内部HTML

非线程安全

目前,组件不是线程安全的,因为其大部分渲染代码会以改变它再次渲染的方式改变self。一些核心组件代码在渲染过程中改变了self.content,例如在“特殊内容”:“None”的情况下。

线程安全是每次有人提出合并非线程安全代码时的一个积极讨论主题,但我们尚未确定这是一个问题,因为Python提供了许多更好的方法来组织代码。例如

在lambda中包裹声明

class YourView:
    to_button = lambda: YourButton()

而不是

class YourView:
    to_button = YourButton()

我们小心地处理新开发中的线程安全,但似乎很有必要能够在路上改变self

UNIX不是为了阻止其用户做愚蠢的事情而设计的,因为这也会阻止他们做聪明的事情。 —— Doug Gwyn

项目详情


下载文件

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

源代码分发

ryzom-0.7.11.tar.gz (93.2 kB 查看哈希值)

上传时间 源代码