Второй пост из четырех частей о том, как перенести идею из каркаса в производственное развертывание с помощью ReactJS. В этом посте мы попробуем настроить библиотеку компонентов в нашем ранее настроенном Monorepo. Мы разобьем наши каркасы на компоненты и создадим библиотеку компонентов с помощью Storybook.

Первоначально это было размещено здесь.

Это второй пост в серии. Вы можете найти первый пост здесь

Где мы

Хорошо, так что до сих пор у нас есть

  • Мы провели мозговой штурм над нашей блестящей идеей создать приложение для просмотра фильмов.
  • Мы решили, какие функции необходимы в рамках MVP.
  • Наша команда дизайнеров предоставила нам каркасы.
  • Мы настроили наш проект как Monorepo.
  • Мы настроили правила линтинга, средство форматирования кода и перехватчики коммитов.

Что мы собираемся делать сейчас

Итак, следующий шаг — разбить каркас на компоненты. Мы создадим библиотеку компонентов, которую можно будет использовать в различных проектах. Наконец, мы настроим сборник рассказов, чтобы продемонстрировать нашу библиотеку компонентов.

TL;DR

Это пост из 5 частей

Исходный код доступен здесь

Демо-версия библиотеки компонентов доступна здесь

Демо-приложение Movie доступно здесь

Настройка библиотеки компонентов

Теперь давайте перейдем к настройке нашей библиотеки компонентов.

Переместить в папку packages

cd packages

Создайте новую папку для нашего components

mkdir components
cd components

Инициализировать проект пряжи

yarn init

Именование здесь важно, так как мы будем ссылаться на наши проекты в нашем рабочем пространстве, используя имя. Я предпочитаю имя в масштабе организации, чтобы избежать конфликтов имен. Так что для нашего примера я буду использовать @awesome-movie-app в качестве названия нашей организации. Не стесняйтесь заменять областью вашей организации.

Следующее, что нужно иметь в виду, это то, как вы хотите публиковать свои пакеты в npm. Если вы хотите опубликовать пакеты в npm, убедитесь, что версия является семантической, и позвольте lerna обрабатывать публикацию в пакетах.

Если у вас есть ограниченная/частная организация NPM, обязательно добавьте publishConfig с доступом restricted в package.json, чтобы избежать случайной публикации пакетов в общедоступном npm.

"publishConfig": {
    "access": "restricted"
}

Что касается цели этого поста, мы не будем публиковать наши пакеты в npm, поэтому мы пропустим определение файла publishConfig.

Так выглядит наш package.json

{
  "name": "@awesome-movie-app/components",
  "version": "1.0.0",
  "description": "Component Library for Awesome Movie App",
  "main": "index.js",
  "repository": "[email protected]:debojitroy/movie-app.git",
  "author": "Debojit Roy <[email protected]>",
  "license": "MIT",
  "private": true
}

Определение требований

Наш проект настроен, давайте определим наши требования, прежде чем двигаться дальше.

  • Наши компоненты будут React компонентов
  • Мы будем использовать TypeScript для сборки наших компонентов.
  • Мы хотим продемонстрировать наши компоненты, используя Storybook
  • Мы будем использовать Bootstrap для базовых стилей.
  • Мы внедрим CSS-in-JS и будем использовать StyledComponents
  • Мы транспилируем наш код, используя Babel

Почему нет вебпака

В идеальном мире мы будем публиковать наши пакеты в npm. Прежде чем публиковать наши пакеты в npm, мы хотели бы красиво их транспилировать и упаковать. Для этого моим идеальным выбором будет webpack.

Но одна очень важная особенность для библиотек — пакет должен поддерживать Tree Shaking. Tree Shaking – это красивое слово, обозначающее удаление лишнего жира, то есть удаление кода, который не используется в библиотеке импорта. Из-за этой известной проблемы веб-пакета, к сожалению, сейчас это невозможно.

Чтобы обойти эту проблему, мы можем использовать Rollup, но поскольку сейчас мы не заинтересованы в публикации нашего пакета на npm, мы будем использовать babel для переноса наших компонентов. В другом посте я расскажу, как использовать Rollup и Tree Shake для вашей библиотеки.

Подготовка проекта

Хорошо, это было слишком много теории, теперь давайте перейдем к настройке нашего проекта.

Последняя часть теории, прежде чем мы двинемся дальше. Поскольку мы используем lerna в качестве высокоуровневого менеджера зависимостей, мы будем использовать lerna для управления зависимостями. Это означает, что для добавления новой зависимости мы будем использовать этот формат

lerna add <dependency-name> --scope=<sub-project-name> <--dev>

имя-зависимости: имя пакета npm, который мы хотим установить имя-подпроекта: это необязательно. Если вы опустите это, то зависимость будет установлена ​​во всех проектах. Если вы хотите, чтобы зависимость была установлена ​​только для определенного проекта, то передайте имя проекта из отдельных package.json —dev: то же, что и параметры пряжи. Если вы хотите установить только зависимости от разработчиков, передайте этот флаг.

Добавление зависимостей проекта

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

Примечание. Мы будем добавлять все из корневой папки проекта, то есть из корневой папки movie-app (на один уровень выше папки packages).

Добавление реакции

lerna add react --scope=@awesome-movie-app/components --dev
lerna add react-dom --scope=@awesome-movie-app/components --dev

Почему одна зависимость за раз

Печально из-за этого ограничения lerna 😞

Почему React зависит от разработчиков 🤔

Эта часть важна. Поскольку эта библиотека будет использоваться в другом проекте, мы не хотим диктовать нашу версию React, а хотим, чтобы проект-потребитель внедрил зависимость. Итак, мы собираемся добавить общие библиотеки в качестве dev зависимостей и пометить их как одноранговые зависимости. Это верно для любых общих библиотек, которые вы, возможно, захотите создать.

Мы добавим React в наши одноранговые зависимости @awesome-movie-app/components.

"peerDependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }

Добавление TypeScript

lerna add typescript --scope=@awesome-movie-app/components --dev

Добавление типов для React

lerna add @types/node --scope=@awesome-movie-app/components
lerna add @types/react --scope=@awesome-movie-app/components
lerna add @types/react-dom --scope=@awesome-movie-app/components

Добавление tsconfig для TypeScript

{
  "compilerOptions": {
    "outDir": "lib",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDirs": ["src"],
    "baseUrl": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "scripts"]
}

Добавление сборника рассказов

lerna add @storybook/react --scope=@awesome-movie-app/components --dev

Добавление некоторых интересных дополнений

lerna add @storybook/addon-a11y --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-actions --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-docs --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-knobs --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-viewport --scope=@awesome-movie-app/components --dev
lerna add storybook-addon-styled-component-theme --scope=@awesome-movie-app/components --dev
lerna add @storybook/addon-jest --scope=@awesome-movie-app/components --dev

Добавление тестовых библиотек

Мы будем использовать jest для модульного тестирования.

lerna add jest --scope=@awesome-movie-app/components --dev
lerna add ts-jest --scope=@awesome-movie-app/components --dev

Мы будем использовать энзим для тестирования наших компонентов React.

lerna add enzyme --scope=@awesome-movie-app/components --dev
lerna add enzyme-adapter-react-16 --scope=@awesome-movie-app/components --dev
lerna add enzyme-to-json --scope=@awesome-movie-app/components --dev

Добавление шуточных компонентов для наддува jest

lerna add jest-styled-components --scope=@awesome-movie-app/components --dev

Настройте enzyme и jest-styled-components для работы с jest. Мы добавим setupTests.js

require("jest-styled-components")
const configure = require("enzyme").configure
const EnzymeAdapter = require("enzyme-adapter-react-16")
const noop = () => {}
Object.defineProperty(window, "scrollTo", { value: noop, writable: true })
configure({ adapter: new EnzymeAdapter() })

Настроить jest.config.js

module.exports = {
  preset: "ts-jest",
  // Automatically clear mock calls and instances between every test
  clearMocks: true,
  // Indicates whether the coverage information should be collected while executing the test
  collectCoverage: true,
  // An array of glob patterns indicating a set of files for which coverage information should be collected
  collectCoverageFrom: [
    "src/**/*.{ts,tsx}",
    "!src/**/index.{ts,tsx}",
    "!src/**/styled.{ts,tsx}",
    "!src/**/*.stories.{ts,tsx}",
    "!node_modules/",
    "!.storybook",
    "!dist/",
    "!lib/",
  ],
  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",
  // An array of regexp pattern strings used to skip test files
  testPathIgnorePatterns: ["/node_modules/", "/lib/", "/dist/"],
  // A list of reporter names that Jest uses when writing coverage reports
  coverageReporters: ["text", "html", "json"],
  // An array of file extensions your modules use
  moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
  // A list of paths to modules that run some code to configure or set up the testing framework before each test
  setupFilesAfterEnv: ["./setupTests.js"],
  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
  snapshotSerializers: ["enzyme-to-json/serializer"],
}

Добавление стилизованных компонентов и BootStrap

lerna add styled-components --scope=@awesome-movie-app/components --dev
lerna add react-bootstrap --scope=@awesome-movie-app/components --dev
lerna add bootstrap --scope=@awesome-movie-app/components --dev
lerna add @types/styled-components --scope=@awesome-movie-app/components

Добавление Вавилона

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

Добавление зависимостей Babel

lerna add @babel/core --scope=@awesome-movie-app/components --dev
lerna add babel-loader --scope=@awesome-movie-app/components --dev
lerna add @babel/cli --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-env --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-react --scope=@awesome-movie-app/components --dev
lerna add @babel/preset-typescript --scope=@awesome-movie-app/components --dev
lerna add core-js --scope=@awesome-movie-app/components --dev

Немного о babel компонентах, которые мы добавили

  • @babel/core: основные функции babel.
  • babel-loader: используется storybook webpack Builder
  • @babel/cli: будет использоваться нами для переноса файлов из командной строки.
  • @babel/preset-env : настройка среды для транспиляции.
  • @babel/preset-react: настройка React для babel
  • @babel/preset-typescript : настройки TypeScript для babel
  • core-js : Core JS для preset-env

Теперь добавим наш файл .babelrc

{
  "presets": [
    "@babel/preset-typescript",
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3",
        "modules": false
      }
    ],
    "@babel/preset-react"
  ]
}

Объединяя все это

Важная заметка

Приведенные ниже шаги могут различаться в зависимости от того, какую версию Storybook и Jest вы используете. Следующие шаги написаны для Storybook v5.3+ и Jest v26.0+

Настройка нашей темы

Первым шагом будет настройка нашего theme. Мы можем начать с пустого theme и заполнять его по мере продвижения.

cd packages/components
mkdir theme

Определение Theme

export interface Theme {
  name: string
  color: {
    backgroundColor: string
    primary: string
    secondary: string
  }
}

Определение темы Light

import { Theme } from "./theme"
const lightTheme: Theme = {
  name: "LIGHT",
  color: {
    backgroundColor: "#fff",
    primary: "#007bff",
    secondary: "#6c757d",
  },
}
export default lightTheme

Определение темы Dark

import { Theme } from "./theme"
const darkTheme: Theme = {
  name: "DARK",
  color: {
    backgroundColor: "#000",
    primary: "#fff",
    secondary: "#6c757d",
  },
}
export default darkTheme

Настройка сборника рассказов

Чтобы настроить storybook, нам нужно сначала настроить папку конфигурации. Мы будем использовать папку по умолчанию .storybook, но не стесняйтесь использовать имя папки.

mkdir .storybook

Теперь внутри папки .storybook мы создадим файлы конфигурации, необходимые для storybook.

main.js

Это файл конфигурации main для сборника рассказов. Мы настроим путь для stories, зарегистрируем наш addons и переопределим конфигурацию webpack для обработки typescript files.

// .storybook/main.js
module.exports = {
  stories: ["../src/**/*.stories.[tj]sx"],
  webpackFinal: async config => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      use: [
        {
          loader: require.resolve("ts-loader"),
        },
      ],
    })
    config.resolve.extensions.push(".ts", ".tsx")
    return config
  },
  addons: [
    "@storybook/addon-docs",
    "@storybook/addon-actions/register",
    "@storybook/addon-viewport/register",
    "@storybook/addon-a11y/register",
    "@storybook/addon-knobs/register",
    "storybook-addon-styled-component-theme/dist/register",
    "@storybook/addon-jest/register",
  ],
}

менеджер.js

Здесь мы настраиваем менеджер Storybook. Есть много параметров, которые можно переопределить, для нашего проекта мы хотим, чтобы панель надстроек находилась на bottom (по умолчанию right).

// .storybook/manager.js
import { addons } from "@storybook/addons"
addons.setConfig({
  panelPosition: "bottom",
})

предварительный просмотр.js

Наконец, мы настроим область Story. Мы инициализируем наши надстройки и передаем глобальные конфигурации.

// .storybook/preview.js
import { addParameters, addDecorator } from "@storybook/react"
import { withKnobs } from "@storybook/addon-knobs"
import { withA11y } from "@storybook/addon-a11y"
import { withThemesProvider } from "storybook-addon-styled-component-theme"
import { withTests } from "@storybook/addon-jest"
import results from "../.jest-test-results.json"
import lightTheme from "../theme/light"
import darkTheme from "../theme/dark"
export const getAllThemes = () => {
  return [lightTheme, darkTheme]
}
addDecorator(withThemesProvider(getAllThemes()))
addDecorator(withA11y)
addDecorator(withKnobs)
addDecorator(
  withTests({
    results,
  })
)
addParameters({
  options: {
    brandTitle: "Awesome Movie App",
    brandUrl: "https://github.com/debojitroy/movie-app",
    showRoots: true,
  },
})

Создание React-компонентов

Теперь мы можем создать наш самый первый реагирующий компонент.

Наша первая кнопка

Сначала мы создадим папку src

mkdir src && cd src

Затем мы создадим папку для нашего компонента. Назовем его Sample

mkdir Sample && cd Sample

Теперь давайте создадим простой styled button и передадим ему некоторые реквизиты.

// styled.ts
import styled from "styled-components"
export const SampleButton = styled.button`
  background-color: ${props => props.theme.color.backgroundColor};
  color: ${props => props.theme.color.primary};
`
// Button.tsx
import React from "react"
import { SampleButton } from "./styled"
const Button: React.FC<{
  value: string
  onClickHandler: () => void
}> = ({ value, onClickHandler }) => (
  <SampleButton onClick={onClickHandler}>{value}</SampleButton>
)
export default Button

Потрясающий !!! Наконец-то у нас есть первый компонент!!!

Добавление модульных тестов

Теперь давайте добавим несколько тестов для нашей новой кнопки.

mkdir tests
// tests/Button.test.tsx
import React from "react"
import { mount } from "enzyme"
import { ThemeProvider } from "styled-components"
import lightTheme from "../../../theme/light"
import Button from "../Button"
const clickFn = jest.fn()
describe("Button", () => {
  it("should simulate click", () => {
    const component = mount(
      <ThemeProvider theme={lightTheme}>
        <Button onClickHandler={clickFn} value="Hello" />
      </ThemeProvider>
    )
    component.find(Button).simulate("click")
    expect(clickFn).toHaveBeenCalled()
  })
})

Добавление историй

Теперь, когда у нас есть новая кнопка, давайте добавим немного stories

mkdir stories

Мы будем использовать новый Формат Component Story Format (CSF)

// stories/Button.stories.tsx
import React from "react"
import { action } from "@storybook/addon-actions"
import { text } from "@storybook/addon-knobs"
import Button from "../Button"
export default {
  title: "Sample / Button",
  component: Button,
}
export const withText = () => (
  <Button
    value={text("value", "Click Me")}
    onClickHandler={action("button-click")}
  />
)
withText.story = {
  parameters: {
    jest: ["Button.test.tsx"],
  },
}

Время проверить, все ли работает

Транспилируем наш код

Как мы обсуждали в начале, мы будем использовать babel для транспиляции нашего кода и позволим вызывающим проектам позаботиться о минификации и встряхивании дерева.

Итак, продолжая, мы добавим несколько скриптов и проверим их работу.

Проверка типов и компиляция

Сначала мы воспользуемся компиляцией TypeScript для компиляции нашего кода.

"js:build": "cross-env NODE_ENV=production tsc -p tsconfig.json"

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

$ cross-env NODE_ENV=production tsc -p tsconfig.json
✨  Done in 5.75s.

Транспиляция с помощью Babel

Следующим шагом будет транспиляция нашего кода с помощью babel

"build-js:prod": "rimraf ./lib && yarn js:build && cross-env NODE_ENV=production babel src --out-dir lib --copy-files --source-maps --extensions \".ts,.tsx,.js,.jsx,.mjs\""

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

$ rimraf ./lib && yarn js:build && cross-env NODE_ENV=production babel src --out-dir lib --copy-files --source-maps --extensions ".ts,.tsx,.js,.jsx,.mjs"
$ cross-env NODE_ENV=production tsc -p tsconfig.json
Successfully compiled 4 files with Babel.
✨  Done in 7.02s.

Настройка режима просмотра для разработки

Во время разработки мы хотели бы, чтобы инкрементная компиляция выполнялась каждый раз, когда мы вносим изменения. Итак, давайте добавим скрипт просмотра.

"js:watch": "rimraf ./lib && cross-env NODE_ENV=development concurrently -k -n \"typescript,babel\" -c \"blue.bold,yellow.bold\"  \"tsc -p tsconfig.json --watch\" \"babel src --out-dir lib --source-maps --extensions \".ts,.tsx,.js,.jsx,.mjs\" --copy-files --watch --verbose\""

Мы должны увидеть такой вывод

Starting compilation in watch mode...
[typescript]
[babel] src/Sample/Button.tsx -> lib/Sample/Button.js
[babel] src/Sample/stories/Button.stories.tsx -> lib/Sample/stories/Button.stories.js
[babel] src/Sample/styled.ts -> lib/Sample/styled.js
[babel] src/Sample/tests/Button.test.tsx -> lib/Sample/tests/Button.test.js
[babel] Successfully compiled 4 files with Babel.
[typescript]
[typescript] - Found 0 errors. Watching for file changes.

Запуск модульных тестов

Как только мы убедимся, что наша компиляция и транспиляция работают, давайте убедимся, что наши тесты работают.

"test": "jest"

Запуск наших тестов должен показать вывод, подобный этому

Потихоньку идём 😊

Теперь нам нужно сгенерировать вывод json для сборника рассказов, который будет потребляться и отображаться рядом с нашими историями. Давайте настроим и это.

"test:generate-output": "jest --json --outputFile=.jest-test-results.json || true"

Сборник рассказов

Наконец, мы хотим запустить сборник рассказов с нашими историями. Давайте запустим сборник рассказов в режиме разработки.

"storybook": "start-storybook -p 8080"

Если все было настроено правильно, мы должны увидеть сборник рассказов в нашем Браузере.

Мы добавим еще пару команд для создания сборника рассказов для развертывания. Мы будем использовать их при настройке непрерывного развертывания в нашем последнем посте — Часть четвертая: размещение приложения Movie и настройка CI/CD.

"prebuild:storybook": "rimraf .jest-test-results.json && yarn test:generate-output",
"build:storybook": "build-storybook -c .storybook -o dist/"

После этого мы можем начать разбивать наши каркасы на компоненты. Я не буду вдаваться в подробности этого, так как есть гораздо лучшие посты, которые лучше объясняют процесс. Вы можете найти код, который мы дорабатываем до сих пор здесь

В следующей части мы настроим и создадим наше приложение для фильмов, продолжим Часть третья: Создание приложения для фильмов с использованием библиотеки компонентов.