返回Storybook 简介
章节
  • 开始上手
  • 简单的组件
  • 复合组件
  • 数据
  • 结论
  • 贡献

构建一个复合组件

从更简单的组件组合成复合组件
此社区翻译尚未更新到最新的 Storybook 版本。请将英文指南中的更改应用到此翻译中,帮助我们更新它。 欢迎提交 Pull Request.

上一章我们构建了第一个组件;本章将扩展我们学到的知识来构建 `TaskList`,一个任务列表。让我们将组件组合在一起,看看引入更多复杂性时会发生什么。

任务列表

Taskbox 通过将已固定任务置于默认任务之上来强调它们。这将产生两种 `TaskList` 变体,您需要为其创建故事:默认项目和默认加已固定项目。

default and pinned tasks

由于 `Task` 数据可以异步发送,我们**还**需要一个在没有连接的情况下渲染的加载状态。此外,当没有任务时,还需要一个空状态。

empty and loading tasks

准备就绪

复合组件与其包含的基本组件并没有太大区别。创建一个 `TaskList` 组件和一个伴随的故事文件:`components/TaskList.jsx` 和 `components/TaskList.stories.jsx`。

TaskList 的粗略实现开始。你需要导入之前的 Task 组件,并将属性和操作作为输入传递。

复制
components/TaskList.jsx
import { Task } from './Task';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { styles } from './styles';

export const TaskList = ({ loading, tasks, onPinTask, onArchiveTask }) => {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  if (loading) {
    return (
      <View style={styles.listItems}>
        <Text>loading</Text>
      </View>
    );
  }

  if (tasks.length === 0) {
    return (
      <View style={styles.listItems}>
        <Text>empty</Text>
      </View>
    );
  }

  return (
    <View style={styles.listItems}>
      <FlatList
        data={tasks}
        keyExtractor={(task) => task.id}
        renderItem={({ item }) => (
          <Task key={item.id} task={item} {...events} />
        )}
      />
    </View>
  );
};

接下来在故事文件中创建 `TaskList` 的测试状态。

复制
components/TaskList.stories.jsx
import { TaskList } from './TaskList';
import { Default as TaskStory } from './Task.stories';
import { View } from 'react-native';

export default {
  component: TaskList,
  title: 'TaskList',
  decorators: [
    (Story) => (
      <View style={{ padding: 42, flex: 1 }}>
        <Story />
      </View>
    ),
  ],
  argTypes: {
    onPinTask: { action: 'onPinTask' },
    onArchiveTask: { action: 'onArchiveTask' },
  },
};

export const Default = {
  args: {
    // Shaping the stories through args composition.
    // The data was inherited from the Default story in Task.stories.js.
    tasks: [
      { ...TaskStory.args.task, id: '1', title: 'Task 1' },
      { ...TaskStory.args.task, id: '2', title: 'Task 2' },
      { ...TaskStory.args.task, id: '3', title: 'Task 3' },
      { ...TaskStory.args.task, id: '4', title: 'Task 4' },
      { ...TaskStory.args.task, id: '5', title: 'Task 5' },
      { ...TaskStory.args.task, id: '6', title: 'Task 6' },
    ],
  },
};

export const WithPinnedTasks = {
  args: {
    tasks: [
      ...Default.args.tasks.slice(0, 5),
      { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
    ],
  },
};

export const Loading = {
  args: {
    tasks: [],
    loading: true,
  },
};

export const Empty = {
  args: {
    // Shaping the stories through args composition.
    // Inherited data coming from the Loading story.
    ...Loading.args,
    loading: false,
  },
};

装饰器 是为故事提供任意包装器的一种方式。在这种情况下,我们使用装饰器在列表周围添加填充,以便于视觉验证。它们还可以用于将故事包装到“提供程序”中——即设置 React 上下文的库组件。

TaskStory.args.task 提供了我们从 `Task.stories.js` 文件中创建和导出的 `Task` 的形状。类似地,我们为 `onPinTask` 和 `onArchiveTask` 添加的 `argTypes` 告诉 Storybook 提供 `TaskList` 组件所需的动作(模拟回调)。

如果您没有立即看到新故事,请尝试重新加载应用程序。如果这不起作用,您可以重新运行 `yarn storybook-generate` 来重新生成 `storybook.requires` 文件。

现在在 Storybook 中查看新的 `TaskList` 故事。

a gif showing the task list component in storybook

构建状态

我们的组件仍然很粗糙,但现在我们对要努力的故事有了一个想法。您可能认为 `listitems` 包装器过于简单。您是对的——在大多数情况下,我们不会创建一个新组件只是为了添加一个包装器。但是 `TaskList` 组件的**真正复杂性**体现在 `withPinnedTasks`、`loading` 和 `empty` 的边缘情况下。

对于加载情况,我们将创建一个新组件来显示加载动画。

创建一个名为 `LoadingRow.jsx` 的新文件,内容如下:

复制
components/LoadingRow.jsx
import { useState, useEffect } from 'react';
import { Animated, Text, View, Easing, StyleSheet } from 'react-native';
import { styles } from './styles';

const GlowView = ({ style, children }) => {
  const [glowAnim] = useState(new Animated.Value(0.3));

  useEffect(() => {
    Animated.loop(
      Animated.sequence([
        Animated.timing(glowAnim, {
          toValue: 1,
          duration: 1500,
          easing: Easing.ease,
          useNativeDriver: true,
        }),
        Animated.timing(glowAnim, {
          toValue: 0.3,
          duration: 1500,
          easing: Easing.ease,
          useNativeDriver: true,
        }),
      ])
    ).start();
  }, []);

  return (
    <Animated.View
      style={{
        ...style,
        opacity: glowAnim,
      }}
    >
      {children}
    </Animated.View>
  );
};

export const LoadingRow = () => (
  <View style={styles.container}>
    <GlowView>
      <View style={styles.loadingItem}>
        <View style={styles.glowCheckbox} />
        <Text style={styles.glowText}>Loading</Text>
        <Text style={styles.glowText}>cool</Text>
        <Text style={styles.glowText}>state</Text>
      </View>
    </GlowView>
  </View>
);

并将 `TaskList.jsx` 更新为以下内容:

复制
components/TaskList.jsx
import { Task } from './Task';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { LoadingRow } from './LoadingRow';
import { MaterialIcons } from '@expo/vector-icons';
import { styles } from './styles';

export const TaskList = ({ loading, tasks, onPinTask, onArchiveTask }) => {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  if (loading) {
    return (
      <View style={[styles.listItems, { justifyContent: "center" }]}>
        <LoadingRow />
        <LoadingRow />
        <LoadingRow />
        <LoadingRow />
        <LoadingRow />
        <LoadingRow />
      </View>
    );
  }

  if (tasks.length === 0) {
    return (
      <View style={styles.listItems}>
        <View style={styles.wrapperMessage}>
          <MaterialIcons name="check" size={64} color={"#2cc5d2"} />
          <Text style={styles.titleMessage}>You have no tasks</Text>
          <Text style={styles.subtitleMessage}>Sit back and relax</Text>
        </View>
      </View>
    );
  }

  const tasksInOrder = [
    ...tasks.filter((t) => t.state === 'TASK_PINNED'),
    ...tasks.filter((t) => t.state !== 'TASK_PINNED'),
  ];
  return (
    <View style={styles.listItems}>
      <FlatList
        data={tasksInOrder}
        keyExtractor={(task) => task.id}
        renderItem={({ item }) => (
          <Task key={item.id} task={item} {...events} />
        )}
      />
    </View>
  );
};

这些更改会导致以下 UI:

TaskList with loading state

成功!我们完成了我们最初设定的目标。如果我们查看更新后的 UI,我们可以很快发现我们的已固定任务现在显示在列表顶部,符合预期的设计。在接下来的章节中,我们将扩展我们所学的知识,通过将这些原则应用于更复杂的 UI,继续增加我们应用程序的复杂性。

💡 别忘了使用 git 提交你的更改!
这个免费指南对您有帮助吗?请在 Twitter 上分享以表示赞赏,并帮助其他开发者发现它。
下一章
数据
了解如何将数据集成到你的 UI 组件中
✍️ 在 GitHub 上编辑 – PR 欢迎!
加入社区
7,424开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索关于
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI