如何使用 StreamField 处理混合内容
• 最后修改:2024-05-05 • 阅读量:55
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_type
和 value
属性:
{% 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_type
和 value
属性的块对象序列,就像 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 可以包含无限数量的块。 StreamField
或 StreamBlock
上的 min_num
和 max_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_num
和 max_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 渲染覆盖此设置,您可以将 T4735T 参数传递给块,并给出要渲染的模板文件的文件名。这对于从 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_name
、 surname
、 photo
和 biography
本身被定义为块,因此也可以写为:
{% 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 %}
,但缩写形式更具限制性,因为它不传递来自调用模板的变量,例如 request
或 page
;因此,建议您仅将其用于不呈现自己的 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 数据类型的块(例如 CharBlock
和 IntegerBlock
),模板的生效位置存在一些限制。有关更多详细信息,请参阅 [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 迁移。