☰
Mathesar 前端代码架构解析:从 Svelte Store 模式到组件库设计
2026/10/13 2:45:02 网站建设 项目流程
  • 数据库
  • 后端
  • 前端
  • 协作

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/ma/mathesar
点击查看免费下载

导读:本文系统梳理 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(中间还可以夹杂其他数据)。这是最复杂、最"定制化"的模式,因而难以统一归类,架构文档给出了两个典型实例:

  1. 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;
  1. 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 importsstore 实例是全局单例,任何导入它的组件共享同一实例全应用只有唯一实例的场景(如 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目录剥离,形成新的组织约定:

  1. 存在mathesar_ui/src/api/目录,内含src/api/types与src/api/utils;新文件都将放在src/api/下。
  2. 每个工具文件同时导出其相关类型。最佳示例是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表达"取消"语义而非"向左箭头")。图标集中存放在两个位置:

  1. 组件库图标:mathesar_ui/src/component-library/common/icons.ts(如iconCancel、iconChooseItemManyAhead等);
  2. 应用代码图标: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 前端架构的三个演进方向,这也正是新贡献者应该遵循的准则:

  1. 状态管理:从"公开 writable store / ESM 全局单例"走向"不可变类 + 细粒度 store 封装 + 编译期只读约束(MakeWritablePropertiesReadable)",通过更严的封装减少跨组件耦合与意外修改;
  2. API 层:从"store 内联 API 调用"走向"src/api集中管理、类型随文件导出"的分离式设计;
  3. 组件体系:围绕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.

项目地址:https://gitcode.com/gh_mirrors/ma/mathesar
点击查看免费下载

相关推荐

上一篇:如何三步实现零代码H5页面创作:h5maker可视化编辑器深度解析
下一篇:如何用开源工具h5maker在10分钟内创建专业级移动端页面?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询