Использование Storybook с React

Оптимизируйте рабочий процесс разработки компонентов пользовательского интерфейса

Что такое Storybook и зачем мне его использовать?

Storybook — это инструмент с открытым исходным кодом для изолированной разработки компонентов пользовательского интерфейса.

Многоразовые компоненты

React позволяет нам писать то, что мы называем «переиспользуемыми компонентами». Если вы не знаете, что такое переиспользуемый компонент, подумайте, например, о кнопках:

  • возможны разные варианты:
  • кнопка primaryс красным фоном
  • кнопка secondaryс зеленым фоном
  • вы также можете иметь различные состояния: кнопка может быть, disabledесли форма в данный момент отправляется

В React очень простой способ справиться с этой проблемой — использовать один Buttonкомпонент, который принимает разные параметры:

  • опора, называемая disabled, которая является либо trueилиfalse
  • реквизит, называемый variant, который является либо , primaryлибо secondary.

Но предположим, что вы пишете этот код и хотите посмотреть, как он выглядит. Обычный подход — перейти на страницу в вашем приложении, затем включить кнопку где-то посередине, передать ей props и посмотреть, как она выглядит.

Comes Storybook

Вот где Storybook вступает в игру: он по сути позволяет вам запускать второе приложение параллельно, где вы можете играть с компонентом Button, без необходимости включать его в свое приложение. Вы можете разрабатывать свои компоненты изолированно.

Теперь, предположим, кто-то из службы поддержки общается с клиентом, который не может войти в систему. Они подходят к вам и спрашивают: «Эй, можете показать мне этот экран с ошибкой?».

Без Storybook ответ заключается в том, что нужно запустить приложение, попытаться воспроизвести действия пользователя, прочитать код, чтобы понять, как это должно выглядеть, и т. д.

С помощью Storybook вам просто нужно ввести «Экран ошибки» в строке поиска, и вы сразу же увидите его!

Настройка Storybook в приложении React

На этом этапе вам понадобится приложение React. Если у вас его нет, смело клонируйте его или следуйте инструкциям в этом посте , чтобы создать его. В этом руководстве предполагается, что вы используете create-react-app .

Storybook делает настройку очень простой. В терминале просто запустите:

npx -p @storybook/cli sb init

По сути, это позволит вам package.json проверить, какую платформу вы используете, а затем сгенерировать правильную конфигурацию для вашего проекта.

Команда должна была обновить ваши package.json скрипты, добавив:

"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"

Нас интересует первый из них. Запускаем:

npm run storybook

В вашем браузере должно открыться что-то вроде этого (если этого не произошло, просто перейдите по ссылке localhost:9009):

storybook-home.png

Давайте посмотрим на интерфейс:

  • слева — боковая панель: здесь вы найдете свои компоненты. Нажмите на Button, и посмотрите, что там!
  • внизу что-то похожее на консоль: на самом деле это раздел "addons". Storybook имеет множество addons, которые позволяют вам улучшить ваш опыт при разработке ваших компонентов: динамически изменять props, регистрировать выходные данные, переключать языки и т. д.

Так откуда же берутся эти компоненты? Когда мы установили Storybook, он сгенерировал эти "демо" истории. Они находятся в src/stories/index.js:

import React from "react";

import { storiesOf } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import { linkTo } from "@storybook/addon-links";
// Importing the demo components from storybook itself
import { Button, Welcome } from "@storybook/react/demo";

storiesOf("Welcome", module).add("to Storybook", () => (
  <Welcome showApp={linkTo("Button")} />
));

storiesOf("Button", module)
  .add("with text", () => (
    <Button onClick={action("clicked")}>Hello Button</Button>
  ))
  .add("with some emoji", () => (
    <Button onClick={action("clicked")}>
      <span role="img" aria-label="so cool">
        😀 😎 👍 💯
      </span>
    </Button>
  ));

Магия, которая добавляет их в Storybook, заключается в .storybook/config.js:

import { configure } from '@storybook/react';

function loadStories() {
  require('../src/stories');
}

configure(loadStories, module);

Напишите свои первые истории

Config Storybook

Первое, что нам нужно сделать, это избавиться от этих демо-историй и изменить способ включения историй в Storybook. Удалите src/stories/ папку вообще, она нам не понадобится.

Заменить все на .storybook/config.js:

import { configure } from '@storybook/react';

const req = require.context('../src/', true, /\.stories\.js$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

Это заставит Storybook выбрать каждый файл, заканчивающийся на .stories.js. Вы увидите, что (в целом) гораздо проще хранить истории рядом с компонентами, которые они тестируют.

Простой компонент Button

Теперь давайте напишем нашу первую историю. Если вы используете мой пример github, перейдите src/components/atomsи создайте следующие файлы:

|––atoms
  |––Button
    |––index.js
    |––Button.js
    |––Button.stories.js

Button.js:

import React from "react";

const Button = props => {
  const { variant, disabled, children } = props;

  // This is the default style
  let backgroundColor = "white";
  let color = "black";

  // Which variant do we want?
  switch (variant) {
    case "primary":
      backgroundColor = "red";
      color = "white";
      break;
    case "secondary":
      backgroundColor = "green";
      color = "white";
      break;
    default:
      break;
  }

  // Let's build the style based on the variant
  // We also add properties depending on the `disabled` state
  const style = {
    backgroundColor,
    color,
    cursor: disabled ? "not-allowed" : "pointer",
    opacity: disabled ? 0.5 : 1
  };

  return (
    <button disabled={disabled} style={style}>
      {children}
    </button>
  );
};

export default Button;

Button.stories.js:

import React from "react";
import { storiesOf } from "@storybook/react";
import Button from "./Button";

// You can see this as "folders" in Storybook's sidebar
const stories = storiesOf("atoms/Button", module);

// Every story represents a state for our Button component
stories.add("default", () => <Button>Button</Button>);
stories.add("default disabled", () => <Button disabled>Button</Button>);
stories.add("primary", () => <Button variant="primary">Button</Button>);
// Passing a prop without a value is basically the same as passing `true`
stories.add("primary disabled", () => (
  <Button variant="primary" disabled>
    Button
  </Button>
));
stories.add("secondary", () => <Button variant="secondary">Button</Button>);
stories.add("secondary disabled", () => (
  <Button variant="secondary" disabled>
    Button
  </Button>
));

index.js:

// This allows us to import `src/components/Button` directly,
// without having to go all the way to `src/components/Button/Button`
export { default } from "./Button";

Теперь снова зайдите в Storybook и посмотрите, что получилось из вашей истории:

книга рассказов-button.png

Просмотрите различные истории, которые мы создали, и обратите внимание, как меняется кнопка.

Все в режиме реального времени.

Storybook имеет очень быстрый механизм горячей перезагрузки. Это означает, что вы можете перейти к своему компоненту, изменить "красный" на синий, и Storybook мгновенно перекомпилирует ваши истории, чтобы включить ваши изменения!

Дополнения

Storybook предоставляет различные очень удобные дополнения, которые помогут нам разрабатывать компоненты изолированно с уверенностью. Давайте настроим некоторые из них.

информация о дополнении

Иногда, когда вы просматриваете Storybook, вы хотите прочитать код для определенной истории. Это именно то, что infoделает дополнение. Чтобы установить его:

npm i -D @storybook/addon-info

Добавьте плагин глобально, отредактировав .storybook/config.js:

...
import { addDecorator } from '@storybook/react';
import { withInfo } from '@storybook/addon-info';

addDecorator(withInfo);
...

Это добавит show info кнопку в правом верхнем углу вашей истории, которая отображает некоторую информацию о ней:

addon-info.png

аддон-действия

Может быть полезно регистрировать, когда действие происходит в нашем компоненте. Допустим, например, что мы изменяем наш компонент Button так, чтобы он принимал prop onClick:

...
const Button = props => {
  const { variant, disabled, children, onClick } = props;
  ...
return (
    <button onClick={onClick} disabled={disabled} style={style}>
      {children}
    </button>
  );

Как нам проверить, что нажатие кнопки вызовет onClick обработчик? Storybook предоставляет официальное дополнение, уже установленное, которое может помочь с этим. В вашей истории импортируйте action, затем добавьте onClick prop:

import { action } from "@storybook/addon-actions";
...
stories.add("default", () => (
  <Button onClick={action("clicked!")}>Button</Button>
));
stories.add("default disabled", () => (
  <Button onClick={action("clicked!")} disabled>
    Button
  </Button>
));
stories.add("primary", () => (
  <Button onClick={action("clicked!")} variant="primary">
    Button
  </Button>
));
stories.add("primary disabled", () => (
  <Button onClick={action("clicked!")} variant="primary" disabled>
    Button
  </Button>
));
stories.add("secondary", () => (
  <Button onClick={action("clicked!")} variant="secondary">
    Button
  </Button>
));
stories.add("secondary disabled", () => (
  <Button onClick={action("clicked!")} variant="secondary" disabled>
    Button
  </Button>
));

Теперь при каждом нажатии кнопки Storybook будет печатать новый журнал:

щелкнул.png

Дополнительные ручки

Прямо сейчас нам нужно написать много разных историй для одного и того же компонента, потому что нам нужно обрабатывать каждую комбинацию реквизита. Что, если бы мы могли редактировать реквизит в реальном времени, в самом Storybook? Решение — addon-knobs , и оно значительно упрощает способ написания историй.

Сначала установите дополнение с помощью:

npm i -D @storybook/addon-knobs

Затем добавьте это к .storybook/addons.js:

import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
// add this line
import '@storybook/addon-knobs/register';

И перепишите свою историю, чтобы использовать новый плагин:

import React from "react";
import { storiesOf } from "@storybook/react";
import Button from "./Button";
import { action } from "@storybook/addon-actions";
// add this line
import { withKnobs, select, boolean } from "@storybook/addon-knobs";

// You can see this as "folders" in Storybook's sidebar
const stories = storiesOf("atoms/Button", module);

// add this line
stories.addDecorator(withKnobs);

// ---- add this block
const variantOptions = {
  none: "",
  primary: "primary",
  secondary: "secondary"
};
// ----

stories.add("with knobs", () => (
  <Button
    onClick={action("clicked!")}
    // ---- and this one
    // syntax is (name, options, default)
    variant={select("variant", variantOptions, "")}
    // syntax is (name, default)
    disabled={boolean("disabled", false)}
    // ----
  >
    Button
  </Button>
));

Теперь, когда вы перейдете в свою историю, в разделе дополнений вы увидите новую вкладку, называемую «Ручки», и вы сможете изменить параметры своего компонента, поэкспериментировав с ними:

ручки.png

А что еще круче, так это то, что addon-infoон синхронизируется с этим props!

ручки-sync.png

Тестирование методом моментального снимка

Поскольку компоненты React можно использовать повторно, очень часто компонент включается во многие другие компоненты. Отслеживание всех мест, где компонент становится зависимостью, и оценка влияния небольшого изменения может стать очень сложной задачей. Storybook позволяет очень легко настроить тесты моментальных снимков в сочетании с jest (create-react-app уже идет с ним).

Сначала установите необходимые зависимости:

npm i -D @storybook/addon-storyshots react-test-renderer require-context.macro

Затем, в .storybook/config.js:

import requireContext from 'require-context.macro';

// const req = require.context('../src', true, /\.stories\.js$/); <-- replaced
const req = requireContext('../src', true, /\.stories\.js$/);

Создайте следующую структуру в src:

|––test
  |––storyshots.test.js

И добавьте это кstoryshots.test.js

import initStoryshots from '@storybook/addon-storyshots';

initStoryshots();

Наконец, запустите npm run test(или npm testсокращенную запись). Это создаст файл снимка в src/test/__snapshots__/storyshots.test.js.snap.

Теперь, когда вы запустите тесты, Storybook отобразит каждую историю и сравнит ее с ранее созданными снимками. Попробуйте изменить что-нибудь в компоненте Button и снова запустите тесты, например:

switch (variant) {
    case "primary":
      backgroundColor = "red";
      color = "white";
      break;
    case "secondary":
      // change this...
      //backgroundColor = "green";
      // ...into this
      backgroundColor = "gray";
      color = "white";
      break;
    default:
      break;
  }

Jest пожалуется, что снимки неверны, и предоставит вам очень полезный отчет:

снимок-неудачный.png

Вы можете просмотреть изменения и решить, сломали ли вы что-то или каждое изменение было намеренным. Если все выглядит нормально, вы можете обновить снимки, используя:

npm run test -- -u

Проведение моментальных тестов после разработки крупной функции может быть очень полезным для оценки проделанной работы и влияния внесенных изменений.