基于 Vite 8 + pnpm 10 + React 18(2026-03-27 更新)
| 当前任务 | 读这些章节 |
|---|---|
| 写新组件 | 二、三、四、十一 |
| 改造现有组件 | 二、三、五 |
| 做代码审查 | 二、三、十一 |
| 配置 Storybook/测试 | 六 |
| 提交/分支管理 | 七 |
| 做视觉审查 | 二、十二 |
不需要读全部,按任务找对应章节即可。
| 工具 | 版本 | 说明 |
|---|---|---|
| Node.js | 20.x | 推荐 nvm 管理 |
| pnpm | 10.x | 必须用 pnpm,不要用 yarn/npm |
pnpm install # 安装依赖
pnpm dev # 启动开发服务器
pnpm build # 生产构建
pnpm lint # Lint/* ❌ 禁止 */
color: #7C5CFC;
background: white;
padding: 8px;
border-radius: 4px;
font-size: 14px;
/* ✅ 正确 */
color: var(--wk-brand-primary);
background: var(--wk-bg-surface);
padding: var(--wk-sp-2);
border-radius: var(--wk-r-xs);
font-size: var(--wk-text-size-md);primitive.css → 原始调色板,禁止在组件里直接用
semantic.css → 语义层,组件里用这一层
component.css → 组件专属变量(待补充)
问题: 在 Vite 下,JS/TS import CSS 文件时,该文件里的 @import 链不会被递归展开,导致跨包的 token 变量全部为空。
现在的解法: viteFinal 里已配置 postcss-import 插件,编译时展开 @import 链,行为和 webpack css-loader 一致。
// viteFinal 里已有,不需要手动处理
css: { postcss: { plugins: [postcssImport()] } }所以 preview.ts 里只需要 import 入口文件:
// ✅ 正确,postcss-import 会自动展开 @import 链
import '../../../packages/dmworkbase/src/theme/index.css'注意: 如果新建了 Vite 项目或独立工具,没有这个配置时会遇到同样问题,解法是加 postcss-import。
- 项目用
body[theme-mode=dark]切换暗色 - 禁止用
@media (prefers-color-scheme: dark)做主题 - 禁止在暗色样式里硬编码颜色
这个组件知道业务数据吗?(Channel / Message / User / WKSDK)
├── 是 → Layer 3 业务组件(暂不重构)
└── 否 → 它依赖其他非 Semi 组件吗?
├── 是 → Layer 2 复合组件
└── 否 → Layer 1 原子组件(默认起点)
开发过程中层级可以升,不能降:
- 发现需要引入其他组件 → 升到 Layer 2
- 发现需要调接口/读全局状态 → 移到 Layer 3
grep "^import" packages/dmworkbase/src/Components/ComponentName/index.tsx \
| grep -v "react\|semi\|css\|png" | wc -l
# 依赖数 0 → Layer 1
# 依赖数 1-4 → Layer 2
# 依赖数 5+ → Layer 3✅ Layer 3 → Layer 2 → Layer 1 → Semi / React
❌ Layer 1 import Layer 2(原子不能依赖复合)
❌ 同层互相 import
// Layer 1 ✅ — 封装 Semi,暴露自己的 props
const WKButton: React.FC<WKButtonProps> = ({ variant, ...rest }) => { ... }
// Layer 2 ✅ — 用 Layer 1,不直接用 Semi Button
import WKButton from '../WKButton'
// Layer 3 ✅ — 允许直接用 Semi,优先用 WK 封装版
import { Notification } from '@douyinfe/semi-ui'
// ❌ 任何层 — 禁止直接用 Semi 基础交互组件
import { Button } from '@douyinfe/semi-ui' // 用 WKButton 代替
import { Checkbox } from '@douyinfe/semi-ui' // 用 Checkbox(本项目版)代替Layer 1 原子组件:
AiBadge / Search / WKButton / Checkbox / IconClick / InputEdit
WKInput / SpaceAvatar / ActionListItem
Layer 2 布局/复合组件:
WKNavHeader / WKAvatar / WKViewQueue / WKViewQueueHeader / WKLayout
SpaceItem / JoinSpaceModal
Layer 3 业务组件(暂不重构):
Conversation / ConversationList / MessageInput / UserInfo / ChannelSetting
GroupCard / MergeforwardMessageList
用上面的决策树判断是 Layer 1/2/3。
如果 agent 支持 MCP(Claude Code 等本地进程 agent):
Storybook 跑着时,MCP server 固定在 http://localhost:16006/mcp。
连上后可以查询:「有没有类似 XXX 的组件?」
如果 agent 不支持 MCP(OpenClaw ACP session 等):
# 列出所有现有组件
ls packages/dmworkbase/src/Components/
# 查某个组件的 props 和用法
cat packages/dmworkbase/src/Components/ComponentName/index.tsx
cat packages/dmworkbase/src/Components/ComponentName/ComponentName.stories.tsxpackages/dmworkbase/src/Components/ComponentName/
├── index.tsx ← 组件实现
├── index.css ← 样式(全部用 var(--wk-*) token)
└── ComponentName.stories.tsx ← Stories(和组件同步写)
// ✅ 必须同时有 default export 和 named export
const MyComponent: React.FC<MyComponentProps> = ({ ...props }) => {
return <div>...</div>
}
export default MyComponent
export { MyComponent } // 兼容有些地方用具名 importexport const Default: Story = { ... } // 默认状态
export const AllVariants: Story = { ... } // 所有 variant/size
export const States: Story = { ... } // disabled/loading/error
export const EdgeCases: Story = { ... } // 长文本/空值/极端数值
// 亮/暗主题用全局切换按钮验证,不需要单独 story# 1. Storybook 里所有 story 正常渲染,无报错
# 2. Console 验证 token 加载成功(空字符串 = token 没生效)
getComputedStyle(document.body).getPropertyValue('--wk-purple-500')
# 应返回:#7C5CFC
# 3. 切换亮/暗主题,组件样式跟着变
# 4. 扫调用方影响(改造现有组件时)
grep -rn "ComponentName" packages/ apps/ --include="*.tsx" --include="*.css" -l
# 5. 确认改动文件数合理
git diff --stat
# 超过 10 个文件要警觉
# 6. 禁止 push,禁止开 PR/MR- 搜索旧 class 名所有引用(CSS + TSX)
- 同步更新所有引用
- 验证主项目功能正常
- 才能提交
grep -rn "旧class名" packages/ apps/ --include="*.tsx" --include="*.css"- 改了 prop 名要加
@deprecated注释,旧 prop 保留兼容一段时间 onCheck→onChange这类变更,两个都保留,旧的标注 deprecated- 不能只改组件,不改调用方
- ❌ 同时保留新旧两个 class(临时兼容可以,但必须同一 PR 彻底迁移)
- ❌ 改了组件不验证主项目
- ❌ 假设「只有 Storybook 用这个组件」
# 固定端口 16006(避免与常用端口冲突)
pnpm storybook
# 等价于:storybook dev -p 16006 --ci
# 地址固定为:http://localhost:16006
# MCP server:http://localhost:16006/mcp(需要 agent 支持 MCP)确认是否在跑:
curl -s -o /dev/null -w "%{http_code}" http://localhost:16006
# 200 = 正在运行 | 000 = 未启动,先跑 pnpm storybook// ✅ import 用 @storybook/react-vite(不是 react-webpack5)
import type { Meta, StoryObj } from '@storybook/react-vite'
import React from 'react'
const meta: Meta<typeof Component> = {
title: 'Base/ComponentName', // Layer 1 用 Base/,Layer 2 用 Layout/
parameters: {
docs: {
description: {
component: '组件说明 + ⚠️ 使用注意事项(禁止用法)'
}
}
}
}Stories 文件已在主项目 tsconfig.json 里 exclude,不会被主项目 tsc 扫到。不需要手动处理。
feat/描述 功能开发
fix/描述 Bug 修复
chore/描述 工程/依赖/文档
refactor/描述 重构(不改功能)
Agent 开分支时,在描述里体现自己的角色或任务,例如:
feat/add-wkavatar-stories
fix/checkbox-class-name-migration
feat(scope): 简短描述 新功能
fix(scope): 简短描述 修复
refactor(scope): 简短描述 重构
chore(scope): 简短描述 工程变更
docs(scope): 简短描述 文档
- 禁止 push,禁止开 PR/MR,除非 Will 明确指示
- commit 前用
git diff --stat确认改动文件数,超过 10 个要警觉 - 不要提交
yarn.lock,项目用pnpm-lock.yaml - 基于
github/main建分支,不要基于其他分支
织码接到组件任务时,任务描述必须包含:
组件名:ComponentName
文件位置:packages/dmworkbase/src/Components/ComponentName/
Layer:1 / 2 / 3
复用组件:[列出要用到的已有组件,先查 MCP 确认]
Props:
- propName: type(说明,必填/选填)
交互:[描述交互行为]
边界条件:
- [边界情况1]
- [边界情况2]
Token 约束:[必须用的 token 变量,如 --wk-brand-primary]
禁止修改:[不能动的文件列表]
禁止行为:禁止 push,禁止开 PR/MR
// ❌ 旧写法
process.env.REACT_APP_API_URL
// ✅ 新写法
import.meta.env.VITE_API_URL
import.meta.env.DEV // 替代 NODE_ENV === 'development'
import.meta.env.PROD // 替代 NODE_ENV === 'production'// ❌ 旧写法
src={require("./assets/icon.png")}
// ✅ 新写法
import icon from "./assets/icon.png"
src={icon}| 坑 | 现象 | 解决方案 |
|---|---|---|
| pnpm 幽灵依赖 | 运行时 Module not found | 在 package.json 显式声明,或加到 .npmrc public-hoist-pattern |
| stories 被主项目 tsc 扫到 | TS 报错 moduleResolution | tsconfig.json exclude stories 和 .storybook |
| class component 在 StrictMode 下副作用双调用 | React 18 StrictMode 特性 | 改函数组件 + useEffect |
| 组件缺 default export | story 渲染报错「does not provide an export named default」 | index.tsx 必须同时有 default 和 named export |
- 单组件 Props 不超过 8 个,超过考虑拆分或用 config 对象合并
- 事件 handler 统一 on 前缀:onConfirm / onCancel / onChange
- boolean prop 用 is/has/can 前缀:isLoading / hasError / canEdit
- 不传整个对象,只传组件需要的字段
// ❌ 太多 props,职责不清
<Modal title onClose onConfirm onCancel onBack loading error data user channel />
// ✅ 合并相关 props
<Modal
title
state={{ loading, error }}
onConfirm onCancel
/>- 单文件不超过 150 行(含注释)→ 超过考虑拆子组件
- 有独立状态的 UI 块 → 抽成子组件
- 相同 JSX 片段出现 3 次以上 → 抽成组件
- 有独立生命周期逻辑 → 抽成子组件
- 超过 3 个 useState → 考虑抽成
useXxxhook - 异步请求逻辑不写在组件里 → 抽成 service 或 hook
useEffect依赖数组超过 4 个 → 考虑拆分逻辑
- 优先用
interface(可扩展),type用于联合类型 / 工具类型 - 禁止
any,用unknown+ 类型守卫替代 - 组件 Props 类型命名统一:
ComponentNameProps - 不用类型断言(
as)绕过类型检查
由 UI 审查机制在 Level 3 确认后自动追加 格式:场景类型 + 决策 + 适用条件 + 日期
用提高选择器优先级代替:
/* ❌ */
.my-btn { height: 46px !important; }
/* ✅ */
.wk-login-panel .semi-button.my-btn { height: 46px; }需要新颜色时先更新 packages/dmworkbase/src/theme/tokens.css,在那里定义,再通过 Token 引用。
/* ❌ */
.my-component { --my-special-color: #7C5CFC; }
/* ✅ 先在 tokens.css 定义,再引用 */
.my-component { color: var(--wk-brand-primary); }/* ❌ 直接改 Semi 内部 class */
.semi-button-primary { background: red; }
/* ✅ 在组件根节点覆盖 Token */
.my-component {
--semi-color-primary: var(--wk-brand-primary);
}项目用 body[theme-mode=dark] + Token 变量实现主题,不用媒体查询。