Wagtail API v2 使用指南:从数据拉取到字段定制的完整实战手册
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
本指南基于 Wagtail 官方文档 docs/advanced_topics/api/v2/usage.md 编写,系统讲解如何通过 Wagtail API v2 模块对外提供公开、只读、JSON 格式的内容接口,供移动端 App、站点前端或任何外部客户端消费。读完本文,你将掌握列表/详情端点的请求方式、响应结构、分页与排序、树位置过滤、全文搜索、国际化过滤、字段级定制(?fields)以及 HTML 路径定位页面等全部实战技能,并了解背后的源码实现机制。
API v2 概览:三个内置端点与只读契约
Wagtail API 模块对外暴露的是公开、只读、JSON 格式化的接口,适用于外部客户端(如移动 App)或站点自身的首屏渲染。要使用这些接口,站点需要先完成 API 模块的启用与端点注册(详见 Wagtail API v2 配置指南),注册后即可通过GET请求访问以下三个内置端点:
- 页面(Pages):
/api/v2/pages/ - 图片(Images):
/api/v2/images/ - 文档(Documents):
/api/v2/documents/
注意:实际可用的端点及其 URL 会因站点的 API 配置方式不同而有所差异。例如配置文档中的示例将页面端点挂载在
/api/v2/下,而你可以通过WagtailAPIRouter与register_endpoint()自由定制端点名称与挂载路径(见 docs/advanced_topics/api/v2/configuration.md 与 router.py)。
从源码角度,每个端点本质上是BaseAPIViewSet的子类,通过get_urlpatterns()生成三类 URL 模式(见 views.py):
- 列表视图:
path("", ..., name="listing"),对应listing_view - 详情视图:
path("<int:pk>/", ..., name="detail"),对应detail_view - 查找视图:
path("find/", ..., name="find"),对应find_view
三个内置端点类分别为:页面wagtail.api.v2.views.PagesAPIViewSet、图片wagtail.images.api.v2.views.ImagesAPIViewSet、文档wagtail.documents.api.v2.views.DocumentsAPIViewSet。图片与文档端点的查询集还会自动剔除用户不可见的受限收藏集(通过get_restricted_collection_ids实现,见 wagtail/images/api/v2/views.py 与 wagtail/documents/api/v2/views.py)。
获取内容:列表端点的请求与响应结构
对任一列表端点发起GET请求即可拉取内容。每个响应都包含两个顶层部分:
items:当前页返回的对象列表;meta.total_count:结果总数量,该计数不受分页影响。
典型响应如下:
GET /api/v2/endpoint_name/ HTTP 200 OK Content-Type: application/json { "meta": { "total_count": "total number of results" }, "items": [ { "id": 1, "meta": { "type": "app_name.ModelName", "detail_url": "https://api.example.com/api/v2/endpoint_name/1/" }, "field": "value" }, { "id": 2, "meta": { "type": "app_name.ModelName", "detail_url": "https://api.example.com/api/v2/endpoint_name/2/" }, "field": "different value" } ] }从实现上看,items与meta.total_count的组装发生在 pagination.py 的WagtailPagination.get_paginated_response()中:先对查询集执行queryset.count()得到total_count,再对切片后的数据序列化。这也解释了为什么total_count总是全量计数而与当前页无关。
列表请求默认只返回每个端点定义的"默认字段"(见下文"默认端点字段"一节)。源码中这一行为由BaseAPIViewSet.listing_default_fields决定(views.py),例如页面端点的列表默认字段为id、type、detail_url、title、html_url、slug、first_published_at。
自定义页面字段:用 ?type 参数解锁专属字段
Wagtail 站点通常包含多种页面类型,每种类型都有自己的字段集合。出于安全与性能考虑,pages端点默认只暴露公共字段(如title、slug)。
要访问自定义字段,需要通过?type参数指定页面类型。该参数有两个作用:
- 将结果过滤为仅包含该类型的页面;
- 使该类型上通过
api_fields显式导出的自定义字段在 API 中可用。
例如,假设在配置文档的示例中,blog.BlogPage模型通过api_fields导出了published_date、body、authors等字段(见 configuration.md),那么请求:
GET /api/v2/pages/?type=blog.BlogPage&fields=published_date,body,authors(name) HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 10 }, "items": [ { "id": 1, "meta": { "type": "blog.BlogPage", "detail_url": "https://api.example.com/api/v2/pages/1/", "html_url": "https://www.example.com/blog/my-blog-post/", "slug": "my-blog-post", "first_published_at": "2016-08-30T16:52:00Z" }, "title": "Test blog post", "published_date": "2016-08-30", "authors": [ { "id": 1, "meta": { "type": "blog.BlogPageAuthor", }, "name": "Karl Hobley" } ] }, ... ] }几点关键约束:
- 只有开发者显式导出的字段才能在 API 中使用。导出方式是在页面模型上定义
api_fields属性(详情见 配置文档)。 ?type支持多个页面类型,用逗号分隔(例如?type=blog.BlogPage,blog.ArticlePage),这是 v2 相对 v1 的新增能力。从源码看,page_models_from_string()负责解析逗号分隔的类型字符串,并校验每个模型都是Page的子类(utils.py)。- 图片/文档端点不适用此机制,因为它们各自只暴露单一模型;但如果项目自定义了图片/文档模型,同样可以通过
api_fields属性将自定义字段导出到 API。
从源码层面看,?type参数的处理发生在PagesAPIViewSet.get_queryset()(views.py):当只指定一个页面类型时,查询集会从Page基类切换到具体的页面模型(这样才能过滤到该模型的自定义api_fields);当指定多个类型时,则使用PageQuerySet.type(*models)进行多态查询。
分页:limit 与 offset
列表响应默认每页返回20条结果。可通过两个查询参数控制:
?limit:每页返回的数量(默认 20);?offset:跳过的结果数量。
例如,请求第 2 页(跳过前 20 条):
GET /api/v2/pages/?offset=20&limit=20 HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 50 }, "items": [ pages 20 - 40 will be listed here. ] }?limit可能存在最大值上限。该上限由项目设置中的WAGTAILAPI_LIMIT_MAX控制:
- 设置为数字:作为新的最大值;
- 设置为
None:禁用最大值检查(不设上限)。
分页的底层实现在 pagination.py:WagtailPagination.paginate_queryset()中默认limit_max取settings.WAGTAILAPI_LIMIT_MAX(缺省 20),默认limit为min(20, limit_max);当limit > limit_max时会抛出BadRequestError(HTTP 400:"limit cannot be higher than ...")。同时,offset与limit都必须是正整数(负数会触发校验错误),这是分页参数的第一道防线。
排序:?order 的四种用法
按单字段升序/降序
通过?order参数指定排序字段,默认升序:
GET /api/v2/pages/?order=title结果将按标题升序(a-z)排列。在字段名前加-号即可改为降序:
GET /api/v2/pages/?order=-title注意:排序是区分大小写的,因此在升序排列时,小写字母总是排在大写字母之后。
从源码看,OrderingFilter(filters.py)会先把order参数按逗号拆分,再逐个校验字段名是否属于可用的数据库字段;校验不通过会返回 400 错误(cannot order by '...' (unknown field))。
多字段连续排序
多个字段用逗号传入?order,即可实现连续排序:
GET /api/v2/pages/?order=title,-slug该请求会先按标题升序(a-z)排列,对于标题相同的记录再按 slug 降序(z-a)排列。源码中通过queryset.order_by(*validated_fields)将拆分后的字段列表直接传给 Django 的order_by。
随机排序
向?order传入random关键字,结果将以随机顺序返回。若没有缓存,每次请求返回的顺序都会不同:
GET /api/v2/pages/?order=random限制:随机排序时不能同时使用
?offset。因为随机顺序无法在多次请求之间保持一致,翻页请求可能返回与前一页重复的结果。源码中对此有显式校验:OrderingFilter检测到random与其他字段组合、或与offset同时出现时会抛出 400 错误(filters.py)。实现上随机排序由 Django ORM 的queryset.order_by("?")完成。
过滤:字段精确匹配与树位置过滤
任意字段的精确匹配
任何字段都可以作为过滤条件:以字段名作为查询参数名,以要匹配的值作为参数值。例如查找 slug 为 "about" 的页面:
GET /api/v2/pages/?slug=about HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 1 }, "items": [ { "id": 10, "meta": { "type": "standard.StandardPage", "detail_url": "https://api.example.com/api/v2/pages/10/", "html_url": "https://www.example.com/about/", "slug": "about", "first_published_at": "2016-08-30T16:52:00Z" }, "title": "About" }, ] }该功能由FieldsFilter(filters.py)实现,它有几个值得注意的细节:
- 过滤只对数据库字段开放(通过
get_available_fields(..., db_fields_only=True)获取可用字段集,type/detail_url等非数据库字段不可过滤); - 布尔字段支持
true/false/1/0四种取值(parse_boolean解析,见 utils.py); - 整型与外键字段会被转换为对应 Python 类型后再过滤,非法值返回 400;
- 标签字段(
TaggableManager)支持按逗号分隔的多个标签过滤; - 过滤值中不允许出现空字符(
\x00); locale虽然是数据库字段,但被单独抽出(由专门的LocaleFilter处理,避免与国际化过滤混淆)。
树位置过滤(仅 pages 端点)
页面可以依据其在页面树中的位置进行过滤,这在构建导航菜单、面包屑时非常实用。
?child_of:传入一个页面的 ID,结果仅包含该页面的直接子页面。例如构建主菜单时,传入首页 ID 并配合show_in_menus过滤:
GET /api/v2/pages/?child_of=2&show_in_menus=true HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 5 }, "items": [ { "id": 3, "meta": { "type": "blog.BlogIndexPage", "detail_url": "https://api.example.com/api/v2/pages/3/", "html_url": "https://www.example.com/blog/", "slug": "blog", "first_published_at": "2016-09-21T13:54:00Z" }, "title": "About" }, { "id": 10, "meta": { "type": "standard.StandardPage", "detail_url": "https://api.example.com/api/v2/pages/10/", "html_url": "https://www.example.com/about/", "slug": "about", "first_published_at": "2016-08-30T16:52:00Z" }, "title": "About" }, ... ] }?ancestor_of:传入一个页面的 ID,结果仅包含该页面的所有祖先(父页面、祖父页面……直到站点根页面)。与type过滤组合,可用于查找某个blog.BlogPage所属的blog.BlogIndexPage;单独使用则可以从当前页面回溯到站点根页面,构建面包屑导航。
?descendant_of:传入一个页面的 ID,结果仅包含该页面的所有后代(子页面、孙页面……)。
从源码看,三个过滤器分别由ChildOfFilter、AncestorOfFilter、DescendantOfFilter实现(filters.py)。其中child_of与descendant_of都支持传root关键字(会使用Site.find_for_request(request).root_page作为参照页),且descendant_of与child_of不能同时使用(源码会抛出 "filtering by descendant_of with child_of is not supported")。
按站点过滤页面
默认情况下,API 根据请求的 hostname(主机名)确定站点。当需要查询其他站点的页面时,使用?site=过滤器,其值要求是站点的已配置主机名。如果多个站点共用同一主机名但端口不同,可以用hostname:port格式按端口过滤:
GET /api/v2/pages/?site=demo-site.local GET /api/v2/pages/?site=demo-site.local:8080搜索:全文检索与 search_operator
向?search参数传入查询词即可对结果执行全文搜索。查询词会先按词边界拆分为多个"词项(terms)",再对每个词项做规范化处理(转小写、去重音符号)。例如:?search=James+Joyce(+号在 URL 中表示空格)。
search_operator:多词项的组合逻辑
search_operator参数决定多个词项如何组合,有两个可选值:
and:查询中的所有词项(排除停用词后)必须全部出现在每条结果中;or:查询中的至少一个词项出现在每条结果中即可。
or通常比and更好用,因为用户可以不必精确输入,而排序算法会保证不相关的结果不会排在前面。
默认操作符的选择取决于站点使用的搜索引擎是否支持相关性排序:
- 若支持排序(如 Elasticsearch),默认操作符为
or; - 若不支持(如数据库后端),默认操作符为
and。
正因如此,当?search与?order同时使用(这会禁用相关性排序)时,官方建议显式使用and操作符:
GET /api/v2/pages/?search=James+Joyce&order=-first_published_at&search_operator=and从源码看,SearchFilter(filters.py)是页面端点过滤器链中的最后一个(见 views.py 中的注释:"needs to be last, as SearchResults querysets cannot be filtered further")。它通过get_search_backend()获取当前搜索引擎,并把search_operator与是否按相关性排序(order_by_relevance,当 URL 中不存在order时为 True)传给搜索后端。此外:
- 若
WAGTAILAPI_SEARCH_ENABLED = False,使用?search会返回 400 "search is disabled"; - 标签过滤(
?tag=...)与搜索不能同时使用(源码会抛出 "filtering by tag with a search query is not supported"); - 对未索引的字段进行过滤或排序会返回 400 错误("cannot filter by ... while searching (field is not indexed)")。
国际化站点的专用过滤器
当设置WAGTAIL_I18N_ENABLED = True(详见 i18n 文档)时,pages 端点会额外提供两个过滤器。
按语言区域过滤:?locale
?locale=过滤器只返回指定 locale 的页面:
GET /api/v2/pages/?locale=en-us HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 5 }, "items": [ { "id": 10, "meta": { "type": "standard.StandardPage", "detail_url": "https://api.example.com/api/v2/pages/10/", "html_url": "https://www.example.com/usa-page/", "slug": "usa-page", "first_published_at": "2016-08-30T16:52:00Z", "locale": "en-us" }, "title": "American page" }, ... ] }LocaleFilter(filters.py)会按language_code查找Locale模型,再执行queryset.filter(locale=locale)。值得一提的是,locale字段是否出现在默认响应中与国际化开关联动:PagesAPIViewSet会在WAGTAIL_I18N_ENABLED开启时把locale追加到列表默认字段,关闭时则从详情默认字段中移除(views.py)。
获取某页面的翻译版本:?translation_of
?translation_of=过滤器接收一个页面 ID,只返回该页面的各语言翻译版本:
GET /api/v2/pages/?translation_of=10 HTTP 200 OK Content-Type: application/json { "meta": { "total_count": 2 }, "items": [ { "id": 11, "meta": { "type": "standard.StandardPage", "detail_url": "https://api.example.com/api/v2/pages/11/", "html_url": "https://www.example.com/gb-page/", "slug": "gb-page", "first_published_at": "2016-08-30T16:52:00Z", "locale": "en-gb" }, "title": "British page" }, { "id": 12, "meta": { "type": "standard.StandardPage", "detail_url": "https://api.example.com/api/v2/pages/12/", "html_url": "https://www.example.com/fr-page/", "slug": "fr-page", "first_published_at": "2016-08-30T16:52:00Z", "locale": "fr" }, "title": "French page" }, ] }实现上,TranslationOfFilter(filters.py)通过queryset.translation_of(page)完成过滤。注意:当?translation_of与?child_of组合使用时,源码会保留child_of的父页面标记(_filtered_by_child_of),确保后续逻辑(如页面资源管理器)仍能获取正确的父页面。
字段选择:?fields 参数完全指南
默认情况下,响应只返回可用字段的一个子集。?fields参数既能添加额外字段,也能移除不需要的默认字段,是 API 调优的核心工具。
添加额外字段
将?fields设为要添加字段的逗号分隔列表。例如?fields=body,feed_image会在响应中追加body与feed_image两个字段。
该能力同样适用于跨关系嵌套字段:?fields=body,feed_image(width,height)会把图片的width、height嵌套进feed_image的表示中。嵌套语法可以递归使用(如authors(name,photo(url)))。
添加全部字段
将?fields设为星号(*)会加入所有可用字段,非常适合用来"发现"模型到底导出了哪些字段:
GET /api/v2/pages/?fields=*移除字段
在字段名前加-前缀即可移除你知道不需要的默认字段。例如?fields=-title,body表示移除title、添加body。该语法可与星号组合:?fields=*,-body表示添加全部字段但排除body。
移除全部默认字段
如果你希望精确定义需要的字段,可以把?fields的第一项设为下划线(_),这会移除所有默认字段。例如?fields=_,title只返回title字段。
从实现角度看,?fields的解析由parse_fields_parameter()(utils.py)完成,它使用严格的语法(不允许空白字符)把参数字符串解析为三元组列表:字段名、是否取反(negated)、嵌套字段列表。随后_get_serializer_class()(views.py)根据解析结果决定字段集合,并注意以下语法规则:
*与_必须位于第一位,且不能取反;*后面只允许带子字段或取反字段(如*,foo(bar)、*,-foo合法,*,foo非法);_与取反字段不能组合(_ ,-foo非法);- 取反字段不能带嵌套子字段;
- 非关联字段不能带子字段(会返回 "'xxx' does not support nested fields");
- 未知字段名会返回 400 "unknown fields: ..."。
对于关联字段,_get_serializer_class()还会通过路由器的get_model_endpoint()找到关联模型的端点类,递归为其生成嵌套序列化器;对于ParentalKey关联的子模型(内联模型),默认会嵌套显示全部字段。这就是为什么?fields=authors(name)能精确控制子对象字段的原因。
详情视图:获取单个对象
在端点 URL 后追加对象 ID 即可获取单个对象:
- 页面:
/api/v2/pages/1/ - 图片:
/api/v2/images/1/ - 文档:
/api/v2/documents/1/
详情视图默认返回所有已导出字段。同样可以使用?fields定制显示哪些字段。例如/api/v2/pages/1/?fields=_,title,body只返回 id 为 1 的页面的title和body两个字段。
源码中,详情视图由detail_view处理(views.py),get_serializer_class()会根据 action 是否为listing_view决定show_details标志:详情视图会包含"仅详情字段"(如页面的meta.parent,见detail_only_fields = ["parent"]),列表视图则不会。此外,PagesAPIViewSet.get_object()会返回页面的specific版本,确保序列化的是具体页面类型而非基类Page。
按 HTML 路径查找页面:find 视图
使用/api/v2/pages/find/?html_path=<path>可以根据页面的 HTML 路径定位单个页面:
GET /api/v2/pages/find/?html_path=/about/该视图返回两种结果:
302重定向响应:跳转到该页面的详情视图;404未找到响应:路径对应的页面不存在。
例如/api/v2/pages/find/?html_path=/总是重定向到站点首页的详情视图。重定向时会保留除查找参数外的其余查询参数,因此可以链式追加?fields=...等定制参数。
实现上,PagesAPIViewSet.find_object()(views.py)会通过Site.find_for_request(request)定位站点,将html_path拆分为路径组件后调用site.root_page.specific.route(request, path_components)完成页面路由;find_view(views.py)随后生成指向详情视图的绝对 URL 并返回 302。另外,所有端点还支持通过?id=在 find 视图中按 ID 查找对象。
默认端点字段速查
下表汇总各端点默认返回的字段,供开发时对照。
公共字段(所有端点)
id(数字):对象的唯一 ID。注意:除页面类型外,其他内容类型各自拥有独立的 ID 空间,因此必须将
id与type字段组合使用,才能得到对象的全局唯一标识。type(字符串):对象的类型,格式为app_label.ModelName。detail_url(字符串):该对象详情视图的 URL。
公共字段由BaseAPIViewSet定义(body_fields = ["id"]、meta_fields = ["type", "detail_url"],见 views.py),type与detail_url在 serializers.py 中分别由TypeField/PageTypeField与DetailUrlField序列化。
页面端点
title(字符串)meta.slug(字符串)meta.show_in_menus(布尔值)meta.seo_title(字符串)meta.search_description(字符串)meta.first_published_at(日期/时间)
以上字段的值取自页面模型上对应的字段。
meta.html_url(字符串):如果站点存在由 Wagtail 生成的 HTML 前端,该字段为页面的 URL。它由PageHtmlUrlField序列化,内部调用page.full_url(见 serializers.py)。meta.parent:嵌套返回父页面的部分信息(仅详情视图可用,属detail_only_fields)。由PageParentField实现,页面没有真正的parent字段,序列化时通过instance.get_parent()查找,并校验父页面在当前用户可见的查询集中(serializers.py)。meta.alias_of(字典):若页面被标记为别名(alias),返回原始页面的 ID 与完整 URL。由PageAliasOfField实现(serializers.py)。
图片端点
title(字符串):图片标题字段的值。在 Wagtail 中,该值用作图片的altHTML 属性。width(数字)/height(数字):原始图片文件的尺寸。meta.tags(字符串列表):与图片关联的标签列表。
图片端点的字段在 wagtail/images/api/v2/views.py 中配置(body_fields追加title、width、height,meta_fields追加tags、download_url)。tags由TagsField序列化为按名称排序的字符串列表(serializers.py)。
文档端点
title(字符串):文档标题字段的值。meta.tags(字符串列表):与文档关联的标签列表。meta.download_url(字符串):文档文件的下载 URL。
文档端点的字段在 wagtail/documents/api/v2/views.py 中配置。生成download_url的绝对地址时依赖WAGTAILAPI_BASE_URL设置:若未设置,会回退使用当前请求的主机名;但配合wagtailfrontendcache前端缓存失效模块使用时,WAGTAILAPI_BASE_URL必须显式设置(因为缓存失效场景不存在"当前请求")(见 configuration.md)。
自 v1 以来的变化
破坏性变更(Breaking changes)
- 列表响应的结果数组由 v1 的
pages/images/documents统一改名为items。
主要新特性(Major features)
?fields参数能力大幅增强:支持移除字段、添加全部字段、以及自定义嵌套字段。
次要新特性(Minor features)
- 页面端点新增
html_url、slug、first_published_at、expires_at、show_in_menus字段; - 文档端点新增
download_url字段; - 页面端点的
type参数支持同时指定多个页面类型; - 布尔字段过滤现在支持
true与false(同时兼容1/0,见 utils.py); order可以与search组合使用;- 新增
search_operator参数。
小结
Wagtail API v2 是一套设计克制、语法统一的只读 JSON 接口:limit/offset控制分页,order支持升序、降序、多字段与随机排序,search与search_operator提供全文检索,child_of/ancestor_of/descendant_of/site覆盖树位置与多站点场景,locale/translation_of服务国际化站点,而?fields的+/-/*/_四类操作符让客户端可以精确裁剪响应体积。所有这些行为都能在 wagtail/api/v2 目录下的 views.py、filters.py、pagination.py、serializers.py 与 utils.py 中找到对应实现,配合 配置指南 即可在生产站点中快速落地一套安全、高效的内容 API。
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考