Второй пост из четырех частей о том, как перенести идею из каркаса в производственное развертывание с помощью ReactJS. В этом посте мы попробуем настроить библиотеку компонентов в нашем ранее настроенном Monorepo. Мы разобьем наши каркасы на компоненты и создадим библиотеку компонентов с помощью Storybook.
Первоначально это было размещено здесь.
Это второй пост в серии. Вы можете найти первый пост здесь
Где мы
Хорошо, так что до сих пор у нас есть
- Мы провели мозговой штурм над нашей блестящей идеей создать приложение для просмотра фильмов.
- Мы решили, какие функции необходимы в рамках MVP.
- Наша команда дизайнеров предоставила нам каркасы.
- Мы настроили наш проект как Monorepo.
- Мы настроили правила линтинга, средство форматирования кода и перехватчики коммитов.
Что мы собираемся делать сейчас
Итак, следующий шаг — разбить каркас на компоненты. Мы создадим библиотеку компонентов, которую можно будет использовать в различных проектах. Наконец, мы настроим сборник рассказов, чтобы продемонстрировать нашу библиотеку компонентов.
TL;DR
Это пост из 5 частей
- Часть первая: каркасы и настройка проекта
- Часть вторая: Настройка библиотеки компонентов
- Часть третья: создание приложения Movie с использованием библиотеки компонентов
- Часть четвертая: Хостинг приложения Movie и настройка CI/CD
Исходный код доступен здесь
Демо-версия библиотеки компонентов доступна здесь
Демо-приложение 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/"
После этого мы можем начать разбивать наши каркасы на компоненты. Я не буду вдаваться в подробности этого, так как есть гораздо лучшие посты, которые лучше объясняют процесс. Вы можете найти код, который мы дорабатываем до сих пор здесь
В следующей части мы настроим и создадим наше приложение для фильмов, продолжим Часть третья: Создание приложения для фильмов с использованием библиотеки компонентов.