Storybook 作为一个 UI 开发工具,能帮助开发人员独立创建组件,并在隔离的开发环境中以交互方式展示组件。 Storybook 是在主应用程序之外运行的,因此用户可以独立开发 UI 组件,而不必担心应用程序特定的依赖关系和要求。
Storybook 的基本思想是遵循 CDD + TDD 原则进行组件开发。
CDD 是 Component Drive Development,即以组件为基础自底而上的开发流程,每个组件都可作为一个独立的 story 进行开发。
TDD 是 Test Drive Development,在开发过程中对每个组件进行 UI 及功能上的测试。
本文介绍在 Vue 框架下的 App 中使用 Storybook 的方法。
以下图所示的 Taskbox App 为例。
Setup Vue Storybook
使用 Vue CLI 创建 App,并启用 Storybook 和 Jest 测试
# Create our application, using a preset that contains jest:
npx -p @vue/cli vue create taskbox --preset hichroma/vue-preset-learnstorybook
cd taskbox
# Add Storybook:
npx -p @storybook/cli sb init
可以使用以下命令检查 App 是否初始化成功:
# Run the test runner (Jest) in a terminal:
npm run test:unit
# Start the component explorer on port 6006:
npm run storybook
# Run the frontend app proper on port 8080:
npm run serve
创建组件的 Story
使用 TDD 的开发模式,在开发每个组件时为这个组件创建一个或多个 story,例如:
- 组件:
Task.vue
- 组件的 story:
Task.stories.js
上述 Taskbox 的最基础组件就是 Task,一个 Task 包含 title、state 两个属性,及 onPinTask、onArchiveTask 两个事件。Task 组件的 story 样例如下:
// src/components/Task.stories.js
// 使用stroybook提供的addon-actions库来mock事件的处理
import { action } from '@storybook/addon-actions';
// 引入组件
import Task from './Task.vue';
export default {
// story的标题
title: 'Task',
// 排除不需要storybook渲染的文件
excludeStories: /.*Data$/,
};
// mock事件处理
export const actionsData = {
onPinTask: action('onPinTask'),
onArchiveTask: action('onArchiveTask'),
};
// 创建测试数据
export const taskData = {
id: '1',
title: 'Test Task',
state: 'Task_INBOX'
};
// Task组件为TASK_PINNED状态时的story
export const Pinned = () => ({
components: { Task },
template: `<task :task="task" @archiveTask="onArchiveTask" @pinTask="onPinTask"/>`,
props: {
task: {
default: () => ({
...taskData,
state: 'TASK_PINNED'
})
},
},
methods: actionsData,
});
// Task组件为其他状态的story
...
上述 story 样例里包含了几个关键点:
- 通过指定不同 props,我们可以创建不同状态下的组件,以测试组件 UI 是否符合预期
- 我们只关心组件的行为是否能正确发生(如是否正确传参)而不关心后续的处理,因此我们可以将事件处理 mock 掉
- 将 mock 的 action、测试数据等 export 出去可以便于我们在其他 story 中复用
- 每一个 story 都是该组件的一个可视化测试,启动 storybook 后就可以看到对应的测试:
运行 story 所需的配置:
// .storybook/main.js
module.exports = {
stories: ["../src/components/**/*.stories.js"],
addons: ["@storybook/addon-actions", "@storybook/addon-links"]
};
如果希望 storybook 加载 app 引用的样式文件,还需要配置 preview.js
:
// .storybook/preview.js
import "../src/index.css";
Vuex 对组件 Story 的影响
当 App 里状态较多时我们会考虑使用 Vuex 管理组件间的数据流,但使用 Vuex 之后组件对外部数据的依赖性增加了,不便于隔离测试, 因此较好的实践是将引入的 Vuex 状态管理抽离成一个容器组件,例如:
-
原组件:
PureTask.vue
<template> <div :class="taskClass"> <input type="checkbox" :checked="isChecked" @click="$emit('archiveTask', task.id)" /> <input type="text" :readonly="true" :value="this.task.title" /> <a @click="$emit('pinTask', task.id)"><span class="icon-star"/></a> </div> </template> <script> export default { name: "pure-task", props: { task: {...} }, ... }; </script>
-
容器组件:
Task.vue
<template> <pure-task :task="task" @archiveTask="archiveTask" @pinTask="pinTask" /> </template> <script> import PureTask from "./PureTask.vue"; import { mapState, mapActions } from "vuex"; export default { name: "task", components: { PureTask }, methods: { ...mapActions(["archiveTask", "pinTask"]) }, computed: { ...mapState(["task"]) } }; </script>
这样 PureTask 组件就是一个可隔离的独立组件,我们可以对它写 story 测试而不用额外考虑数据上下文。
此外,当我们用 Task 组件构建外层的 TaskList 组件时,由于自组件 Task 依赖 Vuex 提供的状态 store,父组件 TaskList 不再是一个可隔离的独立组件,需要在它的 story 里提供 Vuex store 以为 Task 组件提供必需的数据上下文。
//src/components/TaskList.stories.js
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {...},
actions: {...},
});
export default {
title: 'TaskList',
excludeStories: /.*store$/,
};
export const Default = () => ({
components: { TaskList },
template: `<task-list/>`,
store,
});
Snapshot Testing
快照测试是指记录给定输入的组件的“已知合格”输出,然后在将来输出发生变化时标记该组件的做法。 这样每次测试时可以看到我们对组件的哪部分进行了修改,当单元测试不通过时便于快速定位问题。
但在使用快照测试时应确保组件呈现不变的数据,以使快照测试不会每次都失败,例如是否存在日期或随机生成的值。
Setup
安装依赖
npm i -D @storybook/addon-storyshots jest-vue-preprocessor
创建 storybook 测试
// tests/unit/storybook.spec.js
import initStoryshots from "@storybook/addon-storyshots";
initStoryshots();
在 Jest 配置文件 jest.config.js
中增加以下配置:
transformIgnorePatterns: ["/node_modules/(?!(@storybook/.*\\.vue$))"]
运行测试
# 直接运行测试
npm run test:unit
# 运行测试并更新 snapshot
npm run test:unit -- -u
Tips
如果项目中引入了外部组件库,例如 Element-UI,为了处理外部组件库的样式文件,需要把样式文件 mock 掉。可以使用 ES6 Proxy mock CSS Modules:
npm i -D identity-obj-proxy
并在 Jest 配置文件 jest.config.js
中增加以下配置:
"moduleNameMapper": {
"\\.(css|less)$": "identity-obj-proxy"
}
总结
Storybook 是一个 UI 开发工具库,它的核心思想是 CDD + TDD。使用 storybook 可以在隔离的开发环境中以交互方式展示独立的组件,方便对组件 UI 和行为进行测试;在团队开发中有助于保证组件 UI 的一致性,也可以避免大家重复造轮子。