- 数据库
- 后端
- 前端
- 协作
【免费下载链接】mathesar
An intuitive spreadsheet-like interface that lets users of all technical skill levels view, edit, query, and collaborate on Postgres data directly. 100% open source and self hosted, with native Postgres access control.
导读:本文系统梳理 Mathesar 前端(
mathesar_ui/)的核心架构模式,涵盖服务端预渲染数据注入(commonData)、Svelte Store 的三大状态管理模式、Store 向组件传递的三种方式,以及 API 层、组件库、表单库与图标系统的组织设计。读完本文,你将能快速理解 Mathesar 前端代码的组织逻辑,掌握其推荐的 Reactivity 模式与代码演进方向,为二次开发或贡献代码打下基础。
概述:为什么要读这份架构文档
Mathesar 是一个开源的、以电子表格界面操作 Postgres 数据库的应用。其前端代码库规模庞大,包含大量组件与状态管理逻辑。本仓库的mathesar_ui/ARCHITECTURE.md正是为帮助新人快速上手而编写的架构文档,它记录了当前代码库中被推荐使用的主要模式,并明确指出:代码库中还存在着大量"不再推荐、甚至未在文档中记录"的旧模式。因此,本文既是一份架构说明书,也是一份"代码演进路线图"——阅读时若能结合源码中对应的实现,将事半功倍。
注意:文中所引用的源码路径均以仓库根目录为起点,例如
mathesar_ui/src/stores/table-data/filtering.ts对应仓库中的mathesar_ui/src/stores/table-data/filtering.ts。
渲染架构与公共数据(Common Data)
请求流程:Django 服务端首屏渲染
当你首次在浏览器地址栏访问 Mathesar 的根路由时,初始请求由Django 服务端处理,根据用户的登录状态返回对应的 HTML 文件:
- 未认证用户:返回匿名访问页面;
- 已认证用户:返回带预注入数据的页面。
对于已认证用户,Django 会把一组 JSON 数据直接嵌入 HTML 内部,放置在一个id为common-data的 HTML 标签中(项目内部称之为commonData)。这一机制在前端首次加载时,可直接用预置数据渲染 UI,无需再向服务端发起额外的往返请求,从而显著加快首屏速度。
具体实现可参见模板文件mathesar/templates/mathesar/index.html:
{{ common_data|json_script:"common-data" }} {% if route_specific_data %} {% for key, value in route_specific_data.items %} {{ value|json_script:key }} {% endfor %} {% endif %}前端如何读取commonData
前端的解析逻辑集中在mathesar_ui/src/utils/preloadData.ts(即架构文档中提到的preload.ts的当前实现文件):
preloadCommonData():通过document.querySelector('#common-data')定位标签,然后JSON.parse其文本内容,返回CommonData类型的结构化对象;preloadRouteData<T>(routeName):按路由名读取#routeName对应的数据,支持路由级数据预加载;canViewDbPermissionsAndSettings()与getFileStorageBackend():则基于commonData中的routing_context、is_superuser、per_user_databases_enabled、file_backends等字段做业务判断。
CommonData类型分为AuthenticatedCommonData与AnonymousCommonData两种形态。认证态下包含databases、servers、schemas、tables、queries、current_database、user、routing_context等字段;匿名态下则只有routing_context: 'anonymous'。此外,基类字段还包含current_release_tag_name、supported_languages、is_authenticated、is_sso_login_required、per_user_databases_enabled、file_backends等全局信息。
值得注意的是,预加载的schemas/tables数据带有WithStatus<...>包装(state: 'success' | 'failure'),这意味着服务端预取接口可能成功也可能失败,前端必须处理这两种状态。这一点在mathesar_ui/src/stores/schemas.ts中体现得很明显——当commonData.schemas.state === 'success'时,直接调用setSchemasInStore将预加载数据灌入 store;否则记录失败状态并回退到fetchSchemasForCurrentDatabase()重新拉取。
Store 模式:Mathesar 的状态管理方法论
Mathesar 以Svelte Stores作为主要状态管理工具。单独的 store 虽然简单,但在管理复杂状态时,项目演化出了若干模式,架构文档将其归纳为三大类:
模式一:不可变类模式(Coarse Reactivity,粗粒度响应式)
核心思路:一个 writable store 中保存一个自定义不可变类的实例,通过类方法返回新实例来更新 store。
- 更新方式:
pizza.update((p) => p.withTopping('mushrooms'));优点:
- 类本身保持简单;
- 类的单元测试可以声明式编写,无需按顺序逐步构造状态;
- 构造函数可以集中承担复杂校验与恢复逻辑,从而天然避免非法状态。
仓库实例:见
mathesar_ui/src/stores/table-data/filtering.ts中的Filtering类。其构造函数把RawFilterGroup编译为SqlExpr(SQL 表达式树),并计算出appliedFilterCount;其上的withoutColumns、withContextualFilters、terse、fromTerse等方法全部返回新的Filtering实例。该文件还展示了新旧两代过滤系统的兼容层:旧的RawIndividualFilter被转换为新的SqlExpr结构(individualFilterToSqlExpr),覆盖equal、greater_or_equal、json_array_contains、not_null、email_domain_equals、uri_scheme_equals等二十余种过滤条件。
模式二:可变类模式(Granular Reactivity,细粒度响应式)
核心思路:自定义类的某个属性持有一个 writable store 的实例,类提供方法去更新该 store;一个类可以持有多个这样的 store。
- 更新方式:
pizza.addTopping('mushrooms');- 优点:
- 消费端代码更简洁;
- 响应式粒度更细,对性能或 UX 敏感的场景至关重要。
该模式按成员可见性还可以细分出三个子模式:
| 子模式 | 说明 | 仓库示例 |
|---|---|---|
| Public(公开) | 底层 store 直接公开,消费者可绕过类方法直接修改 store。团队希望逐步避免此模式,以减少 bug 机会。 | mathesar_ui/src/stores/table-data/records.ts中的RecordsData |
| Publicly readable, privately writable(公开可读、私有可写) | 借助MakeWritablePropertiesReadable工具类型在编译期强制执行只读约束。团队希望未来更多采用此模式。 | mathesar_ui/src/stores/users.ts中的WritableUsersStore(对外暴露为UsersStore类型) |
| Private(私有) | 底层 store 完全私有,另外派生若干只读 store 对外暴露略作修改的数据。 | 架构文档示例为RecordSummaryStore(两个私有 store 合并为一个公开可读 store) |
其中MakeWritablePropertiesReadable<T>的实现位于mathesar_ui/src/utils/typeUtils.ts:它通过ChangeWritableToReadable将Writable<U>映射为Readable<U>,从而让所有 Svelte store 属性在公开类型中变为只读。这一技巧在编译期保证了"类内部可写、外部只读"的封装性,是团队当前推荐的演进方向。
模式三:嵌套 Store 模式(Granular Reactivity,细粒度响应式)
核心思路:一个 writable store 中可以持有一个或多个writable store(中间还可以夹杂其他数据)。这是最复杂、最"定制化"的模式,因而难以统一归类,架构文档给出了两个典型实例:
TabularData(mathesar_ui/src/stores/table-data/tabularData.ts):一个类内部聚合了大量 store(meta、columnsDataStore、constraintsDataStore、recordsData、display、selection等),同时这个类的实例本身又被setTabularDataStoreInContext包装进一个 writable store 并放入 Svelte context。组件层通过"响应式解构"来处理这一层间接性:
const tabularData = getTabularDataStoreFromContext(); $: ({ isLoading } = $tabularData); $: yadaYada = $isLoading;Form表单系统(mathesar_ui/src/components/form/form.ts):一个外层 store 内嵌"每个字段一个 store"。它利用组件系统之外的unite工具,把许多内层 store 合并成一个外层 store。unite的实现要点是:为每个内层 store 订阅并在数组中按索引回填值,且当最后一个订阅者取消订阅时自动解除所有内层订阅,从而避免内存泄漏。
此外,storeUtils.ts还提供了其他几个关键工具:
collapse:将"外层 store 套内层 store"折叠为单一 store(derived(outerStore, (inner) => inner.subscribe(set)));reactiveSort:对"元素内部又含可读 store"的集合做响应式排序;withSideChannelSubscriptions:为 store 附加"旁路订阅",在第一个订阅者出现时建立、在最后一个订阅者离开时统一销毁——Form的disabled状态联动(请求处理中禁用所有字段)正是借助它实现的;asyncDynamicDerived:动态收集依赖并派生值,支持tick()等待微任务队列中的更新完成。
将 Store 传递给组件:三种方式
| 传递方式 | 机制 | 适用场景 | 现状与建议 |
|---|---|---|---|
| 通过 props | 组件只能从直接父组件接收 store 实例 | 层级较浅、最常见的平凡场景 | 作为基线方案 |
| 通过 context | 使用 Svelte 的 Context API,组件可从任意祖先接收 store 实例 | props 需要穿透很多层组件层级时 | 多少层算"太多"没有硬性规则,不确定时建议先用 props,需要时再升级为 context |
| 通过 ESM imports | store 实例是全局单例,任何导入它的组件共享同一实例 | 全应用只有唯一实例的场景(如 toast 系统) | 早期代码使用较多,团队希望今后少用,以避免共享可变全局状态带来的问题(如 PR #2213 讨论中的问题) |
作为约定,Mathesar 将 Svelte 原生的getContext/setContext封装进自定义工具函数,使消费端代码更简洁、类型更友好。典型例子是mathesar_ui/src/stores/userProfile.ts的getUserProfileStoreFromContext/setUserProfileStoreInContext。同时可以注意到,context 的 key 使用Symbol或模块级私有对象(如tablularData.ts中的const tabularDataStoreContextKey = {}),并在重复setContext时抛出错误,避免覆盖冲突。
而 toast 系统的全局单例实现位于mathesar_ui/src/stores/toast.ts:
import { makeToast } from '@mathesar/component-library'; export const toast = makeToast();重点 Store:TabularData(表格数据的汇聚点)
TabularData(mathesar_ui/src/stores/table-data/tabularData.ts)是理解 Mathesar Store 体系的最佳切入点,它承担了表格渲染时"万流归宗"的职责。构造函数接收database、table、meta、contextualFilters、hasEnhancedPrimaryKeyCell、loadIntrinsicRecordSummaries等 props,并在内部组装:
Meta:排序(sorting)、分组(grouping)、过滤(filtering)、隐藏列(hiddenColumns)、联结(joining)等"用户视图状态";ColumnsDataStore:列定义与元数据;ConstraintsDataStore:约束信息;RecordsData:记录(行)数据,包含分页、搜索、行选择等逻辑;Display:展示相关状态;- 派生 store 群:
processedColumns(处理后的列)、hasPrimaryKey、canSelectRecords/canInsertRecords/canUpdateRecords/canDeleteRecords(基于当前角色的表级/列级权限推导)、joinableTables(可联结表,通过AsyncRpcApiStore调用api.tables.list_joinable)、joinedColumns、allColumns、displayedColumns、selection(基于Plane构造的电子表格选区)以及isLoading、selectedCellData等。
TabularData还通过事件机制联动各子 store:例如列重命名后触发refresh(),列删除后同步清理meta.sorting/meta.filtering/meta.grouping/meta.hiddenColumns中的相关状态并重新拉取约束与可联结表;refresh()方法会并行执行列、记录、约束与可联结表的全量拉取;destroy()则负责销毁全部子 store 并取消未完成的请求,防止内存泄漏。
该文件同时展示了"context 包装"的完整链路:setTabularDataStoreInContext将TabularData实例包进writable并setContext;组件侧getTabularDataStoreFromContext取出该 store,再通过响应式解构($: ({ isLoading } = $tabularData))获取内部数据。
与 API 的交互:从"耦合"到"分离"
架构文档明确指出:目前大部分代码中 API 调用与 store 是紧耦合的,例如 schemas store(mathesar_ui/src/stores/schemas.ts)就直接在fetchSchemasForCurrentDatabase、createSchema、deleteSchema等函数中调用api.schemas.list(...)、api.schemas.add(...)并直接操作内部 store。
不过,随着 RFC(users and permissions 规范提案)的推进,API 调用正在向src/api目录剥离,形成新的组织约定:
- 存在
mathesar_ui/src/api/目录,内含src/api/types与src/api/utils;新文件都将放在src/api/下。 - 每个工具文件同时导出其相关类型。最佳示例是
mathesar_ui/src/api/rpc/users.ts:它定义了BaseUser、User、UserDef类型,并通过rpcMethodTypeContainer声明list、get、add、delete、patch_self、patch_other、password.replace_own、password.revoke等 RPC 方法的输入输出类型。
实际目录结构(从仓库源码可见)分为两层:
mathesar_ui/src/api/ ├── rest/ │ ├── types/dataFiles.ts │ ├── utils/{errorCodes,errors,recordUtils,requestUtils}.ts │ ├── bulkInsert.ts │ ├── dataFiles.ts │ └── fileAttachments.ts └── rpc/ ├── _common/{columnDisplayOptions,commonTypes}.ts ├── analytics.ts / collaborators.ts / columns.ts / constraints.ts ├── data_modeling.ts / databases.ts / explorations.ts / forms.ts ├── records.ts / roles.ts / schemas.ts / servers.ts / tables.ts / users.ts └── index.ts在 store 侧,users.ts中的WritableUsersStore展示了新风格:它把api.users.list()、api.users.delete(...)封装为类的私有方法,配合requestStatus描述请求状态,这正是架构文档中"publicly readable, privately writable"模式的落地实例。
组件库(Component Library)
Mathesar 构建在自定义 UI 组件库之上,该库位于mathesar_ui/src/component-library。其代码结构约定如下:
src component-library <component-name> __meta__ 组件 playbook stories 与测试 ComponentName.svelte ComponentName.scss每个组件目录下,__meta__子目录存放该组件的 playbook 故事与测试;.svelte文件是组件实现,.scss是样式文件。
更多细节见仓库内mathesar_ui/src/component-library/README.md,其中还包含开发约束:
- 库内文件不得导入库目录之外的文件;
- 组件样式不得依赖库目录之外的样式;
- 所有组件样式应为全局样式,class 名应静态而非自动生成;
- 自 PR #3214 起不再使用 Storybook 开发组件,故事将迁移到新的组件 playbook(跟踪于 #3215)。
从目录结构看,该库包含了button、dropdown、modal、toast、form-builder、data-type-based-input、number-input、date-time-picker、select、tabs、pagination、skeleton等数十个通用组件,并有统一的mathesar_ui/src/component-library/index.ts出口。
表单库(Form Library)
Mathesar 使用自研的表单创建与校验库,位于mathesar_ui/src/components/form。核心入口是mathesar_ui/src/components/form/form.ts中的makeForm:
- 它接收
fields(字段 store 的集合)与可选的comboValidators(跨字段组合校验器); - 通过
unite把各字段 store 合并为valuedFields,并派生values、isValid、canSubmit、hasChanges等表单级状态; - 字段的
isValid/canSubmit/hasChanges又被unite聚合后逐一推导出表单整体状态; - 通过
withSideChannelSubscriptions订阅requestStatus,在提交(processing)期间自动将全部字段置为disabled,并在取消订阅时安全清理; - 对外返回
{ ...store, fields, reset, clearServerErrors, requestStatus, isSubmitting }。
原架构文档建议,若需深入了解"示例表单"与"工作原理",可查阅 PR #1932 的说明。在仓库中还可以参考mathesar_ui/src/components/form/field.ts(字段 store 的定义)与validators.ts(校验器),以及位于mathesar_ui/src/systems/data-forms/的表单相关组件实现。
图标系统(Icons)
Mathesar 全应用使用SVG 图标。component-library中的Icon.svelte组件负责根据给定的 svg 数据渲染正确图标,其 props 包括:
data:Font Awesome 风格的图标定义([viewBoxWidth, viewBoxHeight, ..., path]元组);size:尺寸(默认1em);spin/pulse:旋转/脉冲动画;flip/rotate:翻转与旋转(vertical/horizontal/both,角度90/180/270);label/title:可访问性标签与 tooltip;hasNotificationDot:角标圆点。
Font Awesome 图标的存放与命名
所有图标都以"概念"命名,而非外观(例如用iconCancel表达"取消"语义而非"向左箭头")。图标集中存放在两个位置:
- 组件库图标:
mathesar_ui/src/component-library/common/icons.ts(如iconCancel、iconChooseItemManyAhead等); - 应用代码图标:
mathesar_ui/src/icons/index.ts(约 305 行,涵盖数据库、表格、过滤、导出、用户、文件类型等应用级图标)。
命名约定详见 issue #1187 的讨论。
自定义图标
少数场景需要使用自定义图标(非 Font Awesome),其 SVG 数据存放在mathesar_ui/src/icons/customIcons.ts,同样通过Icon组件渲染。仓库中可以看到arrayIcon、outcomeIcon、tableIcon等自定义图标定义,数据格式与 Font Awesome 兼容([width, height, [], '', path]结构),因此可以被Icon组件无缝消费。
结语:架构演进的三个方向
综合架构文档与仓库源码,可以总结出 Mathesar 前端架构的三个演进方向,这也正是新贡献者应该遵循的准则:
- 状态管理:从"公开 writable store / ESM 全局单例"走向"不可变类 + 细粒度 store 封装 + 编译期只读约束(
MakeWritablePropertiesReadable)",通过更严的封装减少跨组件耦合与意外修改; - API 层:从"store 内联 API 调用"走向"
src/api集中管理、类型随文件导出"的分离式设计; - 组件体系:围绕
component-library的自包含组件与概念化图标命名,保持样式全局、静态、可预测。
若想继续深入,建议按以下顺序阅读源码:
- 渲染与数据预加载:
mathesar_ui/src/utils/preloadData.ts→mathesar/templates/mathesar/index.html; - Store 模式总览:
mathesar_ui/src/stores/table-data/tabularData.ts→mathesar_ui/src/stores/table-data/filtering.ts→mathesar_ui/src/stores/users.ts; - Store 工具函数:
mathesar_ui/src/component-library/common/utils/storeUtils.ts; - API 分离示例:
mathesar_ui/src/api/rpc/users.ts; - 组件与表单:
mathesar_ui/src/component-library/icon/Icon.svelte→mathesar_ui/src/components/form/form.ts。
- 数据库
- 后端
- 前端
- 协作
【免费下载链接】mathesar
An intuitive spreadsheet-like interface that lets users of all technical skill levels view, edit, query, and collaborate on Postgres data directly. 100% open source and self hosted, with native Postgres access control.
相关推荐
Graphite 前端源码架构解析:从 Svelte 组件到 Rust 后端的消息驱动设计
Graphite 前端源码架构解析:从 Svelte 组件到 Rust 后端的消息驱动设计 本文以 Graphite 编辑器前端源码目录 frontend/sr
图形学桌面应用前端图像处理Gridsome前端架构模式:组件设计与代码组织原则
Gridsome前端架构模式:组件设计与代码组织原则 组件设计理念 Gridsome采用组件化架构设计,核心组件位于 gridsome/app/componen
前端SSRGraphQLPWA从零开始掌握Python OPC UA:工业物联网数据通信的终极指南
从零开始掌握Python OPC UA:工业物联网数据通信的终极指南 在工业自动化和物联网快速发展的今天,如何高效、安全地实现设备间的数据通信成为开发者面临的核
通信工业制造物联网
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考