用于 React Native 的 Storybook
[!重要]
本自述文件适用于 v9 beta 版本,有关 v8 文档,请参阅v8.6 文档。
使用用于 React Native 的 Storybook,你可以在不运行你的应用程序的情况下设计和开发独立的 React Native 组件。
如果你正在从 8 版本迁移到 9 版本,你可以在这里找到迁移指南
有关 Storybook 的更多信息,请访问:storybook.js.org
[!注意]
确保你的 Storybook 依赖项与主版本号保持一致,否则你将会看到异常行为。
目录
- 🚀 入门
- 📒 编写故事
- 🔌 插件
- 📱 隐藏/显示 Storybook
- ⚙️ withStorybook 包装器
- 🔧 getStorybookUI 选项
- 🧪 在单元测试中使用故事
- 🤝 贡献
- ✨ 示例
入门
新项目
有一些项目模板已配置了 @storybook/react-native
和 @storybook/addon-react-native-web
,并附带一个简单的示例。
对于 Expo,你可以使用这个模板,使用以下命令
# With NPM
npx create-expo-app --template expo-template-storybook AwesomeStorybook
对于 React Native CLI,你可以使用这个模板
npx react-native init MyApp --template react-native-template-storybook
现有项目
运行 init 命令来设置你的项目,包括所有依赖项和配置文件
npx storybook@latest init
唯一需要做的就是在你的应用程序入口点(例如 App.tsx
)中像这样返回 Storybook 的 UI
export { default } from './.storybook';
然后使用 withStorybook 函数包装你的 metro 配置,如下面所示
如果你想在 Storybook 和你的应用程序之间轻松切换,请参阅这篇博客文章
如果你想自己手动添加所有内容,请查看这里的手动指南。
附加步骤:更新你的 metro 配置
我们需要 unstable_allowRequireContext 转换器选项,以根据 main.ts
中的 stories glob 启用动态故事导入。我们还可以从 metro 配置中调用 storybook generate 函数,以便在 metro 运行时自动生成 storybook.requires.ts
文件。
Expo
首先创建一个 metro 配置文件,如果你还没有的话。
npx expo customize metro.config.js
然后使用 withStorybook 函数包装你的配置,如下所示。
// metro.config.js
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const withStorybook = require('@storybook/react-native/metro/withStorybook');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = withStorybook(config, {
// Set to false to remove storybook specific options
// you can also use a env variable to set this
enabled: true,
// Path to your storybook config
configPath: path.resolve(__dirname, './.storybook'),
// Optional websockets configuration
// Starts a websocket server on the specified port and host on metro start
// websockets: {
// port: 7007,
// host: 'localhost',
// },
});
React Native
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const path = require('path');
const withStorybook = require('@storybook/react-native/metro/withStorybook');
const defaultConfig = getDefaultConfig(__dirname);
/**
* Metro configuration
* https://reactnative.net.cn/docs/metro
*
* @type {import('metro-config').MetroConfig}
*/
const config = {};
// set your own config here 👆
const finalConfig = mergeConfig(defaultConfig, config);
module.exports = withStorybook(finalConfig, {
// Set to false to remove storybook specific options
// you can also use a env variable to set this
enabled: true,
// Path to your storybook config
configPath: path.resolve(__dirname, './.storybook'),
// Optional websockets configuration
// Starts a websocket server on the specified port and host on metro start
// websockets: {
// port: 7007,
// host: 'localhost',
// },
});
Reanimated 设置
确保你的项目中包含 react-native-reanimated
并在你的 babel 配置中设置了插件。
// babel.config.js
plugins: ['react-native-reanimated/plugin'],
编写故事
在 Storybook 中,我们使用一种称为 CSF 的语法,看起来像这样
import type { Meta, StoryObj } from '@storybook/react';
import { MyButton } from './Button';
const meta = {
component: MyButton,
} satisfies Meta<typeof MyButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
text: 'Hello World',
color: 'purple',
},
};
你应该在 .storybook
文件夹中的 main.ts
配置文件中配置你的故事文件路径。
// .storybook/main.ts
import { StorybookConfig } from '@storybook/react-native';
const main: StorybookConfig = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [],
};
export default main;
装饰器和参数
对于故事,你可以在默认导出或特定的故事上添加装饰器和参数。
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta = {
title: 'Button',
component: Button,
decorators: [
(Story) => (
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
<Story />
</View>
),
],
parameters: {
backgrounds: {
values: [
{ name: 'red', value: '#f00' },
{ name: 'green', value: '#0f0' },
{ name: 'blue', value: '#00f' },
],
},
},
} satisfies Meta<typeof Button>;
export default meta;
对于全局装饰器和参数,你可以将它们添加到 .storybook
文件夹中的 preview.tsx
文件中。
// .storybook/preview.tsx
import type { Preview } from '@storybook/react';
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
const preview: Preview = {
decorators: [
withBackgrounds,
(Story) => (
<View style={{ flex: 1, color: 'blue' }}>
<Story />
</View>
),
],
parameters: {
backgrounds: {
default: 'plain',
values: [
{ name: 'plain', value: 'white' },
{ name: 'warm', value: 'hotpink' },
{ name: 'cool', value: 'deepskyblue' },
],
},
},
};
export default preview;
插件
CLI 将为你安装一些基本插件,例如 controls 和 actions。On-device 插件是指那些可以在你在手机上看到的设备 UI 中渲染的插件。
目前可用的插件有
@storybook/addon-ondevice-controls
:实时调整你的组件属性@storybook/addon-ondevice-actions
:使用 actions 模拟 onPress 调用,这些 actions 会在 actions 标签页中记录信息@storybook/addon-ondevice-notes
:为你的故事添加一些 Markdown 内容,以帮助记录它们的用法@storybook/addon-ondevice-backgrounds
:改变 Storybook 的背景,以便在不同背景下比较你的组件外观
安装你想要使用的每个插件,并按照如下方式将它们添加到 main.ts
的 addons 列表中
// .storybook/main.ts
import { StorybookConfig } from '@storybook/react-native';
const main: StorybookConfig = {
// ... rest of config
addons: [
'@storybook/addon-ondevice-notes',
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-backgrounds',
'@storybook/addon-ondevice-actions',
],
};
export default main;
在你的故事中使用插件
有关每个 on-device 插件的详细信息,你可以查看其自述文件
隐藏/显示 Storybook
React Native 上的 Storybook 是一个普通的 React Native 组件,可以根据你自己的逻辑在你的 RN 应用程序中的任何地方使用或隐藏。
你还可以创建一个专门用于 Storybook 的独立应用程序,这个应用程序也可以作为你的可视化组件包使用。有些人选择通过使用 React Native 开发者菜单中的自定义选项来切换 Storybook 组件。
withStorybook 包装器
withStorybook
是一个包装函数,用于扩展你的 Storybook Metro 配置。它接受你现有的 Metro 配置和一个选项对象,用于配置 Storybook 的启动方式。
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const withStorybook = require('@storybook/react-native/metro/withStorybook');
const defaultConfig = getDefaultConfig(__dirname);
module.exports = withStorybook(defaultConfig, {
enabled: true,
// See API section below for available options
});
选项
enabled
类型:boolean
,默认值:true
确定指定的选项是否应用于 Metro 配置。这对于同时使用 Storybook 和不使用 Storybook 的项目设置很有用,并且需要有条件地应用这些选项。在这个例子中,它是通过使用环境变量来实现条件化的
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const withStorybook = require('@storybook/react-native/metro/withStorybook');
const defaultConfig = getDefaultConfig(__dirname);
module.exports = withStorybook(defaultConfig, {
enabled: process.env.WITH_STORYBOOK,
// ... other options
});
onDisabledRemoveStorybook
类型:boolean
,默认值:false
如果 onDisabledRemoveStorybook 为 true
且 enabled
为 false
,storybook 包将从构建中移除。如果你想从生产构建中移除 Storybook,这很有用。
useJs
类型:boolean
,默认值:false
生成 .storybook/storybook.requires
文件,使用 JavaScript 而不是 TypeScript。
configPath
类型:string
,默认值:path.resolve(process.cwd(), './.storybook')
你的 Storybook 配置文件目录的位置,它包含 main.ts
和其他项目相关文件。
websockets
类型:{ host: string?, port: number? }
,默认值:undefined
如果指定,在启动时创建一个 WebSocket 服务器。这允许你同步多个设备以显示相同的故事和连接到 UI 中故事的 arg 值。
websockets.host
类型:string
,默认值:'localhost'
运行 WebSocket 的主机,如果指定。
websockets.port
类型:number
,默认值:7007
运行 WebSocket 的端口,如果指定。
getStorybookUI 选项
你可以将这些参数传递给你的 storybook 入口点中的 getStorybookUI 调用
{
initialSelection?: string | Object (undefined)
-- initialize storybook with a specific story. eg: `mybutton--largebutton` or `{ kind: 'MyButton', name: 'LargeButton' }`
storage?: Object (undefined)
-- {getItem: (key: string) => Promise<string | null>;setItem: (key: string, value: string) => Promise<void>;}
-- Custom storage to be used instead of AsyncStorage
onDeviceUI?: boolean;
-- show the ondevice ui
enableWebsockets?: boolean;
-- enable websockets for the storybook ui
query?: string;
-- query params for the websocket connection
host?: string;
-- host for the websocket connection
port?: number;
-- port for the websocket connection
secured?: boolean;
-- use secured websockets
shouldPersistSelection?: boolean;
-- store the last selected story in the device's storage
theme: Partial<Theme>;
-- theme for the storybook ui
}
在单元测试中使用故事
Storybook 提供了测试实用工具,允许你在外部测试环境(如 Jest)中重用你的故事。这样你就可以更容易地编写单元测试,并重用 Storybook 中已完成的设置,但在你的单元测试中。你可以在便携式故事部分找到更多关于它的信息。
贡献
我们欢迎对 Storybook 的贡献!
正在寻找第一个可以解决的问题?
- 我们为我们认为适合新接触代码库或一般开源项目的人的问题打上 Good First Issue 标签。
- 联系我们,我们会找到适合你技能和学习兴趣的事情。
示例
这里有一些示例项目可以帮助你入门
- @axeldelafosse 创建的 Mono Repo 设置 https://github.com/axeldelafosse/storybook-rnw-monorepo
- Expo 设置 https://github.com/dannyhw/expo-storybook-starter
- React Native CLI 设置 https://github.com/dannyhw/react-native-storybook-starter
- 为 RN CLI 项目在原生文件中添加一个独立的入口点和开发者菜单项:https://github.com/zubko/react-native-storybook-with-dev-menu
- 想展示你自己的项目?提交一个 PR 并将其添加到列表中!