构建简单组件
我们将遵循组件驱动开发 (CDD) 方法来构建 UI。这是一个从“自下而上”的过程,从组件开始,最终形成完整的页面。CDD 有助于你在构建 UI 时应对不断增加的复杂度。
任务
Task
是我们应用中的核心组件。每个任务根据其确切状态会略有不同地显示。我们展示一个选中(或未选中)的复选框、一些关于任务的信息以及一个“固定”按钮,允许我们将任务在列表中上下移动。综合起来,我们需要这些 props
title
– 描述任务的字符串state
- 任务当前在哪一个列表中,以及是否已完成?
当我们开始构建 Task
时,我们首先编写与上面草拟的不同类型的任务相对应的测试状态。然后我们使用 Storybook 隔离地构建组件,使用模拟数据。我们将逐步对组件在每种状态下的外观进行“可视化测试”。
这个过程类似于测试驱动开发(TDD),我们可以称之为“可视化 TDD”。
环境搭建
我们打开 .storybook/main.js
文件并看一下
module.exports = {
stories: ['../components/**/*.stories.?(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-ondevice-controls',
'@storybook/addon-ondevice-actions',
],
};
如果你查看 stories
属性,你会看到 Storybook 在 components
文件夹中查找 stories。
在 React Native 的 Storybook 中,由于 Metro bundler 的当前限制,我们依赖于 main.js
中的配置来生成一个名为 storybook.requires
的文件,该文件用于加载项目中的所有 stories 和插件。当你运行 yarn storybook
来启动 Storybook 时,该文件会自动生成。
💡 你也可以通过运行 yarn storybook-generate
命令手动生成 storybook.requires
文件。但是,除非你看到某个 story 没有加载,或者注意到 main.js
配置中的更改没有反映在你的 Storybook 中,否则你不需要重新创建这个文件。要了解更多关于如何生成 storybook.requires
文件的信息,你可以查看你的 metro.config.js
文件中的 withStorybook
函数。
现在我们来创建任务组件及其配套的 story 文件:components/Task.jsx
和 components/Task.stories.jsx
。
我们将从 Task
的基础实现开始,它只需接收我们所需的属性以及你可以对任务执行的两种操作(在列表之间移动任务)
import { StyleSheet, TextInput, View } from 'react-native';
import { styles } from './styles';
export const Task = ({
task: { id, title, state },
onArchiveTask,
onPinTask,
}) => {
return (
<View style={styles.listItem}>
<TextInput value={title} editable={false} />
</View>
);
};
现在添加 story 文件
import { Task } from './Task';
export default {
title: 'Task',
component: Task,
argTypes: {
onPinTask: { action: 'onPinTask' },
onArchiveTask: { action: 'onArchiveTask' },
},
};
export const Default = {
args: {
task: {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
},
},
};
export const Pinned = {
args: { task: { ...Default.args.task, state: 'TASK_PINNED' } },
};
export const Archived = {
args: { task: { ...Default.args.task, state: 'TASK_ARCHIVED' } },
};
在我们的 stories 文件中,我们使用了名为 Component Story Format (CSF) 的语法。这种语法使得编写 stories 更加容易,并且受到最新版 Storybook 的支持。
Storybook 中有两种基本的组织层级:组件及其子 stories。可以将每个 story 视为组件的一种变体。一个组件可以拥有任意数量的 stories。
- 组件
- Story
- Story
- Story
为了让 Storybook 了解我们正在文档化的组件,我们创建一个 default
导出,其中包含
component
-- 组件本身title
-- 在 Storybook 应用的侧边栏中如何引用该组件argTypes
-- 允许我们指定 args 的类型,这里我们用它来定义动作,这些动作将在每次交互发生时被记录下来
为了定义我们的 stories,我们导出一个带有 args
属性的对象。参数(简称 args
)允许我们使用 controls 插件实时编辑组件,而无需重启 Storybook。一旦 args
的值发生变化,组件也会随之变化。
创建 story 时,我们使用一个基础的 task
arg 来构建组件期望的任务结构。通常根据实际数据建模。同样, export
-ing 导出这种结构将使我们能够在后续的 stories 中重用它,稍后你将看到。
现在我们已经完成了基础设置,让我们重新运行 yarn storybook
命令,看看变化。如果你已经有 Storybook 在运行,你也可以运行 yarn storybook-generate
来重新生成 storybook.requires
文件。
你应该会看到一个类似这样的 UI:
你可以使用屏幕底部的菜单打开导航菜单,然后点击在 stories 之间切换。之后,你可以点击其他地方或向下拖动来关闭菜单。通过按下底部栏最右侧的图标,你可以找到插件。
构建状态
现在我们可以开始构建组件以匹配设计稿。
目前组件仍然很基础。首先编写实现设计稿的代码,无需过于深入细节。
import { MaterialIcons } from '@expo/vector-icons';
import { StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { styles } from './styles';
export const Task = ({
task: { id, title, state },
onArchiveTask,
onPinTask,
}) => {
return (
<View style={styles.listItem}>
<TouchableOpacity onPress={() => onArchiveTask(id)}>
{state !== "TASK_ARCHIVED" ? (
<MaterialIcons
name="check-box-outline-blank"
size={24}
color="#26c6da"
/>
) : (
<MaterialIcons name="check-box" size={24} color="#26c6da" />
)}
</TouchableOpacity>
<TextInput
placeholder="Input Title"
value={title}
editable={false}
style={
state === "TASK_ARCHIVED"
? styles.listItemInputTaskArchived
: styles.listItemInputTask
}
/>
<TouchableOpacity onPress={() => onPinTask(id)}>
<MaterialIcons
name="star"
size={24}
color={state !== "TASK_PINNED" ? "#eee" : "#26c6da"}
/>
</TouchableOpacity>
</View>
);
};
完成后,它应该看起来像这样
组件构建完成!
现在我们已经成功地构建了一个组件,而无需服务器或运行整个应用程序。下一步是以类似的方式逐个构建剩余的 Taskbox 组件。
如你所见,隔离构建组件既简单又快速。我们可以期望产出更高质量、更少 bug、更精良的 UI,因为可以深入测试每一种可能的状态。