Repository Wiki
LyraVoid/Mizuki

组件开发规范(docs/rule)

docs/rule/ 目录是 Mizuki 项目的开发规范中心,由一份索引文档(README.md)与七份编号规范(01–07)组成,定义了组件分层架构、组件拆分方法、文件组织结构、CSS 样式约束、原子组件优先原则、侧栏组件接入流程与图标使用方式。它是提交代码前代码审查(Code Review)的判定依据。

Purpose and Scope

本页面覆盖 docs/rule/ 规范体系作为整体的结构、职责与核心规则,包括:

  • 规范目录的组成(README 索引 + 7 份编号规范)及各规范之间的分工
  • 组件分层架构(atoms / molecules / organisms / pages + widgets)的设计意图
  • 侧栏组件接入的三步强制流程(类型声明 → 布局配置 → componentMap 注册),这是规范中最容易出错的环节
  • 代码审查检查清单(Code Review Checklist)的 18 项条款
  • CSS 约束(禁用 !important,Twikoo 例外)与原子组件优先原则

以下相关主题属于兄弟页面,不在本页展开:

  • 部署流程与构建触发:见 docs/DEPLOYMENT.md、docs/AUTO_BUILD_TRIGGER.md
  • 内容创作与渲染管线:见 docs/CONTENT_AUTHORING.md、docs/CONTENT_RENDERING.md
  • 项目整体文档结构:见 docs/README.md

Overview

Mizuki 是一个 Astro 驱动的组件化博客主题项目。当项目规模增长(出现"超大型组件"、重复 UI 代码、侧栏组件配置后不显示等问题)时,团队将开发约束沉淀为 docs/rule/ 下的成文规范,用于:

使用场景解决的问题
新增 UI 功能决定是复用现有原子组件,还是创建新的 atoms / molecules
重构超大型组件提供拆分判断标准、拆分步骤与验证方法
新增侧栏小部件避免"配置了组件但页面不显示"的遗漏
编写样式禁用 !important,统一使用 CSS 变量与 Tailwind 工具类
提交前自查18 项代码审查检查清单

规范目录的文件构成:

文件主题核心内容
README.md索引规范列表总览 + 代码审查检查清单 + 参考资源
01-component-architecture.md组件架构设计原子设计分层、命名规范、职责分离、复用模式
02-component-split-guide.md组件拆分指南拆分判断标准、超大型组件拆分实例、验证方法
03-file-organization-architecture.md文件组织架构完整目录树、目录职责、命名规范、依赖管理
04-css-style-guide.mdCSS 样式指南禁用 !important(Twikoo 除外)、CSS 变量、暗色主题
05-atom-component-usage.md原子化组件使用优先复用 atoms/ 与 misc/,重复超过 2 次即抽取组件
06-sidebar-widget-dev.md侧栏组件开发3 步接入流程,重点是所有渲染器的 componentMap 注册
07-icon-usage-specification.md图标使用规范图标的引用与使用方式

Architecture

规范体系并非孤立的文档集合,而是一套"规范 → 审查 → 代码"的闭环:README.md 作为索引入口汇总各规范要点并沉淀为检查清单,各编号规范分别约束 src/ 下的具体实现层(组件目录结构、类型系统 src/types/config.ts、配置 src/config.ts、侧栏渲染器)。

Loading diagram...

图中的关键关系说明:

  • README.md 是唯一的规范入口,它将六份编号规范(01–06)的关键点压缩成检查清单,开发者日常只看清单,深入细节时再跳转对应编号文档。
  • 组件分层由 01 定义并在 src/components/ 的目录结构(atoms / molecules / organisms / widgets)中落地;02 针对 organisms 层的超大型组件提供拆分路径。
  • 侧栏组件开发(06)是唯一一份跨三个源码文件(类型、配置、渲染器)的"流程型"规范,因此其依赖关系最复杂,也最易遗漏,见下文 侧栏组件接入:三步流程。

组件分层架构(Atomic Design)

规范采用**原子设计(Atomic Design)**理念,将组件分为四层,另设一个"小部件"类别:

Loading diagram...

各层的职责与代表组件(摘自规范文档的组件清单):

层定义特点代表组件
atomsUI 最基础、不可再分的元素职责单一、无业务逻辑、高度可复用、不依赖其他组件Button.astro、Card.astro、Input.astro、Badge.astro、Chip.astro、Icon.astro、Avatar.astro
molecules由多个原子组合的单一职责小组件2-5 个原子组合、简单交互、仍高度可复用SearchBar.astro、Pagination.astro、DropdownMenu.astro、FormItem.astro、ChipCloud.astro
organisms复杂业务组件复杂业务逻辑、多个子组件、专用于特定页面、可能需拆分子目录Navbar.astro、Sidebar.astro、MusicPlayer.svelte、Footer.astro、TOC.astro
widgets侧边栏小功能模块相对独立、可配置显示位置、统一 UI 风格、使用通用容器 WidgetLayout.astroProfile.astro、Calendar.astro、Categories.astro

设计意图:让"不可复用的复杂性"只存在于 organisms 层。atoms 与 molecules 不含业务逻辑,因此可以在任意页面安全复用;widgets 通过统一的 WidgetLayout.astro 容器获得一致的侧栏视觉风格,同时其挂载位置完全由配置驱动(见下文核心流程)。规范中的组件拆分指南(02-component-split-guide.md)则规定了"组件行数控制在合理范围内(< 500 行)、复杂组件已按功能拆分为子组件"等量化红线(见 README 检查清单)。

核心流程:侧栏组件接入(三步缺一不可)

06-sidebar-widget-dev.md 是整个规范体系中唯一一份跨类型系统、配置与渲染器的流程型规范。它针对的实际痛点在文档开头即点明:避免"配置了组件但页面不显示"的遗漏——因为侧栏渲染依赖手动的 componentMap,未注册的组件类型会被静默忽略。

Loading diagram...

步骤 1:在类型系统中声明组件类型

文件:src/types/config.ts。所有侧栏组件必须先在 WidgetComponentType 联合类型中声明,缺少此步 TypeScript 编译不通过,后续配置也无意义:

ts
1export type WidgetComponentType = 2 | "profile" 3 | "announcement" 4 | "categories" 5 | "tags" 6 | "toc" 7 | "music-player" 8 | "music-sidebar" // ✅ 新增类型 9 | "pio" 10 | "site-stats" 11 | "calendar" 12 | "custom";

Source: 06-sidebar-widget-dev.md

步骤 2:在 sidebarLayoutConfig 中配置布局

文件:src/config.ts。职责划分为:properties 定义组件的存在性(position: top / sticky)、动画与延迟;components.left / right / drawer 定义组件在哪个侧栏显示、按什么顺序:

ts
1export const sidebarLayoutConfig: SidebarLayoutConfig = { 2 properties: [ 3 { 4 type: "music-sidebar", 5 position: "sticky", 6 class: "onload-animation", 7 animationDelay: 100, 8 }, 9 // ... 其他组件 10 ], 11 components: { 12 left: ["profile", "announcement", "categories", "tags"], 13 right: ["site-stats", "calendar", "music-sidebar"], // 在右侧栏显示 14 drawer: [ 15 "profile", 16 "announcement", 17 "music-sidebar", 18 "categories", 19 "tags", 20 ], 21 }, 22 // ... 23};

Source: 06-sidebar-widget-dev.md

步骤 3:在所有侧栏渲染器的 componentMap 中注册(最易遗漏)

规范强调"这是最常出错的步骤",且左侧栏与右侧栏的 componentMap 相互独立,必须在每个渲染器中分别注册。以左侧栏 src/components/widgets/sidebar/SideBar.astro 为例:

ts
1import { MusicSidebarWidget } from "../music-sidebar"; 2 3const componentMap: Record<string, unknown> = { 4 profile: Profile, 5 announcement: Announcement, 6 categories: Categories, 7 tags: Tags, 8 toc: SidebarTOC, 9 "music-player": MusicPlayer, 10 "music-sidebar": MusicSidebarWidget, // ✅ 必须注册 11 "site-stats": SiteStats, 12 calendar: Calendar, 13};

Source: 06-sidebar-widget-dev.md

右侧栏 src/components/layout/RightSideBar.astro 使用同一套键名,但导入路径改为别名 @/components/widgets/music-sidebar,并需同样注册 "music-sidebar": MusicSidebarWidget——规范用注释 (与左侧栏独立) 提醒这不是同一份 map:

ts
1import { MusicSidebarWidget } from "@/components/widgets/music-sidebar"; 2 3const componentMap: Record<string, unknown> = { 4 profile: Profile, 5 announcement: Announcement, 6 categories: Categories, 7 tags: Tags, 8 toc: SidebarTOC, 9 "music-player": MusicPlayer, 10 "music-sidebar": MusicSidebarWidget, // ✅ 必须注册(与左侧栏独立) 11 "site-stats": SiteStats, 12 calendar: Calendar, 13};

Source: 06-sidebar-widget-dev.md

分层与容器的落地示例

原子层组件(如 Button.astro)通过 TypeScript Props 接口声明可选变体与默认值,保证无业务逻辑且高度可配置:

typescript
1// Button.astro 2interface Props { 3 variant?: 'primary' | 'secondary' | 'ghost' 4 size?: 'sm' | 'md' | 'lg' 5 disabled?: boolean 6 icon?: string 7} 8 9const { variant = 'primary', size = 'md', disabled = false, icon } = Astro.props 10--- 11 12<button class={`btn btn-${variant} btn-${size} ${disabled ? 'disabled' : ''}`}> 13 {icon && <Icon name={icon} />} 14 <slot /> 15</button>

Source: 01-component-architecture.md

分子组件(如 SearchBar.astro)组合原子并封装简单交互,体现"2-5 个原子 + 简单逻辑"的分层约束:

astro
1--- 2// SearchBar.astro 3import Button from '../atoms/Button.astro' 4import Input from '../atoms/Input.astro' 5 6interface Props { 7 placeholder?: string 8 onSearch?: (query: string) => void 9} 10 11const { placeholder = '搜索...', onSearch } = Astro.props 12--- 13 14<div class="search-bar"> 15 <Input {placeholder} id="search-input" /> 16 <Button variant="primary" size="md" icon="material-symbols:search"> 17 搜索 18 </Button> 19</div> 20 21<script> 22 const input = document.getElementById('search-input') 23 const button = document.querySelector('.search-bar button') 24 25 const handleSearch = () => { 26 const query = input?.value || '' 27 if (onSearch) { 28 onSearch(query) 29 } 30 } 31 32 button?.addEventListener('click', handleSearch) 33 input?.addEventListener('keypress', (e) => { 34 if (e.key === 'Enter') handleSearch() 35 }) 36</script>

Source: 01-component-architecture.md

widget 层组件则必须通过通用容器 WidgetLayout.astro 获得统一的侧栏视觉风格,实现"可配置显示位置 + 统一 UI"的目标:

astro
1--- 2// widget/Profile.astro 3import WidgetLayout from './common/WidgetLayout.astro' 4import Avatar from '../atoms/Avatar.astro' 5--- 6 7<WidgetLayout name="个人资料"> 8 <Avatar src="/avatar.png" /> 9 <div class="profile-info"> 10 <h3>Mizuki</h3> 11 <p>前端开发者</p> 12 </div> 13</WidgetLayout>

Source: 01-component-architecture.md

配置选项

WidgetComponentType(类型声明)

src/types/config.ts 中的联合类型,规范要求所有侧栏组件先在此声明。当前合法取值(以新增 music-sidebar 为例):

取值说明
profile个人资料
announcement公告
categories分类
tags标签
toc目录
music-player音乐播放器
music-sidebar侧栏音乐(示例新增类型)
pio看板娘
site-stats站点统计
calendar日历
custom自定义

sidebarLayoutConfig(布局配置)

src/config.ts 中的布局配置对象,两块职责互相独立:

字段类型作用
properties[].typeWidgetComponentType组件存在性声明
properties[].position"top" | "sticky"组件在侧栏内的定位方式
properties[].classstring附加样式类,如 onload-animation
properties[].animationDelaynumber入场动画延迟(毫秒)
components.leftWidgetComponentType[]左侧栏显示的组件及顺序
components.rightWidgetComponentType[]右侧栏显示的组件及顺序
components.drawerWidgetComponentType[]抽屉侧栏显示的组件及顺序

componentMap(渲染注册表)

每个侧栏渲染器内手写的 Record<string, unknown>,键为 WidgetComponentType 字符串,值为对应的 Astro/Svelte 组件。键名必须与 WidgetComponentType 完全一致,且左侧栏与右侧栏的 map 相互独立,需分别注册。

代码审查检查清单

README.md 将六份规范压缩为提交前自查的 18 项条款(规范落地的最终形态):

  • 组件遵循分层架构规范(atoms/molecules/organisms)
  • 组件文件名符合命名规范(PascalCase)
  • 组件行数控制在合理范围内(< 500行)
  • 复杂组件已按功能拆分为子组件
  • 优先使用现有原子化组件(atoms/、misc/)
  • 重复 UI 代码超过 2 次应抽取为新组件
  • 使用现有通用组件和 Hooks,避免重复代码
  • 组件职责单一明确
  • 样式使用原子组件或统一样式系统
  • 组件使用 TypeScript 定义 Props 接口
  • 代码格式化通过(pnpm run format)
  • Lint 检查通过(pnpm run lint)
  • 没有使用 !important(Twikoo 组件除外)
  • 使用 Tailwind 工具类或 CSS 变量
  • 暗色主题使用 CSS 变量实现
  • 侧栏组件已在所有相关 componentMap 中注册
  • 侧栏组件的类型已在 WidgetComponentType 中声明
  • 侧栏组件已在 sidebarLayoutConfig.components 中配置

Source: README.md

清单中加粗的三条侧栏条款与三步接入流程一一对应,其余条款分别溯源到 01(分层/PascalCase/行数)、02(拆分)、03(文件组织)、04(CSS)、05(原子组件优先)五份规范——这体现了 README.md 作为"索引 + 审查出口"的双重定位。

Failure Modes, Edge Cases & Concurrency

失败模式根因规范规定的规避方式
配置了组件但页面不显示渲染器 componentMap 未注册该类型,查找失败后静默忽略(无报错)三步流程中的步骤 3,在全部 3 个渲染器中逐一注册
组件仅在左侧栏显示、右侧栏缺失左右两侧栏 componentMap 相互独立,只注册了一处检查 SideBar.astro 与 RightSideBar.astro 两个文件(含抽屉侧栏)
配置键无效 / TS 编译失败未先在 WidgetComponentType 中声明类型步骤 1 前置类型声明,让编译器成为第一道防线
超大型组件难以维护职责堆积、未按功能拆分子组件02 拆分指南 + 清单"< 500 行"量化红线
重复 UI 代码扩散绕过现有原子组件重复实现05:重复超过 2 次必须抽取为新组件
样式互相覆盖、暗色主题失效使用 !important 强行覆盖04:禁用 !important(Twikoo 除外),改用提高选择器优先级 / CSS 变量 / Tailwind 工具类

值得注意的设计取舍:规范故意没有把 componentMap 做成自动化注册(如自动扫描目录生成映射),而是保留手动注册并配以三步清单。代价是容易遗漏,收益是每个渲染器对可用组件集合有完全显式的控制,避免侧栏之间的隐式耦合。

Performance / Operational Notes 与 Extension Points

  • 性能导向的分层:atoms / molecules 无业务逻辑且不依赖其他组件,可被任意页面安全复用而不引入副作用;onload-animation + animationDelay 的入场动画配置在 sidebarLayoutConfig.properties 中按组件粒度控制,避免全局动画阻塞首屏。
  • 扩展点 1 —— 新增侧栏小部件:完整路径为 WidgetComponentType 新增枚举 → sidebarLayoutConfig.properties + components.* 配置 → 所有渲染器 componentMap 注册 → 组件内部使用 WidgetLayout.astro 容器。
  • 扩展点 2 —— 新增原子/分子组件:先检索 atoms/ 与 misc/ 是否已有合适组件;只有当"重复 UI 代码超过 2 次"且确无现成组件时才创建新组件,并保持无业务逻辑、PascalCase 命名、TS Props 接口。
  • 扩展点 3 —— 超大型组件拆分:遵循 02 的拆分标准与验证方法,organisms 可拆分为子目录(如 Navbar.astro + 若干 .svelte 分子/原子子组件)。
  • 运维入口:提交前执行 pnpm run format 与 pnpm run lint,两项均为清单强制项。

说明:07-icon-usage-specification.md 与 02/03/04/05 的逐条细则未在本次源工具预算内全文读取,本文对其内容的概括均来自 docs/rule/README.md 中的关键点描述;深入细节请直接查阅对应编号文档。