☰
Mathesar 组件库开发规范与架构解析:component-library 如何做到自包含、全局样式与 Storybook 退役
2026/10/13 1:52:58 网站建设 项目流程
  • 数据库
  • 后端
  • 前端
  • 协作

【免费下载链接】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 前端的全部可复用 UI 能力都沉淀在mathesar_ui/src/component-library/目录中。本文基于该目录的 README 开发指南,结合仓库源码逐项印证其三条核心约定(代码不越界导入、样式自包含、全局静态类名),并解析组件库的目录组织、公共 API 出口、设计令牌(CSS 变量)体系,以及 Storybook 被退役后遗留的组件 playbook 现状,帮助你在该组件库中新增组件时遵循既定规范、不破坏其"未来可拆分为独立包"的设计目标。

组件库定位:一个计划独立成包的设计系统

README 对目录的定义只有一句话,但信息量足够:

This directory contains the components and design system used by the Mathesar front end. This will be spun off into a package of it's own at a later point in time.

即该目录同时承担组件集合与设计系统两个角色,并且团队已明确规划将其拆分为独立发布的包(spun off into a package of it's own)。这一"未来自立门户"的目标,正是后文所有开发约定的出发点——只有做到目录自包含,拆分时才不会牵动应用主代码。

从源码结构看,组件库对外暴露三条清晰的路径:

  1. 值导出入口index.ts:按层次组织全部公共 API——工具函数(common/utils、createDebounce、getPaginationPageCount)、Svelte actions(clickOffBounds、popper、portal、slider、focusTrap)、Simple Components(Button、Badge、Checkbox、Spinner等约 40 个)、Compound Components(Select、ListBox、Pagination、TabContainer等)以及 Systems(confirmation、dropdown、modal、toast、menu、form-builder等组合式系统)。
  2. 类型出口types.ts:通过export *聚合common/utils、commonTypes及各个组件的*Types模块,构成完整的类型面,对应 tsconfig 中的@mathesar-component-library/types别名。
  3. 样式入口styles.scss:文件头注释明确写道"这是组件库样式系统的基础出口,待库移出主项目后可能自动化这一过程",目前它以@import逐一列出common/styles/main.scss及各组件的静态.scss文件。

对应用主代码而言,使用方只需记住一个别名:@mathesar-component-library。例如 App.svelte 中import { Spinner } from '@mathesar-component-library'。

约定一:目录内的文件不得导入目录外的任何文件

README 开发指南的第一条:

Ensure that files within this directory do not import any file outside the scope of the directory.

这条规则的落点在于:库内部引用一律走@mathesar-component-library-dir/*别名,且该别名只映射到目录自身。tsconfig.json 中的路径配置为:

"paths": { "@mathesar/*": ["src/*"], "@mathesar-component-library-dir/*": ["src/component-library/*"], "@mathesar-component-library/types": ["src/component-library/types.ts"], "@mathesar-component-library": ["src/component-library/index.ts"] }

其中@mathesar-component-library-dir/*前缀被刻意设计为仅指向src/component-library/——组件库内部文件互相引用时必须使用它(或相对路径),从机制上杜绝了写@mathesar/systems/...这类越界导入。而 vite.config.js 的getAlias()函数直接从 tsconfig 的paths逐条生成 Vite 的resolve.alias,保证开发服务器、构建与类型检查三者的别名完全一致:

function getAlias() { const alias = []; const { paths } = data.compilerOptions; Object.keys(paths).forEach((key) => { const find = (__dirname, key.replace('/*', '')); const replacement = path.resolve(paths[key][0].replace('/*', '')); alias.push({ find, replacement }); }); return alias; }

对目录实际做导入面统计,可以验证该约定的执行状况:库内部文件(.ts/.svelte)的 import 源只有三类——

  • npm 包:svelte、svelte/store、svelte/action、@popperjs/core、flatpickr、dayjs、@fortawesome/free-solid-svg-icons、iter-tools、fast-diff等(测试文件另有vitest、@testing-library/svelte);
  • 库内部别名:@mathesar-component-library-dir/common/utils、@mathesar-component-library-dir/button/Button.svelte、@mathesar-component-library-dir/icon/Icon.svelte等;
  • 相对路径:同目录或兄弟子目录间的引用。

没有出现任何指向src/component-library/之外的应用源码导入。这正是 README 要求的状态:一旦未来拆分发包,只需把package.json依赖从主工程同步过来,而不需要重构任何 import。

约定二:组件样式不得依赖目录外的样式

README 开发指南的第二条:

Ensure that the styles for the components do not depend on styles specified outside the scope of the directory.

样式自包含由"单一全局入口 + 全部样式文件位于目录内"两点保证:

  • 全部样式源文件都在component-library/内,公共部分集中在 common/styles/:variables.scss定义尺寸/字体等基础变量,themes/main.scss分发light/dark两套主题(颜色令牌如--color-bg-control、--color-fg-danger等),components/下还有input.scss、card.scss、canvas.scss等跨组件的基类。
  • 应用侧的引入点是唯一且固定的:App.svelte 中有一行注释"The component-library style import would only ever be from App.svelte",随后@import 'component-library/styles.scss';。也就是说组件库的样式不会分散注入,主应用代码也不会反向依赖库内某个具体 scss 文件。

约定三:全局样式 + 静态类名,而非自动生成的 scoped 类

README 开发指南的第三条:

All component styles should be global, the classes should be static and not auto-generated.

这意味着组件不使用 Svelte 的<style scoped>自动哈希类名,而是编写全局可寻址的静态类名,例如最简组件 Badge.svelte 全文仅两行——<span class="badge"><slot /></span>,其样式来自全局的badge/Badge.scss。

以 Button.scss 为典型,可以完整看到"全局静态类 + 设计令牌"的运作方式:

  • 静态类名:基类.btn,变体类.btn-{variant}(default、primary、secondary、outcome、plain、ghost、danger、action、tip、link),尺寸工具类.size-small/.size-medium/.size-large,布局工具类.full-width、.padding-compact等。变体名与 commonTypes.ts 中的 TypeScript 联合类型一一对应:

    export type Appearance = | 'default' | 'primary' | 'secondary' | 'plain' | 'ghost' | 'action' | 'outcome' | 'danger' | 'link' | 'custom' | 'tip'; export type Size = 'small' | 'medium' | 'large';
  • 状态矩阵:SCSS 用$btn-states: (hover, focus, active)与$btn-variants两张映射表,把"变体 × 状态"展开为 CSS 变量的默认值,同时兼容伪类选择器(&:hover)与静态类选择器(&.hover)两种触发方式。

  • CSS 变量分层:每个可配置属性生成分层变量,外部覆盖 > 内部默认:background: var(--button-background, var(--button-internal-background)),状态层再套一层var(--button-hover-background, var(--button-background, var(--button-internal-hover-background)))。这使得调用方只需在行内 style 上设置--button-*即可定制任意变体任意状态的单个属性,而组件类名保持静态不变。

  • 主题切换零成本:因为取值全部落在--color-*设计令牌上,common/styles/themes/下的 light/dark 主题只需换一组令牌即可整体换肤。

从源码结构看,"全局静态类名"这一约定还带来一个副作用:.stories/测试文件可以直接用字符串类名断言渲染结果(如 Button.test.ts 通过../Button.svelte相对导入被测组件),无需处理构建期生成的类名哈希。

目录组织:一个组件一个目录,元文件收敛在meta

README 没有细讲目录规范,但源码呈现出一致性很高的组织方式,以button/为例:

button/ ├── Button.svelte # 组件本体 ├── Button.scss # 该组件的全局静态样式(被 styles.scss 导入) └── __meta__/ ├── Button.stories.svelte # 遗留的组件 playbook 故事 ├── Button.test.ts # 组件测试 └── Button.mdx # 文档

各组件目录(badge/、checkbox/、dropdown/、toast/、form-builder/等 60 余个)均遵循"本体 + 同名样式 +__meta__"的模式;跨组件共享的东西则统一收敛到 common/:

  • common/actions/:clickOffBounds.ts、focusTrap.ts、popper.ts、portal.ts、slider.ts五个 Svelte action 的通用实现;
  • common/base-components/:BaseInput.svelte与BaseInputTypes.ts,供TextInput、TextArea、Select等输入类组件复用;
  • common/styles/:变量、主题与跨组件基类(上文已述);
  • common/utils/:约 30 个工具模块(CancellablePromise、ImmutableMap、storeUtils等),其中核心工具在__tests__/子目录配有 vitest 单测(如ImmutableMap.test.ts、filterUtils.test.ts)。

此外还有目录级的测试工具meta/utils/SlotTest.svelte,用于在测试中向任意 slot 注入带data-testid的标记内容,验证"插槽内容是否被正确渲染"这类与具体组件无关的通用断言。

Storybook 的退役:从 #3214 到组件 playbook 迁移

README 专设了 Storybook 小节,说明该库的组件展示方式正在切换:

  • As of #3214, we no longer use Storybook for developing our components.
  • Our Storybook stories will be migrated to a different component playbook, as tracked in #3215.

即自 PR #3214 起 Storybook 不再用于组件开发,原 stories 向另一套"component playbook"的迁移由 issue #3215 跟踪。仓库现状与这段描述吻合:

  • package.json中已无storybook相关脚本与依赖,工具链(vitest + vite.config.js 的test段)走的是 vitest/jest-dom 路线;
  • 但各组件__meta__/下仍保留着 31 个*.stories.svelte遗留文件,它们仍import { Meta, Story } from '@storybook/addon-svelte-csf'(如 Button.stories.svelte)。从源码结构看,这些文件属于"已弃用、待迁移"的存量资产,可作为组件各变体的参照样例,但不再是可运行的开发入口——新组件不应再按 Storybook CSF 格式编写展示文件。

实操清单:在 component-library 中新增一个组件

综合 README 三条约定与源码现状,新增组件(例如MyControl/MyControl.svelte)应满足:

  1. 导入边界:只 import npm 依赖、@mathesar-component-library-dir/*(或目录内相对路径);禁止出现@mathesar/...指向应用层代码的 import。
  2. 样式边界:新建MyControl.scss,只引用common/styles/内的变量与主题令牌;类名使用静态全局名(如.my-control、.my-control-danger),不使用 scoped 自动类名。
  3. 样式注册:在 styles.scss 中追加@import 'my-control/MyControl.scss';(当前该文件是手工维护的单一入口)。
  4. API 出口:在 index.ts 对应分区(Simple Components / Compound Components / Systems)追加导出;类型放入MyControlTypes.ts并在 types.ts 中export type。
  5. 元文件:测试与展示文件放入__meta__/,遵循库内现有__tests__的 vitest +@testing-library/svelte用法;不要再写.stories.svelte。
  6. 复用优先:输入类组件先评估能否基于common/base-components/BaseInput.svelte扩展;图标用common/icons.ts聚合的 FontAwesome 图标与 Icon.svelte。

小结

Mathesar 的component-library用 README 中短短三条开发约定(不越界导入、样式自包含、全局静态类名)支撑起整个设计系统:@mathesar-component-library与@mathesar-component-library-dir双别名把导入边界固化在 tsconfig/Vite 层,styles.scss单一入口把样式边界收敛到文件清单,CSS 变量分层(外部覆盖 > 内部默认)则让"静态类名"依然保有完整的定制与换肤能力。配合按组件分目录、common/共享层与__meta__元文件的组织方式,以及 Storybook 退役后向新 playbook 迁移的过渡状态,这套约定使该目录在满足当前应用内使用之余,具备了低成本拆分为独立 npm 包的工程条件。

  • 数据库
  • 后端
  • 前端
  • 协作

【免费下载链接】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
点击查看免费下载

相关推荐

上一篇:RAGs系统资源监控:使用Prometheus+Grafana实现
下一篇:终极指南:ts-morph 内存文件系统与真实文件系统对比分析

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

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

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

立即咨询