项目:react-ui | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇是「操作手册」:把六类最常见的扩展动作各写成一份可照做的 SOP(标准作业程序)。每份 SOP 都以仓库现状为准——涉及的工具链事实(门禁命令、脚本、目录约定)全部有源码或配置文件背书,机制层面的「为什么」引用已有篇目(《02-组件设计》《03-接口设计》《09-测试与质量保障》),不重复展开。通用前置:Node 22 + pnpm 9.12.3(packageManager 锁定),pnpm install 后即可开工;提交一律 pnpm commit(cz-git 交互生成 conventional 提交,subject 中文讲清为什么)。
以新增组件 Foo 为例,从建目录到发布文档共八步。
第 1 步:建目录。 在 packages/ui/src/components/Foo/ 下按五件套标准形状落文件:Foo.tsx(组件本体,用 polymorphicFactory<FooFactory>(...) 创建;非多态组件可用 factory,pro 包的 PageContainer 即此形态的官方参考)、Foo.module.css(CSS Modules,只消费 --foo-* 局部变量与 data 属性选择器,铁律是不引入 CSS-in-JS——CONTRIBUTING 明文)、Foo.test.tsx(同目录单测)、index.tsx(re-export 组件、类型与 namespace)、可选的子件文件(FooGroup.tsx 等,经静态属性挂载)。形状对齐范本 packages/ui/src/components/Button/Button.tsx。
第 2 步:写 Factory 载荷与 varsResolver。 定义 FooFactory = Factory<{ props, ref, stylesNames, vars, staticComponents? }>;createVarsResolver 读 theme + props 产出 --foo-* 变量(键受 FooCssVariables 白名单类型约束);组件体内按标准链路装配:useProps('Foo', defaultProps, props) 合并默认值 → useStyles<FooFactory>({ name, classes, props, ... }) 拿 getStyles → Box/目标元素渲染。尾部挂全库约定的静态成员:Foo.classes = classes、Foo.varsResolver = varsResolver、静态子组件、Foo.displayName = '@xiaoye-react/ui/Foo',并用 export namespace Foo 导出 Props/StylesNames/CssVariables/Factory 四个类型出口(这是 docs:lint 与二次封装方依赖的镜像面,见《03-接口设计》第 2.2 节)。
第 3 步:接状态。 交互态一律 useUncontrolled(packages/hooks/src/use-uncontrolled/),声明 value/defaultValue/finalValue/onChange 四件套;只能调 setter、不得再直调 onChange(双 onChange 陷阱,见《04-数据模型》第 2 节)。
第 4 步:导出。 在 packages/ui/src/components/index.ts 追加 export * from './Foo'(当前该文件共 129 条星导出,保持字母序)。跑 pnpm check:exports 前需先 pnpm build:packages(该脚本校验 exports 指向的磁盘文件,构建后才有效),确认公开面没有「只有类型没有运行时」的悬空导出。
第 5 步:单测。 Foo.test.tsx 覆盖状态双模(受控不排数据类语义)、StylesApi 注入(classNames/styles/vars 是否落到对应选择器)、a11y(role/aria/键盘)。需要共享断言时用内部包 @xiaoye-react/tests(dates 域的 it-supports-* 系列是复用样板);触及 jsdom 盲区(pointer 命中、合成事件顺序、transitionend)的行为改写 e2e/interactions/ 用例(新建 spec 或挂入相近文件),harness 挂载点加在 e2e/harness/interactions.tsx。
第 6 步:文档页。 建 apps/docs/components/foo/index.zh-CN.md(kebab-case 目录名,如 button → action-icon 的先例)与 demo/ 目录。API 表每一行的属性名必须能在 FooProps 类型里找到——apps/docs/scripts/lint-docs-api-tables.mjs 会用类型检查器逐行核对(跨包继承成员依赖 hooks 构建产物,所以先 build);demo 的 configurator 控制项引用的 prop 名同样被 lint-demo-controls.mjs 核对。跑 pnpm docs:lint 直到全绿。
第 7 步:全量门禁。 顺序照 CI:pnpm lint(棘轮 --max-warnings=57,若新增警告需就地修复或按债务政策留注释豁免)→ pnpm typecheck → pnpm build:packages → pnpm docs:lint → pnpm check:exports → pnpm test:packages →(涉及浏览器行为时)pnpm e2e:interactions。
第 8 步:提交与版本。 pnpm commit;若本组件进入下个发布,按 SOP 四升版本。若组件属于管理端模式(而非通用组件),考虑放 pro 包——判据是「是否只是现有组件的编排」:只编排不造件的去 packages/@xiaoye-react/pro/src/(见 SOP 五)。
第 1 步:建 packages/hooks/src/use-xxx/use-xxx.ts,实现保持零 UI 依赖(只允许 import react 与包内 utils/);导出 hook 函数与 UseXxxOptions/UseXxxReturnValue 类型对。第 2 步:在 packages/hooks/src/index.ts 按既有风格追加导出(实现 + 类型;纯函数副产物如解析器一并导出,参考 use-hotkeys 的 getHotkeyHandler)。第 3 步:同目录 use-xxx.test.ts(jsdom 环境,packages/hooks/vitest.config.ts 已配置 globals 与 setupFiles)。第 4 步:如果 ui 包要消费它,检查 peer 下限——新导出意味着 @xiaoye-react/ui 的 peer 可能需要抬版本(attachMediaListener 与 ^1.4.0 下限的先例,见 README「发布相关的坑」),并同步核对 scripts/release.mjs 的 satisfiesCaret 预检能否通过。第 5 步:更新 packages/hooks/README.md 与 packages/hooks/package.json 的 description——注意两者目前都写「60+」,而实际 hook 目录已有 83 个,新 hook 入库时顺手把口径改准(第 13 篇台账项)。第 6 步:门禁同 SOP 一第 7 步(hooks 无 CSS,docs:lint 仅在文档引用了新导出时相关)。
分「视觉变体」与「尺寸/形态变体」两种情形。视觉变体(如新增 elevated):1) 在 Button.tsx 的 ButtonVariant 联合类型加字面量;2) 决定颜色来源——若走主题解析链,扩展 core/UIProvider/color-functions/ 的 variantColorResolver 或在组件 varsResolver 里为该变体产出额外 --button-* 变量(变量名先加进 ButtonCssVariables 白名单);3) Button.module.css 用 &[data-variant='elevated'] 属性选择器消费变量(Box 会把 variant 写成 data 属性);4) 参照 Tabs 的 pills 变体复用既有 resolver 结果的先例(resolverVariant 映射),能复用就不新造。形态变体(如新尺寸档):优先动主题刻度(theme.spacing/radius/fontSizes)而非组件硬编码;组件侧只在 XxxCssVariables 白名单加键。API 表同步:变体字面量变化必须在文档 API 表的枚举值里体现(docs:lint 核对行存在性,枚举值内容靠自查)。测试:变体的 varsResolver 产出可直接单测(构造 theme + props 断言变量值),视觉回归加 e2e/screenshots.spec.ts 基线(注意基线跨平台不通用)。
版本号手工改三个 package.json(不用 changesets),CHANGELOG 维护在 apps/docs/CHANGELOG.zh-CN.md。流程走 scripts/release.mjs 三档:第 1 档 pnpm release:preflight(不联网:工作树干净、在 main、与 origin/main 对齐、三包版本可解析、ui 的 peer 下限能被本次 hooks 版本满足);第 2 档 pnpm release:check(预检 + 三包依次「测试 → dry-run」,dry-run 会先跑包级 prepublishOnly 即重新 build,列出将来上传的清单——记住只发 files: ["es"]);第 3 档 pnpm release:publish --otp=123456(每包先测试再发布 → npm view --prefer-online 逐包核对注册表 → 消费者侧 ESM 复验 /tmp/verify(比手工流程多验 pro 具名导出)→ 按 ui 版本打附注 tag 推送)。顺序不可变:ui 的 peer 指向 hooks、pro 的 peer 指向 ui 与 hooks,反序会出现「peer 指向注册表上还不存在的版本」的窗口。中断续跑:--from=@xiaoye-react/ui(核对超时但 pnpm 已打印 + 包名@版本 即已入库)。回滚:72 小时内 npm unpublish,之后只能 npm deprecate。只发其中一个包时用根脚本 pnpm publish:hooks / publish:ui / publish:pro(内部是 build + test + pnpm publish,不要用裸 npm publish——它不重写 workspace: 协议,pro 的 peer 会原样发出去直接坏)。
pro 包的准入判据是「模式而非组件」:组合 ui 现有组件、面向一类管理端页面(列表/表单/详情/仪表盘)。第 1 步:建 packages/@xiaoye-react/pro/src/<mode-name>/ 五件套(组件/module.css/test/index.ts)。第 2 步:组件用 factory(pro 现有三个模式都用非多态工厂)创建,经 useProps/useStyles 与 ui 保持同构;从 ui 导入时只用具名导出(peer 协议保证消费方同源)。第 3 步:packages/@xiaoye-react/pro/src/index.ts 追加组件与类型的成对导出。第 4 步:单测放同目录(参照 ProTable.test.tsx 的竞态语义测试风格)。第 5 步:文档站与 docs:lint 对齐(pro 组件的 API 表同样逐行核对);check:exports 覆盖 pro 包(脚本扫描全部 workspace 包)。第 6 步:发布走 SOP 四(pro 在序列末位)。参考范式:ProTable 的 request 驱动 + requestIdRef 竞态保护 + actionsRef.refresh() 命令式句柄,是「数据联动模式」的样板;SearchFilter 的 fields 声明式配置 + render 逃生口,是「表单编排模式」的样板。
若要在 monorepo 里新增示例/内部应用(如 apps/ 下):第 1 步 pnpm-workspace.yaml 的 apps/* glob 已覆盖,直接建包;第 2 步依赖声明用 workspace:* 引 @xiaoye-react/ui/@xiaoye-react/hooks;第 3 步样式引入 @xiaoye-react/ui/style.css 并包裹 UIProvider(《00-项目总览》第 5 节的接入清单);第 4 步若做 SSR,配置 ColorSchemeScript 与 getStyleNonce(CSP);第 5 步确认 check:exports 扫描不误报(脚本会扫描 apps/ 下含 package.json 的目录,exports 字段要指向真实文件);第 6 步注意 apps/nas-console 现状是仅含 dist 产物、源码未检出(第 13 篇台账项),新应用建议直接源码入库避免重蹈。
| 扩展动作 | 必跑门禁(按序) | 关键风险点 |
|---|---|---|
| 新增组件 | lint → typecheck → build → docs:lint → check:exports → test →(e2e) | API 表假行被 docs:lint 拦;未 build 先跑校验必红 |
| 新增 hook | lint → typecheck → test | peer 下限联动;README 口径(60+ vs 83) |
| 样式变体 | lint → typecheck → test → docs:lint | 变量白名单;截图基线跨平台 |
| 发布版本 | release:preflight → check → publish | 顺序 hooks→ui→pro;workspace: 协议;72h 回滚窗口 |
| 新增 pro 模式 | lint → typecheck → build → docs:lint → check:exports → test | 纯 ESM 包具名导出靠消费者复验兜底 |
| 新增应用 | lint → typecheck →(构建) | exports 指向真实文件;源码入库 |
六份 SOP 的共同尾注:所有「写代码」的动作结束后,提交信息用 pnpm commit 生成;推送前 git status --porcelain 必须为空——这同时是发布预检的前提。门禁清单与 CI 的对应关系见《06-部署运行》第 6 节,配置文件的逐个走读见《14-配置与环境详解》。
六份 SOP 各自的最易返工处,合并成一张防呆清单。SOP 一最常见的三次返工:API 表写了类型里不存在的行(docs:lint 拦截——返工成本最低的一类,先写类型再写表即可根治);忘了在组件文件尾部挂 namespace 类型出口(消费方拿不到 Foo.Props,二次封装断链);把变体样式写进 JS 条件而非 data 属性选择器(破坏「差异进变量、状态进 data」铁律,主题层与 StylesApi 从此覆盖不到)。SOP 二的返工点是导出了 hook 却没导出类型对(index.ts 的风格是函数与 UseXxxOptions/ReturnValue 成对出现),以及 ui 侧要用时才发现 peer 下限没抬——先核对 attachMediaListener 先例再动版本。SOP 三的返工点是白名单漏加新变量键(运行时不报错但 vars prop 被类型拒绝,容易误判为 styles-api 失效)。SOP 四的返工点是 dry-run 撞 ERR_PNPM_GIT_UNCLEAN 才想起来清理未跟踪文件(预检第 0 步就该看),以及发布到一半才发现 OTP 没带(包带 2FA 时 --publish 必须追加)。SOP 五的返工点是从 ui 导入了未导出的内部符号(pro 只能用 ui 的公开面,check:exports 会拦)。SOP 六的返工点是 exports 字段漏写导致 check:exports 扫描报错——新包的 exports 要么完整、要么没有(脚本对「无 exports」的包只校验 main/module/types)。这些返工点共同指向一个习惯:每个 SOP 的第 7 步(全量门禁)不是终点检查,而是把前面步骤的约定批量验证一遍的机器——依赖它而不是记忆,是这个仓库对协作者的基本假设。
除六份 SOP 外,还有四类低频扩展,给最小路径提示而不展开成完整 SOP。新增主题色板:不动任何组件,向 UIProvider 传 theme.colors(命名词 ×10 阶)即可,库内置色板在 core/UIProvider/default-colors.ts;若要改「颜色如何变成按钮背景」的规则本身,替换 theme.variantColorResolver(core/UIProvider/color-functions/),这是主题层最后一个可整体替换的函数。接入新的校验库:form 模块经 schema-resolver.ts 走 Standard Schema 接口(@standard-schema/spec),任何实现该规范的校验库(zod/valibot 等)零改动接入;扩展点只在库内 validators/ 十件套之外新增内置规则时,才需要动 packages/ui/src/form/validators/ 并补单测。新增 e2e 用例:先判断属于交互盲区还是视觉回归——前者新建 e2e/interactions/<topic>.spec.ts 并在 e2e/harness/interactions.tsx 加挂载点(如需新宿主区块),后者在 e2e/screenshots.spec.ts 加用例并本地生成基线;两类都不要在 jsdom 单测里硬凑。新增内部包:packages/@xiaoye-react/<name>/ 建 private 包(参照 store 或 notifications 的最小 package.json),注意 scripts/validate-package-exports.mjs 会扫描到它——exports 要么完整指向真实文件、要么暂不声明。这四类与六份 SOP 合起来,覆盖了这个仓库能发生的几乎全部「扩展」形态;共同的前置动作永远是同一条:先在既有代码里找一个最接近的先例读一遍,本仓的先例密度足够高,几乎每个场景都有现成范本。