storybook在联邦UI架构下的配置

storybook在联邦UI架构下的配置

#type / howto #status / evergreen #tech / ops / monorepo #tech / dev / architecture #topic / ui-patterns #resource / storybook

[!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-reactui-vue 包中配置 Storybook,而非在 ui-react-shadcn

理由:

  1. 完整性:L2 层聚合了 Hooks + 组件,可以展示完整的 API
  2. 统一入口:与业务层的导入路径保持一致
  3. 灵活性:可以轻松切换底层组件库(shadcn → Ant Design)而不影响 Storybook
  4. 文档价值:既展示组件外观,也展示 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: 4401
  • ui-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 包。

解决方案:

  1. 使用 grep 搜索所有包引用:

    grep -r "@dailyuse/domain-client" **/package.json
  2. 检查 packages/ 目录确认包是否存在:

    ls packages/
  3. 从所有 package.json 中移除不存在的包引用

  4. 重新安装:

    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 运行,但建议解决以保持架构清晰。

解决策略:

  1. 提取共享类型到独立的 types
  2. 使用依赖注入模式打破循环
  3. 重新审视包边界划分

启动与使用

开发模式

启动 React UI 的 Storybook:

pnpm nx run ui-react:storybook

启动 Vue UI 的 Storybook:

pnpm nx run ui-vue:storybook

访问地址:

构建静态文件

构建 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-vueAPI 设计✅ 展示 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-111.0.0初始版本,基于实际配置经验编写

[!success] 配置完成检查清单

  • Storybook 依赖已安装
  • .storybook 配置文件已创建
  • Nx 构建目标已添加
  • 至少一个示例 Story 可运行
  • 开发服务器启动无错误
  • 构建静态文件成功
  • 文档部署计划已制定
创建于 2026/2/11 更新于 2026/5/27