Хабрахабр

[Перевод] Рассказ о том, как команда фрилансеров пишет фулстек-приложения на JavaScript

Автор материала, перевод которого мы сегодня публикуем, говорит, что GitHub-репозиторий, над которым работал он и ещё несколько фрилансеров, получил, по разным причинам, около 8200 звёзд за 3 дня. Этот репозиторий попал на первое место в HackerNews и в GitHub Trending, за него отдали голоса 20000 пользователей Reddit.

В данном репозитории отражена методика разработки фулстек-приложений, которой посвящена эта статья.

Предыстория

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

№ 1 в GitHub Trending

В наших проектах используется React/React Native, NodeJS и GraphQL. Я работаю в команде фрилансеров. Кроме того, он будет полезен тем, кто в будущем присоединится к нашей команде. Этот материал предназначен для тех, кто хочет узнать о том, как мы разрабатываем приложения.

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

Чем проще — тем лучше

«Чем проще — тем лучше», — это легче сказать, чем сделать. Большинство разработчиков отдают себе отчёт в том, что простота — это важный принцип разработки ПО. Но этому принципу не всегда легко следовать. Если код устроен просто — это облегчает поддержку проекта и упрощает командную работу над этим проектом. Кроме того, соблюдение этого принципа помогает в работе с кодом, который был написан, скажем, полгода назад.

Вот какие ошибки, касающиеся рассматриваемого принципа, мне приходилось встречать:

  • Неоправданное стремление к выполнению принципа DRY. Иногда копирование и вставка кода — это вполне нормально. Не нужно абстрагировать каждые 2 фрагмента кода, которые чем-то похожи друг на друга. Я и сам совершал эту ошибку. Все, пожалуй, её совершали. DRY — это хороший подход к программированию, но выбор неудачной абстракции способен лишь ухудшить ситуацию и усложнить кодовую базу. Если вы хотите узнать подробности об этих идеях — рекомендую почитать материал «AHA Programming» Кента Доддса.
  • Отказ от использования имеющихся инструментов. Один из примеров этой ошибки — использование reduce вместо map или filter. Конечно, с помощью reduce можно воспроизвести поведение map. Но это, вероятно, приведёт к росту размера кода, и к тому, что другим людям будет сложнее понять этот код, учитывая то, что «простота кода» — понятие субъективное. Иногда может понадобиться использовать именно reduce. А если сравнить скорость обработки набора данных с использованием объединённых в цепочку вызовов map и filter, и с использованием reduce, то окажется, что второй вариант работает быстрее. В варианте с reduce набор значений приходится просматривать один раз, а не два. Перед нами — спор производительности и простоты. Я, в большинстве случаев, отдал бы предпочтение простоте и стремился бы к тому, чтобы избежать преждевременной оптимизации кода, то есть, выбрал бы пару map/filter вместо reduce. А если бы оказалось так, что конструкция из map и filter стала узким местом системы, перевёл бы код на reduce.

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

Держите схожие сущности рядом друг с другом

Этот принцип, «принцип колокации», применим ко многим частям приложения. Это и структура папок, в которых хранится код клиента и сервера, это и хранение кода проекта в одном репозитории, это и принятие решений о том, какой именно код оказывается в некоем файле.

▍Репозиторий

Рекомендуется держать код клиента и сервера в одном и том же репозитории. Это просто. Не стоит усложнять то, что усложнять не нужно. При таком подходе удобно организовать согласованную командную работу над проектом. Я работал над проектами, для хранения материалов которых использовались различные репозитории. Это — не катастрофа, но монорепозитории делают жизнь гораздо легче.

▍Структура проекта клиентской части приложения

Мы пишем фулстек-приложения. То есть — и код клиента, и код сервера. В структуре папки типичного клиентского проекта предусмотрены отдельные директории для компонентов, контейнеров, действий, редьюсеров и маршрутов.

Я стремлюсь к тому, чтобы обходиться без этой библиотеки. Действия и редьюсеры присутствуют в тех проектах, в которых используется Redux. В некоторых из моих проектов имеются отдельные папки для компонентов и контейнеров. Я уверен в том, что существуют качественные проекты, в которых используется такая же структура. В папке контейнеров имеются файлы, хранящие код контейнеров BlogPostContainer и ProfileContainer. В папке компонентов может храниться нечто вроде файлов с кодом таких сущностей, как BlogPost и Profile. Контейнер получает данные с сервера и передаёт их «глупому» дочернему компоненту, задача которого заключается в том, чтобы вывести эти данные на экран.

Она, по крайней мере, однородна, а это очень важно. Это — рабочая структура. Минус этого подхода, из-за которого я в последнее время стараюсь им не пользоваться, заключается в том, что он заставляет программиста постоянно перемещаться по кодовой базе. Это ведёт к тому, что разработчик, присоединившийся к работе над проектом, быстро поймёт то, что в нём происходит, и то, какую роль играют его отдельные части. Например, у сущностей ProfileContainer и BlogPostContainer нет ничего общего, но их файлы находятся рядом друг с другом и при этом далеко от тех файлов, в которых они по-настоящему используются.

Такой подход структурирования проекта основан на группировке файлов на основании реализуемых ими возможностей. Я с некоторых пор стремлюсь к тому, чтобы размещать файлы, содержимое которых планируется использовать совместно, в одних и тех же папках. Благодаря этому подходу можно значительно облегчить себе жизнь, если, например, поместить в одну папку родительский компонент и его «глупый» дочерний компонент.

В папке для компонентов обычно хранится код таких элементов, как Button или Input. Обычно мы используем папки routes / screens и папку components. Каждая папка, находящаяся в папке для маршрутов, представляет собой отдельную страницу приложения. Этот код может быть использован на любой странице приложения. А код компонентов, которые используются на нескольких страницах, попадает в папку components. При этом файлы с кодом компонентов и с кодом логики приложения, относящиеся к данному маршруту, находятся внутри той же самой папки.

Это имеет смысл в тех случаях, когда маршрут представлен большим объёмом кода. В пределах папки маршрута можно создавать дополнительные папки, в которых сгруппирован код, ответственный за формирование разных частей страницы. Это усложняет перемещение по проекту. Тут, однако, мне хотелось бы предупредить читателя о том, что не стоит создавать структуры из папок с очень большим уровнем вложенности. Надо отметить, что использование специализированных инструментов, вроде команд поиска, даёт программисту удобные средства для работы с кодом проекта и для поиска того, что ему нужно. Глубокие вложенные структуры папок — это один из признаков чрезмерного усложнения проекта. Но структура файлов проекта также оказывает влияние на удобство работы с ним.

В моём случае этот подход отлично показывает себя на проектах-одностраничниках, реализующих множество возможностей на своей единственной странице. Структурируя код проекта, можно группировать файлы, опираясь не на маршрут, а на реализуемые этими файлами возможности проекта. Этот подход не требует особенных умственных усилий для того, чтобы принимать решения о том, какие именно сущности следует размещать рядом друг с другом, и для того, чтобы что-то искать. Но надо отметить, что группировка материалов проекта по маршрутам проще.

А можно пойти и ещё дальше — положить в один файл код двух компонентов. Если пойти по пути группировки кода дальше, то можно решить, что код контейнеров и компонентов оправданно будет поместить в один и тот же файл. Но в реальности всё далеко не так плохо. Полагаю, вы вполне можете думать сейчас о том, что рекомендовать такие вещи — это прямо-таки кощунство. И если вы используете хуки React, или сгенерированный код (или — и то и другое), я бы порекомендовал вам именно такой подход. На самом деле, такой подход вполне себя оправдывает.

Настоящий вопрос заключается в том, почему вообще может понадобиться делить компоненты на «умные» и «глупые». На самом деле, вопрос о том, как именно разложить код по файлам, имеет не первостепенную важность. На этот вопрос можно дать несколько ответов: Какие выгоды можно извлечь от такого разделения?

  1. Приложения, устроенные таким образом, легче тестировать.
  2. При разработке таких приложений легче использовать инструменты наподобие Storybook.
  3. «Глупые» компоненты можно использовать с множеством разных «умных» компонентов (и наоборот).
  4. «Умные» компоненты можно использовать на разных платформах (например — на платформах React и React Native).

Всё это — реальные доводы в пользу разделения компонентов на «умные» и «глупые», но они применимы далеко не ко всем ситуациям. Например, мы часто, при создании проектов, используем Apollo Client с хуками. Для того чтобы такие проекты тестировать, можно либо создавать моки ответов Apollo, либо моки хуков. То же самое касается и Storybook. Если говорить о смешивании и совместном использовании «умных» и «глупых» компонентов, то я, на самом деле, никогда этого на практике не встречал. В том, что касается кроссплатформенного использования кода, был один проект, в котором я собирался сделать нечто подобное, но так и не сделал. Это должен был быть монорепозиторий Lerna. В наши дни вместо этого подхода вполне можно выбрать React Native Web.

Это — важная концепция, о которой стоит знать. В результате можно сказать, что в разделении компонентов на «умные» и «глупые» есть определённый смысл. Но часто о ней не нужно особенно сильно беспокоиться, особенно учитывая недавнее появление хуков React.

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

Более того, если возникнет такая необходимость, некий компонент всегда можно разделить на два отдельных компонента — «умный» и «глупый».

Стилизация

Мы используем для стилизации приложений emotion / styled components. Всегда есть соблазн выделить стили в отдельный файл. Я видел, как некоторые разработчики так и поступают. Но, после того как я испробовал оба подхода, я в итоге не нашёл причин для перемещения стилей в отдельный файл. Как и в случае со многим другим, о чём мы тут говорим, разработчик может облегчить себе жизнь, совмещая в одном файле стили и компоненты, к которым они относятся.

▍Структура проекта серверной части приложения

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

src │ app.js # Точка входа в приложение └───api # Контроллер маршрутов Express для всех конечных точек приложения └───config # Переменные среды и средства конфигурирования └───jobs # Объявление заданий для agenda.js └───loaders # Разделение кода на модули └───models # Модели баз данных └───services # Бизнес-логика └───subscribers # Обработчики событий для асинхронных задач └───types # Файлы объявлений типов (d.ts) для Typescript

Мы в своих проектах обычно применяем GraphQL. Поэтому в них используются файлы, в которых хранятся модели, сервисы и распознаватели. Вместо того чтобы разбрасывать их по разным местам проекта, я собираю их в одной папке. Чаще всего эти файлы будут использоваться совместно, и с ними будет легче работать в том случае, если они будут храниться в одной и той же папке.

Не переписывайте по многу раз определения типов

Мы используем в своих проектах множество решений, так или иначе имеющих отношение к типам данных. Это TypeScript, GraphQL, схемы баз данных, и иногда MobX. В результате может оказаться так, что типы для одних и тех же сущностей описывают по 3-4 раза. Подобных вещей стоит избегать. Надо стремиться к использованию инструментов, автоматически генерирующих описания типов.

Этого хватит для описания всех используемых типов. На сервере для этой цели можно воспользоваться комбинацией TypeORM/Typegoose и TypeGraphQL. TypeGraphQL поможет в создании типов GraphQL и TypeScript. TypeORM/Typegoose позволит описать схему базы данных и соответствующие типы TypeScript.

Вот пример определения типов TypeORM (MongoDB) и TypeGraphQL в одном файле:

import from 'type-graphql'
import { Entity, ObjectIdColumn, ObjectID, Column, CreateDateColumn, UpdateDateColumn,
} from 'typeorm' @ObjectType()
@Entity()
export default class Policy { @Field(type => ID) @ObjectIdColumn() _id: ObjectID @Field() @CreateDateColumn({ type: 'timestamp' }) createdAt: Date @Field({ nullable: true }) @UpdateDateColumn({ type: 'timestamp', nullable: true }) updatedAt?: Date @Field() @Column() name: string @Field() @Column() version: number
}

GraphQL Code Generator также умеет генерировать множество различных типов. Мы используем этот инструмент для создания типов TypeScript на клиенте, а так же — хуков React, выполняющих обращения к серверу.

Если же вы, к тому же, пользуетесь и GraphQL, то вам стоит взглянуть на новый пакет — MST-GQL, который генерирует дерево состояния из GQL-схемы. Если вы используете MobX для управления состоянием приложения, то вы, воспользовавшись парой строк кода, можете получить автоматически сгенерированные TS-типы.

Совместное использование этих инструментов убережёт вас от переписывания больших объёмов кода и поможет избежать типичных ошибок.

У использования подобных инструментов, конечно, есть свои плюсы и минусы. Другие решения, такие как Prisma, Hasura и AWS AppSync, тоже могут помочь избежать дублирования объявлений типов. В создаваемых нами проектах подобные средства используются не всегда, так как нам нужно развёртывать код на собственных серверах организаций.

Прибегайте всегда, когда это возможно, к средствам автоматического генерирования кода

Если взглянуть на код, который создают без использования вышеописанных средств для автоматического генерирования кода, то окажется, что программистам постоянно приходится писать одно и тоже. Главный совет, который я могу дать по этому поводу, заключается в том, что нужно создавать сниппеты для всего, чем вы часто пользуетесь. Если вы часто вводите команду console.log — создайте сниппет, вроде cl, который автоматически превращается в console.log(). Если вы этого не сделаете и попросите меня помочь вам с отладкой кода, меня это сильно расстроит.

Например — с помощью Snippet generator. Существует множество пакетов со сниппетами, но несложно и создавать собственные сниппеты.

Вот код, который позволяет добавить некоторые из моих любимых сниппетов в VS Code:

{ "Export default": { "scope": "javascript,typescript,javascriptreact,typescriptreact", "prefix": "eid", "body": [ "export { default } from './${TM_DIRECTORY/.*[\\/](.*)$$/$1/}'", "$2" ], "description": "Import and export default in a single line" }, "Filename": { "prefix": "fn", "body": ["${TM_FILENAME_BASE}"], "description": "Print filename" }, "Import emotion styled": { "prefix": "imes", "body": ["import styled from '@emotion/styled'"], "description": "Import Emotion js as styled" }, "Import emotion css only": { "prefix": "imec", "body": ["import { css } from '@emotion/styled'"], "description": "Import Emotion css only" }, "Import emotion styled and css only": { "prefix": "imesc", "body": ["import styled, { css } from ''@emotion/styled'"], "description": "Import Emotion js and css" }, "Styled component": { "prefix": "sc", "body": ["const ${1} = styled.${2}`", " ${3}", "`"], "description": "Import Emotion js and css" }, "TypeScript React Function Component": { "prefix": "rfc", "body": [ "import React from 'react'", "", "interface ${1:ComponentName}Props {", "}", "", "const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {", " return (", " <div>", " ${1:ComponentName}", " </div>", " )", "}", "", "export default ${1:ComponentName}", "" ], "description": "TypeScript React Function Component" }
}

Сэкономить время, помимо сниппетов, могут помочь генераторы кода. Их можно создавать самостоятельно. Мне для этого нравится использовать plop.

С помощью инструментов командной строки можно создать новый компонент, состоящий из 4 файлов, в которых представлено всё то, что можно ожидать найти в компоненте. В Angular есть собственные встроенные генераторы кода. Если каждый новый создаваемый вами компонент должен быть представлен в виде папки, содержащей файл с кодом компонента, файл с тестом и файл Storybook, генератор поможет создать всё это одной командой. Жаль, что в React нет такой вот стандартной возможности, но нечто подобное можно создать и самостоятельно, используя plop. Например, при добавлении новой возможности на сервер достаточно выполнить одну команду в командной строке. Это во многих случаях значительно облегчает жизнь разработчика. После этого автоматически будут созданы файлы сущности, сервисов и распознавателей, содержащие все необходимые базовые конструкции.

Если все используют один и тот же plop-генератор, то код у всех будет получаться весьма однородным. Ещё одна сильная сторона генераторов кода заключается в том, что они способствуют единообразию в командной разработке.

Автоматическое форматирование кода

Форматирование кода — простая задача, но её, к сожалению, не всегда решают правильно. Не тратьте время, вручную выравнивая код или вставляя в него точки с запятой. Используйте Prettier для автоматического форматирования кода при выполнении коммитов.

Итоги

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

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

Уважаемые читатели! Что вы думаете об идеях, касающихся разработки фулстек-приложений на JavaScript, изложенных в этом материале?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть