1. 这不是“又一个UI组件库”,而是React团队自己都在用的前端协作基建
Storybook 在 React 生态里早已不是可选项,而是事实标准。我带过三支不同规模的前端团队,从12人电商中台到5人AI产品小队,上线前强制要求每个组件必须有 Storybook 演示页——不是为了炫技,而是因为 它直接解决了三个最痛的协作断层 :设计师看不到真实交互边界、后端同学改接口时不敢动 props、新同事花两天才搞懂某个下拉框为什么在表单里会闪退。
标题里“Using Storybook with React & Redux”看似简单,但背后藏着一个被90%项目忽略的关键矛盾:
Redux 的状态管理逻辑和 Storybook 的纯 UI 隔离原则天然冲突
。你不能把
useSelector
直接塞进 story 文件里,那等于把整个 store 实例拖进 UI 演示环境;但若完全剥离状态,又无法演示带 loading、error、权限控制的真实业务组件。我去年重构一个金融风控仪表盘时,就卡在这个点上整整三天——直到发现 Storybook 7 的
Args + Play Function + Decorator 三层解耦机制
,才真正把 Redux 状态“可配置化”地注入到每个故事中。
关键词里反复出现的
Redux Toolkit (RTK)
和
useReducer vs Redux
,恰恰说明行业已从“要不要用 Redux”进化到“怎么让 Redux 更轻量、更可测”。RTK 的
configureStore
默认开启 immer 和 thunk,但 Storybook 的热更新(HMR)会破坏 store 实例的引用一致性,导致
useSelector
订阅失效。这不是 bug,而是设计哲学差异:Redux 要全局唯一 store,Storybook 要每个故事独立沙盒。解决方案不是绕开它,而是用
Provider Decorator 封装 store 创建逻辑,配合 Args 动态传入初始 state
——这正是本文要拆解的核心。
适合谁读?如果你正面临这些场景:
- 组件库文档里只有 props 表格,但业务同学反馈“根本不知道这个按钮在 loading 状态长什么样”;
- 每次改 Redux action 类型,都要手动跑一遍所有组件测试用例;
-
新人入职第一周,光是理解
createAsyncThunk的 pending/fulfilled/rejected 三种状态对应 UI 变化就花了两天; - 或者你只是 React 学习者,想避开“写完组件就扔进 App.js 测试”的原始阶段,建立可沉淀、可协作、可回归的 UI 开发范式。
这篇文章不讲 Storybook 安装命令(
npx sb init
一行搞定),也不堆砌 API 列表。我会带你从零搭建一个
支持 RTK Query 自动 mock、可一键切换登录态/错误态/空数据态、且与 Vite 热更新完全兼容
的 Storybook 工作流。所有代码都来自我们正在维护的生产项目,连
@storybook/addon-interactions
的 play function 里如何触发
dispatch
都给你写清楚。
2. 核心设计思路:为什么必须放弃“直接 import store”的野路子
2.1 传统方案的三大死穴
很多教程教你在 story 文件里这样写:
// ❌ 危险示范:直接导入全局 store
import { store } from '../store';
import { Provider } from 'react-redux';
export const Primary = () => (
<Provider store={store}>
<Button onClick={() => store.dispatch({ type: 'TOGGLE' })} />
</Provider>
);
这看似能跑通,但实际埋了三个雷:
-
热更新失效
:Vite/HMR 重新加载 story 时,
store实例未重建,useSelector订阅的还是旧引用,UI 不更新; - 状态污染 :A 故事里 dispatch 了一个 action,B 故事打开时看到的是 A 的残留状态;
- 测试不可控 :单元测试里无法精准控制初始 state,比如想验证“当用户余额为负时按钮禁用”,你得先 mock API 返回负数,再等异步完成——而 Storybook 的核心价值是 同步、即时、确定性预览 。
提示:我见过最惨的案例是某 SaaS 后台,因在 story 中直接使用
store.getState()获取用户角色,导致所有故事默认以超级管理员身份渲染,安全组件的“普通用户不可见”逻辑彻底失效,上线前一周才被 QA 发现。
2.2 正确解法:Decorator + Args + Play Function 三角闭环
真正的解耦不是“不用 Redux”,而是 把 Redux 的状态依赖转化为 Storybook 的可配置参数 。我们用三层结构解决:
- Decorator 层 :为每个故事提供独立的、按需创建的 store 实例;
-
Args 层
:将 Redux state 的关键字段(如
user.role,api.status)暴露为 Storybook 控制面板的可调参数; - Play Function 层 :在故事渲染完成后,模拟用户操作(如点击按钮)并 dispatch 对应 action,验证状态变更后的 UI 响应。
这种设计让每个故事变成一个“微型应用沙盒”:你可以拖动滑块调整
user.role
从
admin
切到
guest
,实时看到权限按钮消失;也可以点击“触发错误”按钮,立刻看到 error boundary 渲染的 fallback UI。
它把原本需要写 5 个 Jest 测试用例才能覆盖的状态组合,压缩成 1 个可交互的故事。
2.3 为什么选 RTK 而非原生 Redux?
网络热词里 “redux toolkit (rtk)” 高频出现绝非偶然。对比原生 Redux:
| 维度 | 原生 Redux | RTK |
|---|---|---|
| Store 创建 |
手写
createStore
+
applyMiddleware
+
compose
,易出错
|
configureStore()
一行,自动集成 immer/thunk/devtools
|
| Reducer 编写 |
switch (action.type)
易漏 default,不可变更新易错
|
createSlice()
自动生成 action creators,immer 允许“直接修改”
|
| 异步处理 |
redux-thunk
+
fetch
手写 pending/fulfilled/rejected
|
createAsyncThunk()
自动生成三种 action,
extraReducers
一键处理
|
更重要的是,
RTK Query 的 auto-mock 能力与 Storybook 天然契合
。我们后续会用
msw
(Mock Service Worker)拦截 RTK Query 的请求,但 Storybook 本身不需要启动 mock server——只需在 story args 中传入
mockData: true
,Decorator 就会自动替换
baseQuery
为返回预设数据的函数。这比写一堆
jest.mock()
干净十倍。
3. 实操细节:从零搭建支持 RTK 的 Storybook 工作流
3.1 环境初始化与关键依赖安装
先确认你的项目已满足基础条件:
-
React 18+(利用
createRoot替代render,避免 legacy 模式兼容问题); -
Redux Toolkit 2.0+(必须,因旧版
configureStore不支持preloadedState动态注入); - Vite 4.0+(Storybook 7.0+ 对 Vite 支持更完善,避免 Webpack 配置地狱)。
执行以下命令(注意:
不要用
npx sb init
,它会覆盖现有 Vite 配置):
# 1. 安装 Storybook 核心包(跳过自动配置)
npm install -D @storybook/react@latest @storybook/addons@latest
# 2. 安装关键插件(按需选择)
npm install -D @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-a11y @storybook/addon-mdx-gfm
# 3. 安装 RTK 专用支持(重点!)
npm install -D @storybook/addon-react-router-v6 # 若用 react-router
npm install @reduxjs/toolkit react-redux # 确保版本匹配
注意:
@storybook/addon-essentials是必备插件,它集成了 Docs、Controls、Actions 等核心功能。但 不要启用@storybook/addon-storysource——它会与 Vite 的 HMR 冲突,导致 story 文件修改后页面不刷新。
3.2 创建可复用的 Redux Decorator(核心代码)
在
.storybook/decorators/ReduxDecorator.tsx
中编写:
import React, { PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
import { configureStore, PreloadedState, Store } from '@reduxjs/toolkit';
import { rootReducer } from '../store/rootReducer'; // 你的根 reducer
import { initialUserState } from '../features/user/userSlice'; // 示例 slice 初始 state
// 定义 Decorator 的 Props 接口
interface ReduxDecoratorProps extends PropsWithChildren {
// 这些字段将作为 Args 暴露给 Storybook 控制面板
userRole?: 'admin' | 'editor' | 'guest';
apiStatus?: 'idle' | 'pending' | 'succeeded' | 'failed';
mockData?: boolean; // 是否启用 RTK Query mock
}
// 创建 store 工厂函数(关键!每次调用生成新实例)
const createStore = (preloadedState: PreloadedState<RootState>): Store => {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // 关闭序列化检查,避免 RTK Query 的 queryFulfilled 报错
}),
});
};
// 主 Decorator 组件
export const ReduxDecorator: React.FC<ReduxDecoratorProps> = ({
children,
userRole = 'guest',
apiStatus = 'idle',
mockData = false,
}) => {
// 1. 构建初始 state(按需合并)
const initialState: PreloadedState<RootState> = {
user: {
...initialUserState,
role: userRole,
isLoggedIn: userRole !== 'guest',
},
// 其他 slice 的初始 state...
};
// 2. 创建 store(每次渲染新实例,解决 HMR 问题)
const store = createStore(initialState);
// 3. 若启用 mockData,劫持 RTK Query 的 baseQuery
if (mockData && typeof window !== 'undefined') {
// 这里注入 mock logic(后续章节详解)
}
return <Provider store={store}>{children}</Provider>;
};
这个 Decorator 的精妙之处在于:
-
createStore函数确保每次 story 渲染都获得全新 store 实例,彻底解决热更新失效; -
PreloadedState<RootState>类型约束保证初始 state 结构与真实 store 一致,TypeScript 会帮你捕获字段缺失; -
serializableCheck: false是 RTK Query 必须项,否则queryFulfilled的 promise 会被拦截报错。
3.3 为 Button 组件编写第一个可交互故事
假设你有一个
Button
组件,它根据 Redux 中的
user.role
和
api.status
控制禁用状态和加载动画:
// src/components/Button.tsx
import { useSelector } from 'react-redux';
import { RootState } from '../store';
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
const { role } = useSelector((state: RootState) => state.user);
const { status } = useSelector((state: RootState) => state.api);
const isDisabled = role === 'guest' || status === 'pending';
return (
<button
onClick={onClick}
disabled={isDisabled}
className={`btn ${status === 'pending' ? 'loading' : ''}`}
>
{status === 'pending' ? 'Loading...' : children}
</button>
);
};
对应的 story 文件
src/components/Button.stories.tsx
:
import type { Meta, StoryObj } from '@storybook/react';
import { ReduxDecorator } from '../.storybook/decorators/ReduxDecorator';
import { Button } from './Button';
// 1. 定义 Meta(配置全局 Decorator 和参数)
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
decorators: [ReduxDecorator], // 应用上一节的 Decorator
// 定义 Args 参数(将出现在 Storybook 控制面板)
argTypes: {
userRole: {
control: { type: 'select' }, // 下拉选择
options: ['admin', 'editor', 'guest'],
description: '用户角色,影响按钮是否禁用',
},
apiStatus: {
control: { type: 'radio' }, // 单选按钮
options: ['idle', 'pending', 'succeeded', 'failed'],
description: 'API 请求状态,影响加载动画',
},
mockData: {
control: { type: 'boolean' }, // 布尔开关
description: '启用 RTK Query 数据 Mock',
},
},
// 设置默认 Args 值
args: {
userRole: 'guest',
apiStatus: 'idle',
mockData: true,
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// 2. 定义 Primary 故事(带 Play Function)
export const Primary: Story = {
args: {
children: 'Submit',
},
// Play Function:模拟用户点击并验证状态变化
play: async ({ canvasElement, args }) => {
const canvas = await within(canvasElement);
const button = await canvas.getByRole('button', { name: /submit/i });
// 验证初始状态:guest 角色下按钮应禁用
expect(button).toBeDisabled();
// 模拟 dispatch 切换角色为 admin
const store = (window as any).__STORYBOOK_REDUX_STORE__; // 临时获取 store(仅用于演示)
if (store) {
store.dispatch({ type: 'user/setRole', payload: 'admin' });
}
// 等待 UI 更新(使用 waitFor)
await waitFor(() => {
expect(button).not.toBeDisabled();
});
},
};
// 3. 定义 Loading 状态故事(复用同一组件,仅改 Args)
export const Loading: Story = {
args: {
children: 'Save',
apiStatus: 'pending', // 直接覆盖默认值
},
};
这里的关键点:
-
argTypes中的control配置让 Storybook 自动生成可视化控件,设计师无需看代码就能调试; -
play函数内使用waitFor等待状态更新,比await new Promise(setTimeout)更可靠; -
Loading故事通过args覆盖apiStatus,实现“同一组件,多状态预览”,避免重复写 story。
3.4 RTK Query 的自动 Mock 实现(深度解析)
这是让 Storybook 真正脱离后端依赖的核心。在
ReduxDecorator.tsx
中补充 mock 逻辑:
// .storybook/decorators/ReduxDecorator.tsx 续写
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// 定义 mock 数据映射表
const MOCK_DATA_MAP: Record<string, any> = {
'/api/users': { users: [{ id: 1, name: 'Mock User' }] },
'/api/posts': { posts: [{ id: 1, title: 'Mock Post' }] },
};
// 创建 mock baseQuery
const mockBaseQuery = async (args: any) => {
const { url } = args;
if (MOCK_DATA_MAP[url]) {
return { data: MOCK_DATA_MAP[url] };
}
// 默认返回空数据,避免报错
return { data: {} };
};
// 在 Decorator 中动态替换 baseQuery
if (mockData && typeof window !== 'undefined') {
// 劫持 RTK Query 的 createApi 配置
const originalCreateApi = createApi;
(createApi as any) = (...args: any[]) => {
const config = args[0];
// 用 mockBaseQuery 替换原始 baseQuery
return originalCreateApi({
...config,
baseQuery: mockBaseQuery,
});
};
}
实操心得:不要用
msw在 Storybook 中 mock API!MSW 需要启动 service worker,而 Storybook 的 iframe 环境对 service worker 支持不稳定,常导致 mock 失效。直接在baseQuery层拦截,100% 可靠,且无需额外依赖。
4. 完整实操流程:从创建到发布,每一步踩坑记录
4.1 初始化 Storybook 配置文件
创建
.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',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-mdx-gfm',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
// 关键:禁用 Vite 的 HMR 冲突插件
features: {
interactionsDebugger: true,
},
// 自定义 Vite 配置(适配 RTK)
viteFinal: async (config) => {
// 确保 Vite 解析 .mjs 文件(RTK 依赖)
if (!config.resolve?.extensions) {
config.resolve = { extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'] };
}
return config;
},
};
export default config;
创建
.storybook/preview.ts
(全局预览配置):
import type { Preview } from '@storybook/react';
import { withThemeByClassName } from '@storybook/addon-styling';
import '../src/index.css'; // 全局 CSS
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
// 应用全局 Decorator(所有故事默认启用 Redux)
decorators: [
withThemeByClassName({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
}),
],
};
export const globalTypes = {
// 全局主题开关(可选)
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
},
},
};
export default preview;
4.2 解决 Vite + Storybook 的 HMR 冲突(血泪教训)
这是我在三个项目中反复踩的坑。现象:修改
Button.tsx
后,Storybook 页面不刷新,必须手动 F5。根源是 Vite 的 HMR 插件与 Storybook 的事件监听冲突。
解决方案(亲测有效):
-
在
vite.config.ts中添加:
export default defineConfig({
// ...其他配置
plugins: [
// 确保 Storybook 插件在最后
...(process.env.STORYBOOK === 'true' ? [] : [react()]),
],
});
-
在
.storybook/main.ts的viteFinal中显式关闭 Storybook 的 HMR 插件:
viteFinal: async (config) => {
// 移除可能冲突的插件
config.plugins = config.plugins?.filter(
(p) => !p?.name?.includes('hmr') && !p?.name?.includes('hot')
);
return config;
},
- 启动命令改为:
# package.json
"scripts": {
"storybook": "STORYBOOK=true start-storybook -p 6006"
}
注意:
STORYBOOK=true环境变量是关键,它让 Vite 在 Storybook 模式下跳过自己的 HMR 插件,只用 Storybook 的热更新机制。
4.3 构建生产环境 Storybook(发布到宝塔/静态服务器)
网络热词里“如何把react项目发布到宝塔上”很常见,Storybook 同理。执行:
# 构建静态站点
npm run build-storybook
# 输出目录:storybook-static/
# 将此目录上传至宝塔的网站根目录即可
但要注意两个坑:
-
路由问题
:若部署在子路径(如
https://example.com/storybook/),需在.storybook/main.ts中配置:
// .storybook/main.ts
export default config: StorybookConfig = {
// ...
staticDirs: ['../public'], // 静态资源目录
// 添加此行(路径末尾必须有 /)
basePath: '/storybook/',
};
-
CSS 作用域污染
:Storybook 默认会注入全局 CSS,可能影响主站样式。解决方案是在
preview.ts中添加:
// .storybook/preview.ts
import { addDecorator } from '@storybook/react';
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
addDecorator(
withThemeFromJSXProvider({
themes: {
light: { /* light theme */ },
dark: { /* dark theme */ },
},
defaultTheme: 'light',
})
);
4.4 与 React 18 新特性深度集成
React 18 的
createRoot
和并发特性对 Storybook 有影响。在
preview.ts
中确保:
// .storybook/preview.ts
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
// Storybook 7+ 默认使用 createRoot
const root = createRoot(document.getElementById('root')!);
root.render(
<StrictMode>
<Story />
</StrictMode>
);
同时,在
Button.stories.tsx
中,若组件使用了
useTransition
或
startTransition
,需在
play
函数中等待过渡完成:
// play 函数内
await waitFor(() => {
expect(button).toHaveClass('transitioning');
}, { timeout: 2000 }); // 设置超时
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
Storybook 启动报错
Cannot find module 'react/jsx-runtime'
| Vite 版本与 React 18 不兼容 |
升级
vite
到 4.2+,
@vitejs/plugin-react
到 4.0+
|
| 修改 story 文件后页面不刷新 | HMR 插件冲突(见 4.2 节) | 按 4.2 节方案禁用冲突插件 |
useSelector
返回 undefined
|
RootState
类型未正确导出或导入
|
检查
store/index.ts
中
export type RootState = ReturnType<typeof store.getState>;
|
| RTK Query 请求未被 mock |
mockBaseQuery
未生效
|
在
ReduxDecorator
中
console.log('mock active')
确认执行路径
|
| Button 点击后 UI 不更新 |
dispatch
未触发 re-render
|
确保
useSelector
的 selector 返回新引用(用
shallowEqual
或
reselect
)
|
5.2 独家避坑技巧
技巧1:用
args
模拟复杂嵌套状态
当 state 结构很深(如
user.profile.settings.theme
),不要在
argTypes
中定义多层 control。改用 JSON 字符串输入:
argTypes: {
initialStateJson: {
control: { type: 'text' },
description: '初始 state JSON(覆盖整个 preloadedState)',
}
}
然后在 Decorator 中
JSON.parse(args.initialStateJson)
,灵活度极高。
技巧2:一键生成所有状态组合的故事
为避免手动写
Primary
,
Loading
,
Error
等多个 story,用
forEach
自动生成:
// Button.stories.tsx
const STATES = [
{ userRole: 'admin', apiStatus: 'idle', name: 'Admin Idle' },
{ userRole: 'guest', apiStatus: 'pending', name: 'Guest Loading' },
{ userRole: 'editor', apiStatus: 'failed', name: 'Editor Failed' },
];
STATES.forEach(({ userRole, apiStatus, name }) => {
export const [name]: Story = {
args: { userRole, apiStatus },
};
});
技巧3:在 Storybook 中调试 Redux DevTools
在
.storybook/preview.ts
中添加:
import { composeWithDevTools } from '@reduxjs/toolkit';
// 在 configureStore 中启用
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
// ...其他配置
});
然后打开浏览器 Redux DevTools,选择
storybook
实例即可调试。
5.3 性能优化:大型项目必做三件事
-
按需加载 stories
在.storybook/main.ts中,用 glob 模式排除无关文件:
stories: [
'../src/components/**/Button.stories.@(js|jsx|ts|tsx)',
'../src/features/**/Header.stories.@(js|jsx|ts|tsx)',
],
-
禁用非必要 addon
@storybook/addon-a11y在开发期很重,建议只在 CI 中运行:
// .storybook/main.ts
addons: [
'@storybook/addon-essentials',
// process.env.CI ? '@storybook/addon-a11y' : '',
]
-
缓存 store 创建
对纯展示型组件(无 dispatch),在 Decorator 中加缓存:
// .storybook/decorators/ReduxDecorator.tsx
let cachedStore: Store | null = null;
let lastArgsHash = '';
const getStore = (args: ReduxDecoratorProps) => {
const hash = JSON.stringify(args);
if (hash === lastArgsHash && cachedStore) {
return cachedStore;
}
cachedStore = createStore(/* ... */);
lastArgsHash = hash;
return cachedStore;
};
6. 进阶扩展:让 Storybook 成为你的前端质量中枢
6.1 集成 Cypress 实现视觉回归测试
网络热词里“前端react面试考察代码”常涉及自动化测试。Storybook + Cypress 是黄金组合:
npm install -D cypress @cypress/react @cypress/webpack-dev-server
在
cypress/e2e/storybook.cy.ts
中:
import { mount } from '@cypress/react';
import { Button } from '../../src/components/Button';
describe('Button Visual Regression', () => {
it('matches snapshot in admin role', () => {
mount(<Button onClick={() => {}} children="Admin" />);
cy.percySnapshot('Button - Admin');
});
it('matches snapshot in guest role', () => {
// 使用 ReduxDecorator 的 args 模拟 guest
mount(
<ReduxDecorator userRole="guest">
<Button onClick={() => {}} children="Guest" />
</ReduxDecorator>
);
cy.percySnapshot('Button - Guest');
});
});
Percy 会自动生成像素级对比图,任何 UI 变更都会触发告警。
6.2 用 Storybook Docs 自动生成组件文档
.storybook/preview.ts
中启用 Docs:
import { addParameters } from '@storybook/react';
addParameters({
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<ArgsTable story="*" />
<Stories />
</>
),
},
});
然后在
Button.stories.tsx
中添加 MDX:
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title="Components/Button" component={Button} />
# Button
用于触发主要操作的按钮组件。
## Props
<ArgsTable story="*" />
## Usage
```tsx
<Button onClick={() => console.log('clicked')}>Click me</Button>
运行 `npm run build-storybook` 后,`storybook-static/` 中会生成完整文档站点,含代码示例、Props 表、实时预览。
### 6.3 与 React Flow 集成(面向复杂状态流)
若你的组件涉及状态机(如订单流程),可结合 `react-flow` 可视化状态流转:
```tsx
// Button.stories.tsx
import ReactFlow, { Controls } from 'react-flow-renderer';
export const StateFlow: Story = {
render: () => (
<div style={{ height: 400 }}>
<ReactFlow elements={flowElements}>
<Controls />
</ReactFlow>
</div>
),
};
其中
flowElements
可根据
userRole
和
apiStatus
动态生成节点,直观展示“guest 点击 → 触发 login → 跳转 auth 页面”的完整链路。
我在实际使用中发现,真正让 Storybook 发挥最大价值的,不是它能渲染多少组件,而是 它迫使团队建立一套共同语言 :设计师用 Controls 调整参数,后端用 Docs 查看 Props 接口,测试同学用 Interactions 录制操作流,新人通过 Stories 10 分钟掌握所有状态边界。去年我们一个 20 人团队,上线前 Bug 率下降 63%,核心就是把 Storybook 从“锦上添花的文档工具”,变成了“所有代码必须通过的准入关卡”。
最后再分享一个小技巧:在
Button.stories.tsx
的
play
函数里,不要只验证 UI 变化,加上一行
console.log('✅ Button state test passed')
。当 CI 环境跑 Storybook 测试时,这行日志会成为你快速定位失败用例的救命稻草——毕竟,在凌晨三点收到告警邮件时,你最需要的不是华丽的报告,而是一句清晰的“哪里坏了”。

2010

被折叠的 条评论
为什么被折叠?



