Baserow 插件开发指南:实现自定义视图过滤器类型(View Filter Type)
【免费下载链接】baserowBuild databases, automations, apps & agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserow
视图过滤器(View Filter)允许用户按字段条件筛选视图中的行——只有满足全部过滤条件的行才会被显示。Baserow 将这些过滤条件抽象为可注册的“过滤器类型”(如 equal、contains、is lower than、is empty 等),并允许插件作者通过插件机制扩展全新的过滤器类型。本文基于仓库中的 view-filter-type 插件教程,结合后端ViewFilterType抽象类与前端ViewFilterType基类的真实源码实现,完整讲解如何从零实现一个自定义视图过滤器:包括后端get_filter生成 DjangoQ对象的核心逻辑、REST API 调用方式、前端实时匹配(matches)机制,以及两端注册流程的源码级细节,帮助你在 Baserow 插件中安全地扩展数据筛选能力。
1. 核心概念:过滤器类型是什么,为什么需要双端实现
一个视图过滤器由三部分组成:字段(field)、类型(type)、值(value)。过滤器实例存储在后端ViewFilter模型中,可以在 视图模型定义 中看到ViewFilterGroup与ViewFilter两个模型——它们支持分组(filter group)语义,用于构建 AND/OR 复合过滤条件。
过滤器类型本身是两端各一份代码的:
- 后端负责在数据库查询时把过滤器编译成 Django
Q对象并应用到 queryset,保证 API 返回的行是真正经过过滤的; - 前端负责在用户选择字段时展示可用的过滤器类型、渲染值输入组件,并在行数据被编辑后实时判断该行是否仍满足当前过滤器。
正如 官方教程 所述:前端需要一份同样的过滤逻辑,是因为当用户编辑某一行后,系统要立即判断该行是否还匹配视图当前过滤器;如果不匹配,就会向用户显示警告——若保存编辑,该行将被从视图中过滤掉。这种实时比较不能等待服务器往返,因此过滤逻辑必须在前后端各实现一次。
本文将以一个最简单的equal_to相等过滤器为例(该功能 Baserow 已内置,选它纯粹因为逻辑简单),完整走一遍插件开发流程。开发插件的目录结构建议参考 插件样板说明 与 插件入门。
2. 后端:实现 ViewFilterType 并返回 Django Q 对象
2.1 后端抽象类的真实定义
所有后端过滤器类型都必须继承ViewFilterType抽象类,它定义在 backend/src/baserow/contrib/database/views/registries.py。其核心契约如下:
type:过滤器类型的唯一标识字符串,前后端必须一致(例如'equal_to');compatible_field_types:声明该过滤器兼容哪些字段类型。列表中既可以写字面量字段类型名('text'),也可以写接收具体Field实例并返回布尔值的 callable,用于表达更细粒度的兼容规则(例如只兼容开启某属性的字段);get_filter(self, field_name, value, model_field, field):必须重写。它接收字段名、过滤值、Django 模型字段(model_field)与 BaserowField实例(field),需要返回一个Q对象(或OptionallyAnnotatedQ,见 field_filters.py)。返回的Q对象会被框架自动合并进视图的 queryset。
两个容易忽视的默认行为值得注意:
- 值不兼容时的兜底策略:基类提供了
default_filter_on_exception()方法,默认返回Q(pk__in=[])(registries.py),即当过滤值类型与字段不兼容时默认不返回任何行。你可以在子类中重写它来改变这一策略(例如返回Q()表示不过滤); - 字段兼容性的解析链路:
field_is_compatible(field)在检查时会先调用字段类型的get_compatible_filter_field_type(field)做“类型别名”解析——某些字段类型(如公式字段)可以把自己映射为另一种类型参与兼容性判断(registries.py)。这就是为什么内置的公式字段也能被普通文本过滤器使用。
2.2 完整示例:equal_to 过滤器
创建一个EqualToViewFilterType,声明它只兼容text字段:
# plugins/my_baserow_plugin/backend/src/my_baserow_plugin/view_filters.py from django.db.models import Q from baserow.contrib.database.views.registries import ViewFilterType class EqualToViewFilterType(ViewFilterType): type = 'equal_to' compatible_field_types = ['text'] def get_filter(self, field_name, value, model_field, field): value = value.strip() # 如果提供了空值,我们不做任何过滤。 if value == '': return Q() # 检查 model_field 是否能接受该值。 try: value = model_field.get_prep_value(value) return Q(**{field_name: value}) except Exception: pass return Q()get_filter的三个关键分支值得逐个理解:
| 分支 | 代码 | 含义 |
|---|---|---|
| 空值 | if value == '': return Q() | 空的Q()对象与任何条件组合都不产生约束,因此“未填写过滤值 = 不过滤”是 Baserow 过滤器的通用约定 |
| 值可转换 | model_field.get_prep_value(value)后Q(**{field_name: value}) | get_prep_value是 Django 模型字段的入口,负责把字符串值转换成数据库可比较的形式(如数值、日期解析);成功后用Q(**{field_name: value})生成等值比较 |
| 值不可转换 | except Exception: return Q() | 当值无法被目标字段接受时(例如给数值字段填了非法文本),示例选择退化为“不过滤”,从而避免 500 错误 |
2.3 注册到注册表
最后一步是在插件的AppConfig.ready()中把过滤器类型注册进view_filter_type_registry。该注册表的name为"view_filter",重复注册会抛出ViewFilterTypeAlreadyRegistered,未注册的类型查询会抛出ViewFilterTypeDoesNotExist(registries.py):
# plugins/my_baserow_plugin/backend/src/my_baserow_plugin/config.py from django.apps import AppConfig from baserow.core.registries import plugin_registry from baserow.contrib.database.views.registries import view_filter_type_registry class PluginNameConfig(AppConfig): name = 'my_baserow_plugin' def ready(self): from .plugins import PluginNamePlugin from .view_filters import EqualToViewFilterType plugin_registry.register(PluginNamePlugin()) view_filter_type_registry.register(EqualToViewFilterType())2.4 值得重写的进阶钩子
除了get_filter,基类还预留了若干可选方法(registries.py),在编写复杂过滤器时可以直接利用:
get_preload_values(view_filter):为展示目的预加载附加数据。内置的link_row_has过滤器就用它预取所选行的名称,供 API 序列化时展示;get_export_serialized_value(value, id_mapping)/set_import_serialized_value(value, id_mapping):当过滤值引用了内部 ID(如 select 选项、行 ID)时,在视图导出/导入(模板、复制表)过程中负责值的转换与 ID 重映射。视图的export_serialized流程会逐个过滤器调用前者(registries.py);time_sensitive:声明过滤结果是否依赖当前时间(例如 “today”“yesterday” 这类日期操作符),注册表会据此汇总时间敏感过滤器列表,供前端决定何时重新计算过滤结果。
3. 通过 REST API 使用新过滤器
后端过滤器类型创建后,即可通过 API 为某个视图(需先存在一个包含相应字段的 grid 视图)添加过滤器实例:
POST /api/database/views/{view_id}/filters/ Host: api.baserow.io Content-Type: application/json { "field": {field_id}, "type": "equal_to", "value": "Example" }等价 curl 命令:
curl -X POST -H 'Content-Type: application/json' -i \ https://api.baserow.io/api/database/views/{view_id}/filters/ \ --data '{ "field": {field_id}, "type": "equal_to", "value": "Example" }'请求体三个字段与ViewFilter模型一一对应:field指向被过滤的字段 ID,type必须是已注册的过滤器类型标识,value是过滤值(按字符串存储,由get_filter负责解释)。
过滤器创建后,刷新网格视图的列表接口即可只看到满足条件的行:
GET /api/database/views/grid/{view_id}/ Host: api.baserow.io Content-Type: application/jsoncurl -X GET -H 'Content-Type: application/json' -i \ https://api.baserow.io/api/database/views/grid/{view_id}/4. 前端:实现并注册 ViewFilterType
后端只负责“算得对”,前端负责“选得出、看得到、实时比对”。前端基类ViewFilterType定义在 web-frontend/modules/database/viewFilters.js,构造函数会基于getType()静态方法生成type,并在类型或名称缺失时直接抛错(viewFilters.js)。子类需要实现的方法:
| 方法 | 职责 | 基类默认行为 |
|---|---|---|
static getType() | 过滤器类型标识,必须与后端type一致 | 无,必须实现 |
getName() | 用户在下拉菜单中看到的显示名称 | 返回null(构造函数会因此报错) |
getInputComponent(field) | 返回处理过滤值输入的 Vue 组件,须遵循 v-model 原则;可按字段类型返回不同组件 | 返回null,即不显示任何输入框 |
getCompatibleFieldTypes() | 返回兼容的字段类型名列表,也可混合传入接收 field 的函数谓词 | 返回[],即不兼容任何字段 |
matches(rowValue, filterValue, field, fieldType) | 判断行的值是否满足过滤值,用于实时比对 | 抛出异常,必须实现 |
4.1 示例实现
// plugins/my_baserow_plugin/web-frontend/viewTypes.js import { ViewFilterType } from '@baserow/modules/database/viewFilters' import ViewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText' export class EqualViewFilterType extends ViewFilterType { static getType() { return 'equal_to' } getName() { return 'is 2' } getInputComponent() { // 处理值输入的组件,这里复用现有的文本输入组件, // 也可以自定义组件,组件需遵循 v-model 原则。 return ViewFilterTypeText } getCompatibleFieldTypes() { return ['text'] } matches(rowValue, filterValue) { if (rowValue === null) { rowValue = '' } rowValue = rowValue.toString().toLowerCase().trim() filterValue = filterValue.toString().toLowerCase().trim() return filterValue === '' || rowValue === filterValue } }注意matches中filterValue === ''返回true的写法:它与后端Q()“空值不过滤”的语义保持一致——过滤值未填时任何行都算“匹配”,这样用户编辑行时不会因为过滤器值为空而误触发“将被过滤”警告。
基类还提供了一批可直接复用的行为(viewFilters.js):serialize()会把type、name、compatibleFieldTypes序列化出去供 UI 使用;getDefaultValue(field)决定新建过滤器时的默认值(时区敏感过滤器可在此返回当前时区);prepareValue(value, field)在值提交前做转换(比如把日期选择器值拼成时区\0值\0操作符的三段式字符串);isAllowedInPublicViews()与isDeprecated()分别控制过滤器在公共视图中是否可用、是否从下拉框中隐藏(已废弃类型仍可用于既有过滤器)。
内置实现中的fieldIsCompatible(field)与getCompatibleFieldValue(field, valuesMap, notFoundValue)两个方法体现了“类型别名”机制的前端对应物:它们先通过字段类型的getCompatibleFilterFieldType(field)解析出规范类型再比对兼容列表(viewFilters.js),与后端field_is_compatible的get_compatible_filter_field_type链路对称——这正是第 2.1 节提到的公式字段能够被文本类过滤器使用的底层原因。
对于需要按字段类型切换输入组件与值解析逻辑的场景,仓库提供了一个中间基类SpecificFieldFilterType(viewFilters.js):它维护一张“字段类型 → 具体处理器(如NumberFieldViewFilterHandler、TextLikeFieldViewFilterHandler)”的映射,getInputComponent与值解析(parseRowValue/parseFilterValue)都委托给对应处理器。内置的EqualViewFilterType就继承自它(viewFilters.js),你的插件若要对多种字段类型做不同解析,可以参照这一模式。
4.2 前端注册
与 field type 教程 中的注册方式一致,前端通过app.$registry.register('viewFilter', ...)注册过滤器。内置过滤器就是在 web-frontend/modules/database/plugin.js 中这样注册的(先执行registerNamespace('viewFilter'),再逐个register):
// plugins/my_baserow_plugin/web-frontend/plugin.js import { PluginNamePlugin } from '@my-baserow-plugin/plugins' import { EqualViewFilterType } from '@my-baserow-plugin/viewFilters' export default (context) => { const { app } = context app.$registry.register('plugin', new PluginNamePlugin(context)) app.$registry.register('viewFilter', new EqualViewFilterType(context)) }5. 验证与检查清单
完成上述代码后,在视图中为某个 text 字段添加过滤器,应该能在过滤器类型下拉菜单中看到显示名称(示例中的is 2),并能输入文本值与字段值比较。可以按以下清单核对实现是否完整:
- 两端 type 一致:后端
EqualToViewFilterType.type与前端EqualViewFilterType.getType()都返回'equal_to'; - 两端兼容列表一致:后端的
compatible_field_types与前端的getCompatibleFieldTypes()都声明了['text'],否则会出现“前端可选、后端拒绝”或反向的错位; matches与get_filter语义一致:后端空值返回Q()(不过滤),前端空值返回true(全部匹配),两者对同一数据集的判定结果应当一致;- 注册无冲突:重复注册会触发
ViewFilterTypeAlreadyRegistered异常,开发时注意插件热重载; - 值的可移植性:若过滤值引用了内部 ID(选项、行),记得实现
get_export_serialized_value/set_import_serialized_value钩子,保证模板与复制导出的正确性。
6. 小结
自定义视图过滤器类型的开发路径可以概括为:后端继承ViewFilterType→ 实现get_filter返回Q→ 注册进view_filter_type_registry→ 前端继承同名ViewFilterType→ 实现getName/getInputComponent/getCompatibleFieldTypes/matches→ 通过register('viewFilter')注册。后端保证 API 数据层面的正确过滤(registries.py),前端保证交互展示与行编辑后的实时匹配警告(viewFilters.js),两者缺一不可。理解了compatible_field_types的 callable 扩展位、get_export_serialized_value等钩子以及前后端对称的“字段类型别名”机制之后,你就可以在 Baserow 插件中实现远超内置能力(如针对特殊字段结构、多值语义)的自定义过滤逻辑了。
【免费下载链接】baserowBuild databases, automations, apps & agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考