Использование 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
):
Давайте посмотрим на интерфейс:
- слева — боковая панель: здесь вы найдете свои компоненты. Нажмите на
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 и посмотрите, что получилось из вашей истории:
Просмотрите различные истории, которые мы создали, и обратите внимание, как меняется кнопка.
Все в режиме реального времени.
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
кнопку в правом верхнем углу вашей истории, которая отображает некоторую информацию о ней:
аддон-действия
Может быть полезно регистрировать, когда действие происходит в нашем компоненте. Допустим, например, что мы изменяем наш компонент 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 будет печатать новый журнал:
Дополнительные ручки
Прямо сейчас нам нужно написать много разных историй для одного и того же компонента, потому что нам нужно обрабатывать каждую комбинацию реквизита. Что, если бы мы могли редактировать реквизит в реальном времени, в самом 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>
));
Теперь, когда вы перейдете в свою историю, в разделе дополнений вы увидите новую вкладку, называемую «Ручки», и вы сможете изменить параметры своего компонента, поэкспериментировав с ними:
А что еще круче, так это то, что addon-info
он синхронизируется с этим props!
Тестирование методом моментального снимка
Поскольку компоненты 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 пожалуется, что снимки неверны, и предоставит вам очень полезный отчет:
Вы можете просмотреть изменения и решить, сломали ли вы что-то или каждое изменение было намеренным. Если все выглядит нормально, вы можете обновить снимки, используя:
npm run test -- -u
Проведение моментальных тестов после разработки крупной функции может быть очень полезным для оценки проделанной работы и влияния внесенных изменений.