RUVDS.com corporate blog
Website development
JavaScript
18 June

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

Original author: Elie Steinbock
Translation
Автор материала, перевод которого мы сегодня публикуем, говорит, что 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. Я стремлюсь к тому, чтобы обходиться без этой библиотеки. Я уверен в том, что существуют качественные проекты, в которых используется такая же структура. В некоторых из моих проектов имеются отдельные папки для компонентов и контейнеров. В папке компонентов может храниться нечто вроде файлов с кодом таких сущностей, как BlogPost и Profile. В папке контейнеров имеются файлы, хранящие код контейнеров BlogPostContainer и ProfileContainer. Контейнер получает данные с сервера и передаёт их «глупому» дочернему компоненту, задача которого заключается в том, чтобы вывести эти данные на экран.

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

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

Обычно мы используем папки routes / screens и папку components. В папке для компонентов обычно хранится код таких элементов, как Button или Input. Этот код может быть использован на любой странице приложения. Каждая папка, находящаяся в папке для маршрутов, представляет собой отдельную страницу приложения. При этом файлы с кодом компонентов и с кодом логики приложения, относящиеся к данному маршруту, находятся внутри той же самой папки. А код компонентов, которые используются на нескольких страницах, попадает в папку 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. Этого хватит для описания всех используемых типов. TypeORM/Typegoose позволит описать схему базы данных и соответствующие типы TypeScript. TypeGraphQL поможет в создании типов GraphQL и TypeScript.

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

import { Field, ObjectType, ID } 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, выполняющих обращения к серверу.

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

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

Другие решения, такие как 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.

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

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

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


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

Итоги


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

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

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




+22
12.1k 99
Comments 8
Top of the day