构建屏幕
我们专注于自下而上构建 UI;从小处着手并增加复杂性。这样做使我们能够孤立地开发每个组件,找出其数据需求,并在 Storybook 中进行尝试。所有这些都无需启动服务器或构建屏幕!
在本章中,我们将继续通过在屏幕中组合组件并在 Storybook 中开发该屏幕来提高复杂性。
屏幕组件
由于我们的应用程序非常简单,因此我们将构建的屏幕非常简单,只是将 <TaskList>
组件包装在一些布局中,并应用一个带有来自 redux 的加载和错误状态的数据层(假设如果我们在连接到服务器时遇到一些问题,我们将设置该字段)。我们将这样做有两个原因:首先是保持数据管理和表示清晰分离,其次屏幕组件确实有助于在应用程序中移动内容以及在 Storybook 中进行设计评审。
让我们首先使用必要的字段更新我们的 store
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
的新组件
<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
中添加以下内容
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
模板中。
<InboxScreen />
然而,真正有趣的地方是在 Storybook 中渲染 story。
由于 loading
和 error
是 InboxScreen
组件的内部状态,它们通常不受外部控制,因此我们允许将它们作为参数传入。这将使我们能够在 Storybook 中展示这些变体。
因此,当我们在 inbox-screen.stories.js
中设置 story 时
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,
};
我们看到 Error
、Loading
和 Default
story 都运行良好。
在 Storybook 中循环浏览状态可以轻松测试我们是否正确完成了此操作
组件驱动开发
我们从底层的 Task
开始,然后发展到 TaskList
,现在我们已经到了整个屏幕 UI。我们的 InboxScreen
容纳了一个嵌套的容器组件,并包含随附的 story。
组件驱动开发 使您可以在组件层次结构向上移动时逐步扩展复杂性。好处包括更集中的开发过程和所有可能的 UI 排列组合的增加覆盖率。简而言之,CDD 帮助您构建更高质量和更复杂的用户界面。
我们还没有完成 - 当 UI 构建完成时,工作并没有结束。我们还需要确保它在一段时间内保持耐用。