- 数据库
- 灾备
【免费下载链接】databasus
PostgreSQL backup tool with Point-In-Time-Recovery and restore verification
本文为 Databasus(一款支持 PostgreSQL 时间点恢复与恢复验证的备份工具)前端代码库的工程规范指南,基于 frontend/AGENTS.md 展开。读完本文,你将掌握该项目在 React 19 + TypeScript + Vite + Ant Design + TailwindCSS 技术栈下的组件编写结构、剪贴板安全访问、表单渐进披露、基于typeof en强类型约束的多语言字典体系,以及 Feature-Sliced Design (FSD) v2.1 的分层导入规则与代码归属决策方法,并了解每条规范在仓库源码与测试中的落点。
技术栈基线:AntD 5 是唯一组件库
Databasus 前端由 React 19、TypeScript、Vite 驱动,组件库限定为AntD 5 单一来源,配套 TailwindCSS 工具类负责布局与间距。规范明确要求:
- 组件只用 AntD 原语(
Button、Input、Modal、Form、Table、Menu、Tabs等),禁止引入 Mantine、MUI、Chakra、shadcn 或 Radix; - 图标只用
@ant-design/icons,禁止lucide-react、@heroicons/react、react-icons或 FontAwesome。
frontend/package.json 中的依赖清单印证了这一约束:antd ^5.29.3、@ant-design/icons ^5.6.1是仅有的 UI 依赖,其余如cron-parser(解析备份计划的 cron 表达式)、recharts(图表)、i18next/react-i18next(国际化)都属于功能库而非组件库。这种"单一组件库"策略保证了视觉一致性与打包体积的可控。
React 组件的标准内部结构
规范为每个组件文件规定了固定的声明顺序,核心代码骨架如下:
interface Props { someValue: SomeValue; } const someHelperFunction = () => { ... } export const ReactComponent = ({ someValue }: Props): JSX.Element => { // First put states const [someState, setSomeState] = useState<...>(...) // Then place functions const loadSomeData = async () => { ... } // Then hooks useEffect(() => { loadSomeData(); }); // Then calculated values const calculatedValue = someValue.calculate(); return <div> ... </div> }结构顺序(Structure order)分五层,从文件顶层到 JSX 返回依次为:
- Props 接口— 定义组件入参;
- 组件外的辅助函数— 纯工具函数;
- 组件主体内部,再细分四段:
- States— 所有
useState声明; - 普通函数— 事件处理器、异步操作、组件内格式化函数,凡是不调用 React Hook 的函数都归入此段;
- Hooks—
useRef+ ref 变更、useCallback、useMemo、useEffect。注意:被useCallback包裹的函数属于 Hook,必须放在 Hooks 段,而不是普通函数段; - 计算值— 内联派生数据,例如 AntD
Table的columns数组。
- States— 所有
其中一条硬性规则值得单独强调:所有 Hook(包括每个useEffect)必须位于所有普通函数定义之后。如果某个useEffect要读取某个 handler,该 handler 必须定义在它上方——不能为了"把 effect 集中在前面"而把 handler 挪到 effect 之后。这条规则同时规避了依赖声明与函数定义顺序带来的阅读歧义。
垂直间距(Vertical Spacing)
函数体内部要求用"垂直节奏"来组织:
- 在逻辑上不同的步骤之间(setup / 主工作 / return、独立分支、守卫语句前后)各留一行空行;
- 函数体开头和结尾不留空行;绝不允许连续两行空行;
- 不要在紧凑表达式、跨行的单条语句、或不超过 5 行的短函数内部插空行;
- 如果仅靠空行已经无法让人快速定位函数体结构,正确做法是抽取(提取函数/常量/文件),而不是加注释。
剪贴板操作:ClipboardHelper 统一收口
规范规定一切剪贴板操作必须走ClipboardHelper(frontend/src/shared/lib/ClipboardHelper.ts),禁止直接调用navigator.clipboard。查看源码可以看到两条 API 的完整实现:
export class ClipboardHelper { static isClipboardApiAvailable(): boolean { return !!(navigator.clipboard && window.isSecureContext); } static async copyToClipboard(text: string): Promise<void> { if (this.isClipboardApiAvailable()) { await navigator.clipboard.writeText(text); return; } // 非安全上下文(如 HTTP)下回退到隐藏 textarea + execCommand('copy') const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } // ... }按规范的使用模式:
- 复制:直接调
ClipboardHelper.copyToClipboard(text)。源码显示它先探测isClipboardApiAvailable(),在非安全上下文(HTTP 部署时isSecureContext为false)自动回退到execCommand('copy')方案——这对自托管在内网 HTTP 地址下的 Databasus 实例是必要的兼容路径。 - 粘贴:先检查
ClipboardHelper.isClipboardApiAvailable()。可用时调用ClipboardHelper.readFromClipboard();不可用时展示ClipboardPasteModalComponent(位于shared/ui),让用户通过文本输入框手动粘贴。navigator.clipboard.readText()同样依赖安全上下文与用户手势,因此"能读则读、不能读则兜底"是这套交互的必然形态。
表单的渐进披露(Progressive Disclosure)
表单交互遵循两条渐进披露规则:
- 提交按钮(
Save、Update password等)只在表单处于脏状态(dirty)时渲染——没有任何修改时页面上根本不存在提交入口; - 依赖字段(例如
Confirm password)只在它所依赖的字段已有值之后才渲染。
这两条规则把"未修改的表单"和"待提交的表单"在视觉上区分开,减少用户误触,也符合备份工具这类低频但高风险操作的表单语境(改密码、改存储凭证等)。
面向用户的文案排版
英文字典(frontend/src/shared/i18n/locales/en.ts)中,所有用户可见字符串(标签、描述、通知、模态框正文、错误信息)统一使用普通连字符-;em dash(—)与 en dash(–)只允许出现在 Markdown 文档和代码注释中。
其他语言则遵循各自语言的排版惯例,规范明确指向 website/AGENTS.md 的 Translation quality 章节:俄语保留其语法需要的«—»,法语在: ; ! ?和%前加空格,中文使用全角标点。该章节同时列出了每种语言的规范界面术语。
i18n 体系:以 en 为唯一键源的多语言字典
界面通过i18next+react-i18next提供6 种语言:英语、俄语、西班牙语(巴西葡语)、简体中文、法语,全部位于shared/i18n目录。frontend/src/shared/i18n/dictionaries.ts 是整个体系的中枢:
export const DICTIONARIES = { en, ru, es, pt, zh, fr }; export type Locale = keyof typeof DICTIONARIES; export const DEFAULT_LOCALE: Locale = 'en';字典的强完整性约束
- en 是键集合的唯一事实来源(source of truth)。其他每种字典在类型上都是
typeof en,因此缺失一个键或多留一个键都会导致pnpm build失败——任何语言都不可能处于"半翻译"状态发布出去; - 每个字典必须是单文件内的单个对象字面量。TypeScript 对"多余键"的检测只对原地书写的字面量生效;如果通过变量传入嵌套对象或使用展开运算符(spread),类型检查就会失效;
- 键按业务域分组(
backups、databases、storages、status、errors、common等),而不是按文件路径分组——组件移动位置不会牵连键名变更; - frontend/src/shared/i18n/dictionaries.test.ts 在测试层面再加一道闸:把每种字典拍平后与英文逐键比对,确保每条翻译的
{{placeholders}}、Trans标签集合与英文一致,<code>片段逐字节相同,且没有空消息。源码中的正则值得参考:
// {{name}} and {{name, format}} both count as the placeholder "name" const getPlaceholders = (text: string) => uniqueSorted([...text.matchAll(/\{\{\s*([^}\s,]+)[^}]*\}\}/g)].map((m) => m[1])); // <bold>, </bold> and <workspaceName/> all count as the Trans tag of that name const getTransTags = (text: string) => uniqueSorted([...text.matchAll(/<\/?([A-Za-z][\w-]*)\s*\/?>/g)].map((m) => m[1]));也就是说,"占位符/标签与英文一致"不仅靠 lint,而是有 vitest 测试(pnpm test)持续验证。
代码侧的强制规则
这是本文档中约束最密集的一节,逐条展开:
src中不允许任何用户可见字符串字面量。由i18next/no-literal-stringlint 规则强制,且覆盖 SCREAMING_SNAKE 常量和默认参数值(上游插件默认豁免这两类,项目专门改写了规则——见下文"eslint 配置"一节)。技术字符串保持字面量:命令、代码块、连接串、cron 表达式、引擎与工具名、文件路径、localStorage键。个别确实需要豁免的技术字符串,使用// eslint-disable-next-line i18next/no-literal-string -- <reason>逐条说明理由;在 JSX 中则把它写成独立一行的表达式({'pg_dump'})以便指令生效。整个文件只有技术字符串时(例如 shell 命令构造器),允许在文件顶部一次性禁用并写明理由。frontend/eslint.config.js 是这套规则的真正实现。可以看到项目把插件的
no-literal-string规则做了二次包装:const i18nextLiteralStringOptions = { mode: 'all', // 检查所有字符串字面量与模板,而不只是 JSX 文本 'should-validate-template': true, 'jsx-components': { exclude: ['Trans'] }, // 排除"永不承载文案"的属性:className、data-*、href、mode、placement…… 'jsx-attributes': { exclude: [ /* 正则与白名单 */ ] }, words: { exclude: [ '[\\s0-9!-/:-@[-`{-~]+', // "0 2 * * *"、"********" 等 '(https?|wss?|s?ftp|s3|mongodb(\\+srv)?|postgres(ql)?|mysql|mariadb|jdbc:\\w+)://\\S*', // 连接串 '\\.{0,2}/\\S*', // 绝对/相对路径 '#[0-9a-fA-F]{3,8}', // CSS 颜色值 '-?\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|ms|s|fr)', // CSS 尺寸 '[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+', // 示例邮箱 '[a-z]{2}-[A-Z]{2}', // BCP 47 语言标签 ], }, // ... };其中
words.exclude的正则白名单非常贴切备份工具的业务:postgres(ql)?、mysql、mariadb、mongodb(\\+srv)?、s3等连接串前缀直接豁免;cron 表达式(如0 2 * * *)由"纯数字/标点"正则豁免。这解释了为什么备份配置表单里可以直接写引擎名与调度表达式而不会被 lint 拦截。组件内取文案用
const { t } = useTranslation()(来自react-i18next)。组件之外的代码存键不存文本。模块级常量、标签表、解析器如果直接调
t(),会冻结在"执行时激活的语言"上。正确做法是存TranslationKey,带参数的消息存LocalizedText({ key, params }),由组件在渲染时翻译(t(key)、translateLocalizedText(text, t))。状态同理:state 里存错误或键,不存翻译后的字符串。每个面向用户的 enum 都有标签表。形式为
<TYPE>_LABEL_KEYS: Record<Enum, TranslationKey>,放在它标注的类型旁边,并通过该 slice 的index.ts导出。永远不要用值拼键(t(`status.${value}`))——这样新增一个枚举成员时构建会直接失败,而不是在运行时悄悄显示原始值。句子保持完整。带内联标记的句子是一个键,通过
<Trans>渲染:<Trans i18nKey="..." components={{ code: <InlineCodeComponent />, bold: <strong />, docsLink: <a href={...} /> }} />键在变量中时用
shared/i18n提供的<TransByKey>(因为Trans无法对整个键联合做类型检查)。字典里写命名标签,例如Run <code>pg_dump</code> first。值插值到完整句子中('Delete {{name}}?'),绝不把翻译碎片拼起来;当插入的词在其他语言中会屈折变化时,每个值各配一个整句键。用户在 Trans 句子中录入的值(工作区、数据库、用户名)是自闭合标签,元素由组件提供:字典写
Delete <workspaceName/>?,组件写components={{ workspaceName: <strong>{name}</strong> }}。绝不能把它塞进values——Trans会展开其中的占位符语法。这一安全细节在 frontend/src/shared/i18n/createI18n.ts 中另有呼应:react: { useSuspense: false, // 只有组件通过 components 提供的标签才变成元素 // 字典里孤立出现的 <strong> 之类会原样显示为文本 transKeepBasicHtmlNodesFor: [], // 先转义 values 再解析标签;否则 "<strong>x</strong>" 这样的值会变成元素 transDefaultProps: { shouldUnescape: true, tOptions: { interpolation: { escapeValue: true } }, }, },行内代码一律走
InlineCodeComponent(shared/ui),不用裸<code>——仅等宽字体不足以把代码从周围文字中区分出来。计数句式写成"一个字符串适配所有数量":
Members: {{count}},而不是{{count}} members。字典没有复数形式,因为俄语的_few变体在typeof en下会成为游离键。翻译文本绝不当作 HTML 渲染,禁用
dangerouslySetInnerHTML。依赖语言的输出从 Hook 获取,这样切换语言才能触发重渲染:文本用
useTranslation,数字格式化用useLocale()返回的formatNumber(替代toLocaleString()),相对时间用formatRelativeTime(替代 dayjs 的fromNow())。API 错误到达用户的唯一通道是
translateApiError(error, t)。frontend/src/shared/i18n/translateApiError.ts 的实现清晰呈现了这个三层兜底:export const translateApiError = (error: unknown, t: TFunction): string => { if (!(error instanceof ApiError)) { return t('errors.unknown'); } const codeKey = error.code ? API_ERROR_CODE_KEYS[error.code] : undefined; if (codeKey) { return t(codeKey, { status: error.status ?? '' }); // 已知错误码 -> 翻译 } // 后端消息以小写开头("user is already a member") return error.message ? StringUtils.capitalizeFirstLetter(error.message) : t('errors.unknown'); };已知错误码给翻译,否则用后端消息,最后兜底通用消息——绝不直接展示
error.message。指向 databasus.com 页面的链接统一走
getWebsitePageUrl(pageId, locale),配合WEBSITE_PAGES常量表,在官网发布了对应语言版本时自动以所选语言打开页面。
时钟制式与日期顺序:读navigator.language是有意的
shared/time/getUserTimeFormat.ts与shared/time/utils.ts刻意读取navigator.language:12/24 小时制和日/月先后是区域性约定,不是语言属性——英语世界两种时钟制式都有人用。浏览器报告的是用户所在区域,而界面语言选择只决定"用哪些词"。因此这两处读取被明确保留;但裸的toLocaleString()不被允许,因为数字千分位应跟随所选语言,由useLocale()提供的formatNumber处理。
FSD(Feature-Sliced Design)v2.1 分层架构
项目采用 FSD v2.1,启用的层级为app/、pages/、widgets/、features/、entity/、shared/(对照仓库可见 frontend/src 下正是entity/、features/、pages/、shared/、widgets/五个目录)。
导入方向
依赖只能向下:app → pages → widgets → features → entity → shared。
- 模块只能从严格位于其下方的层级导入;
- 同层之间不同 slice 的横向导入被禁止。
✅ 允许 某个 widget 导入 features/backups, entity/backups, shared/ui 某个 feature 导入 entity/databases, shared/api 某个 entity 导入 shared/lib ❌ 违规 某个 entity 导入 features/databases (向上:entity 低于 features) 某个 feature 导入 pages/AuthPageComponent (向上:pages 高于 features) features/users 导入 features/backups (同层:跨 slice 导入) shared/lib 导入 entity/users (向上:shared 是最底层)一个实现细节值得注意:项目没有配置路径别名,上述路径为便于阅读而以src/为基准书写,真实 import 全部是相对路径。
新代码放哪里
- 只在一个页面使用 → 留在该页面的
pages/slice 内; - 可复用基础设施、不含业务逻辑→
shared/(UI kit、utils、API client、路由常量、认证令牌、CRUD 辅助); - 在 2 个以上页面复用的用户交互 →
features/; - 在 2 个以上页面/feature 复用的领域模型 →
entity/; - 应用级 Provider、路由、主题 →
app/。
拿不准时就留在pages/,直到出现第二个真实消费方再抽取。
快速归属表
| 场景 | 单一使用 | 两处及以上复用 |
|---|---|---|
| 个人资料表单 | pages/profile/ui/ProfileForm.tsx | features/profile-form/ |
| 数据库卡片 | pages/databases/ui/DatabaseCard.tsx | entity/database/ui/DatabaseCard.tsx |
| 备份数据抓取 | pages/backup/api/fetch-backup.ts | entity/backup/api/ |
| 认证令牌 / 会话 | shared/auth/(永远) | shared/auth/(永远) |
| 登录表单 | pages/login/ui/LoginForm.tsx | features/auth/ |
| CRUD 辅助 | shared/api/(永远) | shared/api/(永远) |
| 日期格式化工具 | — | shared/lib/format-date.ts |
| 模态框内容 | pages/[page]/ui/SomeModal.tsx | — |
五条 MUST 规则
- 只向下导入。禁止向上导入,禁止同层跨 slice 导入;
- 对外 API 只通过
index.ts暴露。外部消费方只能从 slice 的index.ts导入,不能碰内部文件:✅ features/users ❌ features/users/ui/AuthNavbarComponent - 按业务域命名文件,而不是按技术角色:
// ❌ model/types.ts, model/utils.ts, lib/helpers.ts // ✅ model/user.ts, model/backup.ts, api/fetch-database.ts shared/中禁止业务逻辑。共享层只放基础设施;领域计算放在entity/或更高层。model/中一个类型一个文件。每个代表领域实体、DTO、请求体或响应形状的interface/class/enum单独一个文件,以类型命名。不要把Foo+FooStatus+FooResponse这样的兄弟类型塞进同一个Foo.ts——拆开后各自仍从 slice 的index.ts再导出,消费方依旧只依赖 slice 的公共 API:// ❌ model/RestoreVerification.ts — 枚举 + 表统计 + 主接口挤在一个文件 // ✅ model/RestoreVerification.ts — interface RestoreVerification // ✅ model/RestoreVerificationTableStat.ts — interface RestoreVerificationTableStat // ✅ model/VerificationStatus.ts — enum VerificationStatus // ✅ model/VerificationTrigger.ts — enum VerificationTrigger例外:与父类型紧耦合的小型纯形状辅助类型(字面量联合别名、
Pick<>)可以留在父类型文件里。拆分适用于"有自己身份"的任何类型——即任何你合理地会单独去别处 import 的类型。
Slice 内部的段(Segments)
ui/— 组件与样式model/— 状态、类型、领域逻辑、校验api/— 后端调用、请求函数、API 专属类型lib/— 仅供本 slice 使用的内部辅助config/— slice 级配置 / 特性开关
app/与shared/只有段、没有 slice,其内部各段之间允许互相导入。
AVOID 清单
- 过早创建 entity(单一消费方 → 留在页面里);
- 把 CRUD 放进
entity/——CRUD 是基础设施,归shared/api/; - 仅仅为了认证令牌 / DTO 创建
userentity——那些属于shared/auth/或shared/api/; - 为了"将来复用"而抽取单一使用的代码;
- 上帝 slice(如
user-management/同时覆盖认证 + 个人资料 + 密码)——按聚焦职责拆分; - 从一个 entity 导入另一个 entity 的
ui/段——entity 的 UI 只允许被 features/widgets/pages 导入; - 滥用
@x跨导入写法——那是最后手段,不是常规工具。
同层 slice 之间的共享代码:四步降级策略
当同层两个 slice 需要共享代码时,按顺序尝试:
- 合并——如果两者总是同步变更,它们本就是一个 slice;
- 把共享逻辑下沉到
entity/——UI 留在 features/widgets; - 在更高层组合(IoC)——父级 page/widget 导入两者,通过 props/slots 接线;
@x记法——仅限 entity 之间、显式且有文档说明的跨导入。最后手段。
重构纪律:格式化与 Lint 是收尾动作
规范最后一条:应用变更时不要忘记重构旧代码——可以缩短、提升可读性、改善质量,公共逻辑可抽到函数、常量或文件。
在frontend/根目录下,每次50~100 行以上的较大改动之后必须执行:
pnpm format # Prettier 全量格式化(含 import 排序插件) pnpm lint # ESLint 校验改动(含 i18next/no-literal-string)这两个脚本定义在 frontend/package.json:format实际是prettier --write "**/*.{ts,tsx,js,jsx,json,css,md}",配合@trivago/prettier-plugin-sort-imports与prettier-plugin-tailwindcss;lint是eslint .,即上文分析的 frontend/eslint.config.js 规则集。此外还有pnpm test(vitest 全量运行,包含 i18n 字典一致性测试)与pnpm build(tsc -b && vite build,由typeof en键完整性检查把关)。
小结
frontend/AGENTS.md 所定义的这套规范,把 Databasus 前端的质量约束拆成了四个可机器执行的层面:tsc -b的类型系统(字典键完整性、enum 标签表)、ESLint(no-literal-string全模式 + 连接串/cron 白名单)、Vitest(占位符与 Trans 标签跨语言一致性)、Prettier(格式与 import 排序)。再加上 FSD 的分层导入纪律与"拿不准就留在 pages/"的克制原则,共同支撑一个 6 语言、多备份引擎管理界面在长期演进中保持结构清晰与文案零遗漏。对于读者,这套实践中最可迁移的三点是:以源语言字典为typeof锚点强制全量翻译、用 Record 标签表代替运行时拼键、以及把 lint 规则的豁免做成显式正则白名单而非随手eslint-disable。
- 数据库
- 灾备
【免费下载链接】databasus
PostgreSQL backup tool with Point-In-Time-Recovery and restore verification
相关推荐
sourcekit-lsp代码补全深度解析:提升开发效率的5个技巧
sourcekit lsp代码补全深度解析:提升开发效率的5个技巧 sourcekit lsp是Swift和C系列语言的Language Server Prot
开发工具终极科普:sptransformer-npu 是什么?RNA剪接位点预测模型与昇腾NPU适配全解析
终极科普:sptransformer npu 是什么?RNA剪接位点预测模型与昇腾NPU适配全解析 sptransformer npu 是一个面向 RNA 剪接
人工智能生物信息学本地部署AscendRedisInsight 前端国际化(i18n)体系解析:i18next + react-i18next 的工程化实践
RedisInsight 前端国际化(i18n)体系解析:i18next + react i18next 的工程化实践 RedisInsight 是 Redis
数据库客户端桌面应用后端前端数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考