返回至Storybook 简介
章节
  • 开始
  • 简单组件
  • 复合组件
  • 数据
  • 屏幕
  • 部署
  • 测试
  • 插件
  • 结论
  • 贡献

构建屏幕

用组件构建屏幕
此社区翻译尚未更新至最新版本的 Storybook。请帮助我们将英文指南中的更改应用于此翻译,以更新它。 欢迎提交 Pull Request.

我们专注于自下而上构建 UI;从小处着手并增加复杂性。这样做使我们能够孤立地开发每个组件,找出其数据需求,并在 Storybook 中进行尝试。所有这些都无需启动服务器或构建屏幕!

在本章中,我们将继续通过在屏幕中组合组件并在 Storybook 中开发该屏幕来提高复杂性。

屏幕组件

由于我们的应用程序非常简单,因此我们将构建的屏幕非常简单,只是将 <TaskList> 组件包装在一些布局中,并应用一个带有来自 redux 的加载和错误状态的数据层(假设如果我们在连接到服务器时遇到一些问题,我们将设置该字段)。我们将这样做有两个原因:首先是保持数据管理和表示清晰分离,其次屏幕组件确实有助于在应用程序中移动内容以及在 Storybook 中进行设计评审。

让我们首先使用必要的字段更新我们的 store

复制
app/store.js

import { createStore } from 'tracked-redux';

export const actions = {
  ARCHIVE_TASK: 'ARCHIVE_TASK',
  PIN_TASK: 'PIN_TASK',
  // The new actions to handle both error and loading state
+ SET_ERROR: 'SET_ERROR',
+ SET_LOADING: 'SET_LOADING',
};

// The action creators bundle actions with the data required to execute them
export const archiveTask = id => ({ type: actions.ARCHIVE_TASK, id });
export const pinTask = id => ({ type: actions.PIN_TASK, id });

+ export const setError = () => ({ type: actions.SET_ERROR });
+ export const setLoading = () => ({ type: actions.SET_LOADING });

// A sample set of tasks
const defaultTasks = [
  { id: '1', title: 'Something', state: 'TASK_INBOX' },
  { id: '2', title: 'Something more', state: 'TASK_INBOX' },
  { id: '3', title: 'Something else', state: 'TASK_INBOX' },
  { id: '4', title: 'Something again', state: 'TASK_INBOX' },
];

// Store initial state
const initialState = {
  isError: false,
  isLoading: false,
  tasks: defaultTasks,
};

// All our reducers simply change the state of a single task.
function taskStateReducer(taskState) {
  return (state, action) => {
    return {
      ...state,
      tasks: state.tasks.map(task =>
        task.id === action.id ? { ...task, state: taskState } : task
      ),
    };
  };
}

// The reducer describes how the contents of the store change for each action
const reducers = (state, action) => {
  switch (action.type) {
    case actions.ARCHIVE_TASK:
      return taskStateReducer('TASK_ARCHIVED')(state, action);
    case actions.PIN_TASK:
      return taskStateReducer('TASK_PINNED')(state, action);
+   case actions.SET_ERROR:
+     return {
+       ...state,
+       isError: true,
+     };
+   case actions.SET_LOADING:
+     return {
+       ...state,
+       isLoading: true,
+     };
    default:
      return state || initialState;
  }
};

export const store = createStore(reducers);

接下来,在 app/components 目录中创建一个名为 inbox-screen.hbs 的新组件

复制
app/components/inbox-screen.hbs
<div>
  <div class="page lists-show">
    <nav>
      <h1 class="title-page">
        <span class="title-wrapper">
          Taskbox
        </span>
      </h1>
    </nav>
    {{#if this.loading}}
      <LoadingRow />
      <LoadingRow />
      <LoadingRow />
      <LoadingRow />
      <LoadingRow />
    {{else if this.error}}
      <div class="page lists-show">
        <div class="wrapper-message">
          <span class="icon-face-sad"></span>
          <div class="title-message">
            Oh no!
          </div>
          <div class="subtitle-message">
            Something went wrong
          </div>
        </div>
      </div>
    {{else}}
      <TaskList
        @tasks={{this.tasks}}
        @pinTask={{this.pinTask}}
        @archiveTask={{this.archiveTask}}
      />
    {{/if}}
  </div>
</div>

我们现在有了一种处理应用程序各种状态的方法。我们现在可以将用于处理数据加载和错误处理的现有逻辑移至组件中。

app/components/inbox-screen.js 中添加以下内容

复制
app/components/inbox-screen.js
import Component from '@glimmer/component';
import { action } from '@ember/object';

import { store, pinTask, archiveTask } from '../store';

export default class InboxScreenComponent extends Component {
  get loading() {
    return this.args.loading ?? store.getState().isLoading;
  }

  get error() {
    return this.args.error ?? store.getState().isError;
  }

  get tasks() {
    return store
      .getState()
      .tasks.filter(t => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
  }

  @action
  pinTask(task) {
    store.dispatch(pinTask(task));
  }

  @action
  archiveTask(task) {
    store.dispatch(archiveTask(task));
  }
}

我们现在可以安全地删除我们在上一章中创建的 tasks 文件夹,并将 InboxScreen 添加到 application 模板中。

复制
app/templates/aplication.hbs
<InboxScreen />

然而,真正有趣的地方是在 Storybook 中渲染 story。

由于 loadingerrorInboxScreen 组件的内部状态,它们通常不受外部控制,因此我们允许将它们作为参数传入。这将使我们能够在 Storybook 中展示这些变体。

因此,当我们在 inbox-screen.stories.js 中设置 story 时

复制
app/components/inbox-screen.stories.js
import { hbs } from 'ember-cli-htmlbars';

export default {
  title: 'InboxScreen',
  component: 'InboxScreen',
};

const Template = args => ({
  template: hbs`<InboxScreen @error={{this.error}} @loading={{this.loading}} />`,
  context: args,
});

export const Default = Template.bind({});
Default.args = {
  loading: false,
  error: false,
};

export const Error = Template.bind({});
Error.args = {
  ...Default.args,
  error: true,
};

export const Loading = Template.bind({});
Loading.args = {
  ...Default.args,
  loading: true,
};

我们看到 ErrorLoadingDefault story 都运行良好。

💡 在 Ember 中,路由支持 loadingerror 状态。尽管动机是更多地转向基于组件的方法。请查看 ember-await,它很好地封装了数据管理的理念,并具有指示每种状态的机制。

在 Storybook 中循环浏览状态可以轻松测试我们是否正确完成了此操作

组件驱动开发

我们从底层的 Task 开始,然后发展到 TaskList,现在我们已经到了整个屏幕 UI。我们的 InboxScreen 容纳了一个嵌套的容器组件,并包含随附的 story。

组件驱动开发 使您可以在组件层次结构向上移动时逐步扩展复杂性。好处包括更集中的开发过程和所有可能的 UI 排列组合的增加覆盖率。简而言之,CDD 帮助您构建更高质量和更复杂的用户界面。

我们还没有完成 - 当 UI 构建完成时,工作并没有结束。我们还需要确保它在一段时间内保持耐用。

💡 不要忘记使用 git 提交您的更改!
这份免费指南对您有帮助吗?发推文以表示赞赏并帮助其他开发者找到它。
下一章
部署
了解如何在线部署 Storybook
✍️ 在 GitHub 上编辑 – 欢迎提交 PR!
加入社区
6,721位开发者及更多
为什么为什么选择 Storybook组件驱动的 UI
开源软件
Storybook - Storybook 中文

由以下机构维护
Chromatic - Storybook 中文
特别感谢 Netlify CircleCI