Stroybook的基本使用

原文:Stroybook的基本使用

Storybook 作为一个 UI 开发工具,能帮助开发人员独立创建组件,并在隔离的开发环境中以交互方式展示组件。 Storybook 是在主应用程序之外运行的,因此用户可以独立开发 UI 组件,而不必担心应用程序特定的依赖关系和要求。

Storybook 的基本思想是遵循 CDD + TDD 原则进行组件开发。

CDD 是 Component Drive Development,即以组件为基础自底而上的开发流程,每个组件都可作为一个独立的 story 进行开发。

TDD 是 Test Drive Development,在开发过程中对每个组件进行 UI 及功能上的测试。

本文介绍在 Vue 框架下的 App 中使用 Storybook 的方法。


以下图所示的 Taskbox App 为例。

Screen Shot 2020-03-09 at 16.34.56.png

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 后就可以看到对应的测试:
Screen Shot 2020-03-09 at 17.17.35.png

运行 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 的一致性,也可以避免大家重复造轮子。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容