StreamField 提供了一种内容编辑模型,适用于不遵循固定结构的页面(例如博客文章或新闻报道),其中文本可能散布有副标题、图像、引述和视频。它还适用于更专业的内容类型,例如地图和图表(或者,对于编程博客,代码片段)。在此模型中,这些不同的内容类型表示为一系列“块”,这些“块”可以重复并以任何顺序排列。

有关 StreamField 的更多背景信息以及为何在文章正文中使用它而不是富文本字段,请参阅博客文章 Rich text fields and faster horses

StreamField 还提供丰富的 API 来定义您自己的块类型,范围从简单的子块集合(例如由名字、姓氏和照片组成的“人”块)到具有自己的编辑界面的完全自定义组件。在数据库中,StreamField 内容存储为 JSON,确保保留字段的完整信息内容,而不仅仅是其 HTML 表示形式。

使用 StreamField

StreamField 是一个模型字段,可以像任何其他字段一样在页面模型中定义:

from django.db import models

from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.images.blocks import ImageChooserBlock

class BlogPage(Page):
    author = models.CharField(max_length=255)
    date = models.DateField("Post date")
    body = StreamField([
        ('heading', blocks.CharBlock(form_classname="title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
    ], use_json_field=True)

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        FieldPanel('body'),
    ]

在此示例中, BlogPage 的正文字段被定义为 StreamField ,作者可以在其中从三种不同的块类型组成内容:标题、段落和图像,这些内容可以按任何顺序使用和重复。作者可用的块类型被定义为 (name, block_type) 元组列表:“名称”用于标识模板内的块类型,并且应遵循变量名称的标准 Python 约定:小写和下划线,无空格。

您可以在 [StreamField block reference] 中找到可用块类型的完整列表。

注意:
StreamField 并不是其他字段类型(例如 RichTextField)的直接替代品。如果您需要将现有字段迁移到 StreamField,请参考 迁移 RichTextFields 到 StreamField

注意:
虽然块 (block) 定义看起来类似于模型字段,但它们不是同一件事。块 (block) 只在 StreamField 中有效 —— 用它们代替模型字段是行不通的。

模板渲染 (Template rendering)

StreamField 为整个流内容以及每个单独的块提供 HTML 表示形式。要将此 HTML 包含到您的页面中,请使用 {% include_block %} 标记:

{% load wagtailcore_tags %}

    ...

{% include_block page.body %}

在默认渲染中,流的每个块都包装在 <div class="block-my_block_name"> 元素中(其中 my_block_name 是 StreamField 定义中给出的块名称)。如果您希望提供自己的 HTML 标记,则可以迭代字段的值,并依次在每个块上调用 {% include_block %}

{% load wagtailcore_tags %}

    ...

<article>
    {% for block in page.body %}
        <section>{% include_block block %}</section>
    {% endfor %}
</article>

为了更好地控制特定块类型的渲染,每个块对象提供 block_typevalue 属性:

{% load wagtailcore_tags %}

    ...

<article>
    {% for block in page.body %}
        {% if block.block_type == 'heading' %}
            <h1>{{ block.value }}</h1>
        {% else %}
            <section class="block-{{ block.block_type }}">
                {% include_block block %}
            </section>
        {% endif %}
    {% endfor %}
</article>

组合 blocks

除了直接在 StreamField 中使用内置块类型之外,还可以通过以各种方式组合子块来构造新的块类型。这方面的例子包括:

  • 一个“带标题的图像”块,由图像选择器和文本字段组成
  • “相关链接”部分,作者可以在其中提供任意数量的其他页面链接
  • 幻灯片块,其中每张幻灯片可以是图像、文本或视频,以任意顺序排列

一旦以这种方式构建了新的块类型,您就可以在使用内置块类型的任何地方使用它 - 包括将其用作另一个块类型的组件。例如,您可以定义一个图像库块,其中每个项目都是一个“带标题的图像”块。

StructBlock

StructBlock 允许您将多个“子”块组合在一起以呈现为单个块。子块作为 (name, block_type) 元组列表传递到 StructBlock

body = StreamField([
    ('person', blocks.StructBlock([
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ])),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

当读回 StreamField 的内容时(例如渲染模板时),StructBlock 的值是一个类似字典的对象,其键对应于定义中给出的块名称:

<article>
    {% for block in page.body %}
        {% if block.block_type == 'person' %}
            <div class="person">
                {% image block.value.photo width-400 %}
                <h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
                {{ block.value.biography }}
            </div>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

子类化 StructBlock

将 StructBlock 的子块列表放置在 StreamField 定义中通常很难阅读,并且很难在多个位置重用同一块。作为替代方案, StructBlock 可以进行子类化,将子块定义为子类的属性。上例中的“person”块可以重写为:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

然后可以按照与内置块类型相同的方式在 StreamField 定义中使用 PersonBlock

body = StreamField([
    ('person', PersonBlock()),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

Block 图标

在内容作者用于向 StreamField 添加新块的菜单中,每个块类型都有一个关联的图标。对于 StructBlock 和其他结构块类型,使用占位符图标,因为这些块的用途特定于您的项目。要设置自定义图标,请将选项 icon 作为关键字参数传递给 StructBlock ,或者传递给 Meta 类的属性:

body = StreamField([
    ('person', blocks.StructBlock([
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ], icon='user')),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'

有关可用图标的列表,请参阅我们的图标概述。特定于项目的图标也显示在样式指南中。

ListBlock

ListBlock 定义了重复块,允许内容作者插入任意数量的特定块类型的实例。例如,由多个图像组成的“图库”块可以定义如下:

body = StreamField([
    ('gallery', blocks.ListBlock(ImageChooserBlock())),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

当读回 StreamField 的内容时(例如渲染模板时),ListBlock 的值是子值列表:

<article>
    {% for block in page.body %}
        {% if block.block_type == 'gallery' %}
            <ul class="gallery">
                {% for img in block.value %}
                    <li>{% image img width-400 %}</li>
                {% endfor %}
            </ul>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

StreamBlock

StreamBlock 定义了一组子块类型,可以通过与 StreamField 本身相同的机制以任何顺序混合和重复。例如,支持图像和视频幻灯片的轮播可以定义如下:

body = StreamField([
    ('carousel', blocks.StreamBlock([
        ('image', ImageChooserBlock()),
        ('video', EmbedBlock()),
    ])),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

StreamBlock 也可以按照与 StructBlock 相同的方式进行子类化,其中子块被指定为类的属性:

class CarouselBlock(blocks.StreamBlock):
    image = ImageChooserBlock()
    video = EmbedBlock()

    class Meta:
        icon = 'image'

以这种方式定义的 StreamBlock 子类也可以传递给 StreamField 定义,而不是传递块类型列表。这允许设置一组通用的块类型以在多种页面类型上使用:

class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()


class BlogPage(Page):
    body = StreamField(CommonContentBlock(), use_json_field=True)

当读回 StreamField 的内容时,StreamBlock 的值是具有 block_typevalue 属性的块对象序列,就像 StreamField 本身的顶级值一样。

<article>
    {% for block in page.body %}
        {% if block.block_type == 'carousel' %}
            <ul class="carousel">
                {% for slide in block.value %}
                    {% if slide.block_type == 'image' %}
                        <li class="image">{% image slide.value width-200 %}</li>
                    {% else %}
                        <li class="video">{% include_block slide %}</li>
                    {% endif %}
                {% endfor %}
            </ul>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

限制块数

默认情况下,StreamField 可以包含无限数量的块。 StreamFieldStreamBlock 上的 min_nummax_num 选项允许您设置最小或最大块数:

body = StreamField([
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], min_num=2, max_num=5, use_json_field=True)

或者同样的:

class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        min_num = 2
        max_num = 5

block_counts 选项可用于设置特定块类型的最小或最大计数。它接受一个字典,将块名称映射到包含 min_nummax_num 之一或两者的字典。例如,要允许 1 到 3 个“标题”块:

body = StreamField([
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], block_counts={
    'heading': {'min_num': 1, 'max_num': 3},
}, use_json_field=True)

或者同样的:

class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        block_counts = {
            'heading': {'min_num': 1, 'max_num': 3},
        }

单个块的模板

默认情况下,每个块都使用简单、最少的 HTML 标记或根本不使用标记来呈现。例如,CharBlock 值呈现为纯文本,而 ListBlock 在 <ul> 包装器中输出其子块。要使用您自己的自定义 HTML 渲染覆盖此设置,您可以将 T4​​735T 参数传递给块,并给出要渲染的模板文件的文件名。这对于从 StructBlock 派生的自定义块类型特别有用:

('person', blocks.StructBlock(
    [
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ],
    template='myapp/blocks/person.html',
    icon='user'
))

或者,当定义为 StructBlock 的子类时:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        template = 'myapp/blocks/person.html'
        icon = 'user'

在模板内,块值可作为变量 value 访问:

{% load wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>
    {{ value.biography }}
</div>

由于 first_namesurnamephotobiography 本身被定义为块,因此也可以写为:

{% load wagtailcore_tags wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
    {% include_block value.biography %}
</div>

编写 {{ my_block }} 大致相当于 {% include_block my_block %} ,但缩写形式更具限制性,因为它不传递来自调用模板的变量,例如 requestpage ;因此,建议您仅将其用于不呈现自己的 HTML 的简单值。例如,如果我们的 PersonBlock 使用模板:

{% load wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>

    {% if request.user.is_authenticated %}
        <a href="#">Contact this person</a>
    {% endif %}

    {{ value.biography }}
</div>

那么当通过 {{ ... }} 标签渲染块时, request.user.is_authenticated 测试将无法正常工作:

{# 错误:#}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>
            {{ block }}
        </div>
    {% endif %}
{% endfor %}

{# 正确的: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>
            {% include_block block %}
        </div>
    {% endif %}
{% endfor %}

与 Django 的 {% include %} 标签一样, {% include_block %} 还允许通过语法 {% include_block my_block with foo="bar" %} 将附加变量传递到包含的模板:

{# In page template: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        {% include_block block with classname="important" %}
    {% endif %}
{% endfor %}

{# In PersonBlock template: #}

<div class="{{ classname }}">
    ...
</div>

还支持语法 {% include_block my_block with foo="bar" only %} ,以指定除 foo 之外的父模板中的任何变量都不会传递给子模板。

除了从父模板传递变量之外,块子类还可以通过重写 get_context 方法来传递自己的附加模板变量:

import datetime

class EventBlock(blocks.StructBlock):
    title = blocks.CharBlock()
    date = blocks.DateBlock()

    def get_context(self, value, parent_context=None):
        context = super().get_context(value, parent_context=parent_context)
        context['is_happening_today'] = (value['date'] == datetime.date.today())
        return context

    class Meta:
        template = 'myapp/blocks/event.html'

在此示例中,变量 is_happening_today 将在块模板中可用。当通过 {% include_block %} 标记呈现块时, parent_context 关键字参数可用,并且是从调用模板传递的变量的字典。

所有块类型(不仅仅是 StructBlock )都支持 template 属性。但是,对于处理基本 Python 数据类型的块(例如 CharBlockIntegerBlock ),模板的生效位置存在一些限制。有关更多详细信息,请参阅 [About StreamField BoundBlocks and values]。

自定义 (Customisations)

所有块类型都实现一个通用 API,用于呈现其前端和表单表示,以及在数据库中存储和检索值。通过对各种块类进行子类化并重写这些方法,可以进行各种自定义,从修改 StructBlock 表单字段的布局到实现组合块的全新方式。有关更多详细信息,请参阅 [How to build custom StreamField blocks]。

修改 StreamField 数据

StreamField 的值表现为一个列表,在将实例保存回数据库之前可以插入、覆盖和删除块。新项目可以作为 (block_type, value) 的元组写入列表 - 读回时,它将作为 BoundBlock 对象返回。

# 将第一个块替换为“heading”类型的新块
my_page.body[0] = ('heading', "My story")

# 删除最后一个块
del my_page.body[-1]

# 将富文本块附加到流中
from wagtail.rich_text import RichText
my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever after.</p>")))

# 将更新后的数据保存回数据库
my_page.save()

如果要在 StreamField 的值中使用继承自 StructBlock 的块,则可以将该块的值作为 python 字典提供(类似于块的 .to_python 方法所接受的内容)。

from wagtail import blocks

class UrlWithTextBlock(blocks.StructBlock):
   url = blocks.URLBlock()
   text = blocks.TextBlock()

# using this block inside the content

data = {
    'url': 'https://github.com/wagtail/',
    'text': 'A very interesting and useful repo'
}

# append the new block to the stream as a tuple with the defined index for this block type
my_page.body.append(('url', data))
my_page.save()

按名称检索块

StreamField 值提供了用于检索给定名称的所有块的 blocks_by_name 方法:

my_page.body.blocks_by_name('heading')  # 返回“标题”块的列表

不带参数调用 blocks_by_name 将返回类似 dict 的对象,将块名称映射到该名称的块列表。这在模板代码中特别有用,因为在模板代码中不可能传递参数:

<h2>Table of contents</h2>
<ol>
    {% for heading_block in page.body.blocks_by_name.heading %}
        <li>{{ heading_block.value }}</li>
    {% endfor %}
</ol>

first_block_by_name 方法返回流中给定名称的第一个块,如果未找到匹配块,则返回 None

hero_image = my_page.body.first_block_by_name('image')

first_block_by_name 也可以在不带参数的情况下调用,以返回类似 dict 的映射:

<div class="hero-image">{{ page.body.first_block_by_name.image }}</div>

自定义验证

可以通过覆盖块的 clean 方法将自定义验证逻辑添加到块中。有关更多信息,请参见 StreamField 验证。

迁移

由于 StreamField 数据存储为单个 JSON 字段,而不是排列在正式的数据库结构中,因此在更改 StreamField 的数据结构或与其他字段类型进行转换时,通常需要编写数据迁移。有关 StreamField 如何与 Django 的迁移系统交互的更多信息,以及将富文本迁移到 StreamField 的指南,请参阅 StreamField 迁移。