storybook在联邦UI架构下的配置
storybook在联邦UI架构下的配置
[!info] related notes federated-ui-architecture-dev-workflow
Storybook 配置指南:Monorepo 联邦 UI 架构
[!info] 适用场景 本指南适用于采用统一逻辑驱动的联邦 UI 架构的 Nx monorepo 项目,其中 UI 层分为多个抽象层级,支持多框架并存。
架构概览
四层 UI 架构设计
graph TD
A[ui-core 无框架核心逻辑] --> B[ui-react React Hooks]
A --> C[ui-vue Vue Composables]
B --> D[ui-react-shadcn React 组件库]
C --> E[ui-vue-shadcn Vue 组件库]
D --> F[业务应用层 apps/desktop]
E --> G[业务应用层 apps/web]
层级职责:
| 层级 | 包名 | 职责 | 依赖 |
|---|---|---|---|
| L1 | @dailyuse/ui-core | 框架无关的核心逻辑、状态管理、验证规则 | 无框架依赖 |
| L2 | @dailyuse/ui-react@dailyuse/ui-vue | 框架特定的 Hooks/Composables,连接核心逻辑 | ui-core + React/Vue |
| L3 | @dailyuse/ui-react-shadcn@dailyuse/ui-vue-shadcn | 样式化组件库,基于 shadcn/ui 设计系统 | ui-react/ui-vue + UI 库 |
| L4 | 业务应用 | 消费统一的组件接口 | ui-react/ui-vue(聚合导出) |
[!tip] 聚合导出模式
ui-react作为聚合层,通过export * from '@dailyuse/ui-react-shadcn'提供单一导入点,业务层只需import { Button, useForm } from '@dailyuse/ui-react'。
Storybook 配置策略
为何在 L2 层配置 Storybook?
[!question] 设计决策 在
ui-react和ui-vue包中配置 Storybook,而非在ui-react-shadcn中
理由:
- 完整性:L2 层聚合了 Hooks + 组件,可以展示完整的 API
- 统一入口:与业务层的导入路径保持一致
- 灵活性:可以轻松切换底层组件库(shadcn → Ant Design)而不影响 Storybook
- 文档价值:既展示组件外观,也展示 Hooks 用法
完整配置步骤
Step 1: 安装依赖
在 workspace 根目录的 package.json 中添加 Storybook 依赖:
{
"devDependencies": {
"@nx/storybook": "^22.2.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/react": "^8.6.14",
"@storybook/react-vite": "^8.6.14",
"@storybook/vue3": "^8.6.14",
"@storybook/vue3-vite": "^8.6.14",
"storybook": "^8.6.14"
}
}
[!warning] 版本兼容性 确保
@storybook/react和@storybook/react-vite版本一致,否则会出现模块解析错误:Failed to resolve import "@storybook/react/dist/entry-preview.mjs"
运行安装:
pnpm install
Step 2: 配置 ui-react 的 Storybook
2.1 创建 .storybook 目录
mkdir packages/ui-react/.storybook
2.2 配置 main.ts
文件路径: packages/ui-react/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: true,
},
};
export default config;
关键配置项:
stories: 指定 story 文件的匹配模式framework: 使用react-vite以获得最佳开发体验docs.autodocs: 自动生成文档页面
2.3 配置 preview.ts
文件路径: packages/ui-react/.storybook/preview.ts
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
Step 3: 添加 Nx 构建目标
在 packages/ui-react/project.json 中添加 Storybook 任务:
{
"targets": {
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {
"port": 4401,
"configDir": "packages/ui-react/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nx/storybook:build",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/ui-react",
"configDir": "packages/ui-react/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}
端口分配:
ui-react: 4401ui-vue: 4402
[!note] 端口规划 统一规划端口可以避免冲突,建议在文档中维护端口映射表。
Step 4: 创建示例 Story
文件路径: packages/ui-react/src/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
// 示例按钮组件
function Button({
label,
onClick,
variant = 'primary'
}: {
label: string;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}) {
const styles = {
padding: '10px 20px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
fontSize: '14px',
backgroundColor: variant === 'primary' ? '#0070f3' : '#eaeaea',
color: variant === 'primary' ? '#ffffff' : '#000000',
};
return (
<button style={styles} onClick={onClick}>
{label}
</button>
);
}
// Story 元数据
const meta: Meta<typeof Button> = {
title: 'Example/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Story 实例
export const Primary: Story = {
args: {
label: 'Primary Button',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
label: 'Secondary Button',
variant: 'secondary',
},
};
Step 5: 配置 ui-vue 的 Storybook
5.1 配置 main.ts
文件路径: packages/ui-vue/.storybook/main.ts
import type { StorybookConfig } from '@storybook/vue3-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|ts|tsx|vue)'
],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
autodocs: true,
},
};
export default config;
[!tip] Vue 特定配置 注意
stories模式中包含了.vue文件扩展名。
5.2 配置 preview.ts
文件路径: packages/ui-vue/.storybook/preview.ts
import type { Preview } from '@storybook/vue3';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
5.3 创建 Vue Story
文件路径: packages/ui-vue/src/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3';
const Button = {
name: 'Button',
props: {
label: {
type: String,
required: true,
},
variant: {
type: String,
default: 'primary',
validator: (value: string) => ['primary', 'secondary'].includes(value),
},
},
template: `
<button :style="styles" @click="handleClick">
{{ label }}
</button>
`,
computed: {
styles() {
return {
padding: '10px 20px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
fontSize: '14px',
backgroundColor: this.variant === 'primary' ? '#0070f3' : '#eaeaea',
color: this.variant === 'primary' ? '#ffffff' : '#000000',
};
},
},
methods: {
handleClick() {
console.log('Button clicked');
},
},
};
const meta: Meta<typeof Button> = {
title: 'Example/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
label: 'Primary Button',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
label: 'Secondary Button',
variant: 'secondary',
},
};
常见问题与解决方案
问题 1: Workspace 包未找到错误
错误信息:
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND: "@dailyuse/domain-client@workspace:*"
is in the dependencies but no package is present in the workspace
原因:
package.json 中引用了不存在的 workspace 包。
解决方案:
-
使用 grep 搜索所有包引用:
grep -r "@dailyuse/domain-client" **/package.json -
检查
packages/目录确认包是否存在:ls packages/ -
从所有
package.json中移除不存在的包引用 -
重新安装:
pnpm install
[!bug] 常见遗留引用
@dailyuse/domain-client@dailyuse/domain-server@dailyuse/application-client@dailyuse/application-server@dailyuse/infrastructure-client@dailyuse/infrastructure-server
问题 2: Nx 项目图处理失败
错误信息:
Failed to process project graph.
Plugin worker @nx/vitest exited unexpectedly with code 0
解决方案:
# 重置 Nx 缓存
pnpm nx reset
# 重新运行命令
pnpm nx run ui-react:storybook
[!info] 原因 Nx 缓存损坏或与新配置不兼容时可能出现此问题。
问题 3: Storybook 模块解析错误
错误信息:
Failed to resolve import "@storybook/react/dist/entry-preview.mjs"
原因:
缺少 @storybook/react 核心包,仅安装 @storybook/react-vite 不足够。
解决方案:
pnpm install @storybook/react @storybook/vue3
依赖关系:
@storybook/react-vite→ 需要@storybook/react@storybook/vue3-vite→ 需要@storybook/vue3
问题 4: 循环依赖警告
警告信息:
WARN There are cyclic workspace dependencies:
D:\home\projects\dailyuse\packages\domain-shared,
D:\home\projects\dailyuse\packages\editor
影响: 通常不影响 Storybook 运行,但建议解决以保持架构清晰。
解决策略:
- 提取共享类型到独立的
types包 - 使用依赖注入模式打破循环
- 重新审视包边界划分
启动与使用
开发模式
启动 React UI 的 Storybook:
pnpm nx run ui-react:storybook
启动 Vue UI 的 Storybook:
pnpm nx run ui-vue:storybook
访问地址:
- React: http://localhost:4401
- Vue: http://localhost:4402
构建静态文件
构建 React UI 文档:
pnpm nx run ui-react:build-storybook
构建 Vue UI 文档:
pnpm nx run ui-vue:build-storybook
输出目录:
- React:
dist/storybook/ui-react/ - Vue:
dist/storybook/ui-vue/
CI/CD 集成
在 CI pipeline 中构建 Storybook:
- name: Build Storybook
run: |
pnpm nx run ui-react:build-storybook --configuration=ci
pnpm nx run ui-vue:build-storybook --configuration=ci
最佳实践
1. Story 组织结构
推荐的目录结构:
packages/ui-react/src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.stories.tsx
│ │ └── Button.test.tsx
│ └── Input/
│ ├── Input.tsx
│ ├── Input.stories.tsx
│ └── Input.test.tsx
├── hooks/
│ ├── useForm.ts
│ └── useForm.stories.tsx # Hooks 也可以有 stories
└── index.ts
[!tip] 就近原则 Story 文件与组件文件放在同一目录,便于维护和查找。
2. Story 命名规范
// 使用 ComponentName.stories.tsx 格式
// ✅ 正确
Button.stories.tsx
FormInput.stories.tsx
DataTable.stories.tsx
// ❌ 避免
button.stories.tsx
ButtonStories.tsx
stories-button.tsx
3. Story 分类
使用 title 字段组织 Story 层级:
const meta: Meta = {
// 基础组件
title: 'Components/Button',
// 表单组件
title: 'Forms/Input',
// Hooks
title: 'Hooks/useForm',
// 复合组件
title: 'Patterns/LoginForm',
};
4. 展示 Hooks 用法
虽然 Hooks 本身不是视觉组件,但可以通过包装器展示:
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from './useForm';
function FormExample() {
const { values, handleChange, handleSubmit } = useForm({
initialValues: { email: '', password: '' }
});
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Submit</button>
<pre>{JSON.stringify(values, null, 2)}</pre>
</form>
);
}
const meta: Meta<typeof FormExample> = {
title: 'Hooks/useForm',
component: FormExample,
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {};
5. 聚合导出的 Story 策略
由于 ui-react 重导出 ui-react-shadcn,Story 中应直接从 ui-react-shadcn 导入组件:
// ✅ 正确:直接从源包导入
import { Button } from '@dailyuse/ui-react-shadcn';
// ❌ 避免:从聚合包导入(可能导致构建配置复杂)
import { Button } from '@dailyuse/ui-react';
原因:
- 避免 tsup 的
external配置影响 Storybook 的模块解析 - 保持 Storybook 构建独立性
- 清晰表明组件来源
6. 主题和样式集成
在 preview.ts 中集成全局样式:
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css'; // 导入全局样式
const preview: Preview = {
parameters: {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
],
},
},
decorators: [
(Story) => (
<div style={{ padding: '2rem' }}>
<Story />
</div>
),
],
};
export default preview;
架构优势总结
1. 关注点分离
| 层级 | 关注点 | Storybook 作用 |
|---|---|---|
| ui-core | 业务逻辑 | 无需 Storybook |
| ui-react/ui-vue | API 设计 | ✅ 展示 Hooks + 组件 |
| ui-react-shadcn | 样式实现 | 可选,通过 L2 间接展示 |
| 业务应用 | 功能集成 | 无需 Storybook |
2. 可移植性
当需要更换组件库时(如 shadcn → Ant Design):
// 只需修改 L2 层的重导出
// packages/ui-react/src/index.ts
// 从 shadcn 切换到 antd
- export * from '@dailyuse/ui-react-shadcn';
+ export * from '@dailyuse/ui-react-antd';
业务层代码无需改动,Storybook 配置也无需改动!
3. 文档统一性
业务开发者只需查阅 ui-react 的 Storybook,即可了解:
- 可用的组件列表
- Hooks 的使用方式
- 组件的 API 设计
- 实际的视觉效果
延伸阅读
- [[nx-monorepo-best-practices|Nx Monorepo 最佳实践]]
- [[ui-architecture-design|UI 架构设计原则]]
- [[component-testing-strategy|组件测试策略]]
- [[storybook-accessibility-testing|Storybook 无障碍测试]]
变更日志
| 日期 | 版本 | 变更内容 |
|---|---|---|
| 2026-02-11 | 1.0.0 | 初始版本,基于实际配置经验编写 |
[!success] 配置完成检查清单
- Storybook 依赖已安装
.storybook配置文件已创建- Nx 构建目标已添加
- 至少一个示例 Story 可运行
- 开发服务器启动无错误
- 构建静态文件成功
- 文档部署计划已制定