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

В своем романе «Атомные привычки» автор Джеймс Клир утверждает, что:

«Мастерство создается привычками. Однако иногда, когда мы работаем на автопилоте, мы склонны ошибаться… То, что мы набираемся опыта, выполняя привычки, не означает, что мы совершенствуемся. На самом деле мы возвращаемся назад по шкале улучшений, и большинство привычек превращаются в автопилот». Другими словами, практика приводит к совершенству, а плохая практика только ухудшает ситуацию.

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

К счастью, в отличие от других языков, которые более привержены конкретным парадигмам проектирования (Java, Ruby), Node — это дом многих идей. В этом сообществе я чувствую себя в безопасности, чтобы подвергнуть сомнению некоторые из наших старых добрых инструментов и шаблонов. Список ниже содержит мои личные убеждения, которые приведены с аргументацией и примерами.

Являются ли эти разрушительные мысли правильными? Я не уверен. Однако есть одна вещь, в которой я уверен: чтобы Node.js прожил дольше, нам нужно поощрять критиков, сосредоточивать нашу лояльность на инновациях и поддерживать обсуждение. Результатом этого обсуждения является не «не используйте этот инструмент!» а лучше ознакомиться с другими методами, которые при некоторых обстоятельствах могут подойти лучше

TOC — Шаблоны, которые необходимо пересмотреть

  1. Дотенв
  2. Вызов службы из контроллера
  3. Внедрение зависимостей Nest.js для всех классов
  4. Паспорт.js
  5. Супертест
  6. Fastify вспомогательное украшение
  7. Ведение журнала из предложения catch
  8. Морган лесоруб
  9. NODE_ENV

1. Dotenv в качестве источника конфигурации

💁‍♂️ О чем это: Очень популярный метод, при котором настраиваемые значения приложения (например, имя пользователя БД) хранятся в простом текстовом файле. Затем, когда приложение загружается, библиотека dotenv устанавливает все значения текстового файла в качестве переменных среды, чтобы код мог прочитать это.

// .env file
USER_SERVICE_URL=https://users.myorg.com
//start.js
require('dotenv').config();
//blog-post-service.js
repository.savePost(post);
//update the user number of posts, read the users service URL from an environment variable
await axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)

📊 Популярность: 21 806 137 загрузок в неделю!

🤔 Почему это может быть неправильно: Dotenv настолько прост и интуитивно понятен для начала, что можно легко упустить основные функции: например, сложно вывести схему конфигурации и понять значение каждого ключа и его типирование. Следовательно, не существует встроенного способа быстрого отказа при отсутствии обязательного ключа — поток может завершиться ошибкой после запуска и представить некоторые побочные эффекты (например, записи БД уже были изменены до отказа). В приведенном выше примере сообщение в блоге будет сохранено в БД, и только тогда код поймет, что отсутствует обязательный ключ — это оставляет приложение зависшим в недопустимом состоянии. Вдобавок ко всему, при наличии множества ключей невозможно организовать их иерархически. Если этого недостаточно, он побуждает разработчиков зафиксировать этот файл .env, который может содержать производственные значения — это происходит потому, что нет четкого способа определить значения по умолчанию для разработки. Команды обычно обходят это, фиксируя файл .env.example, а затем просят любого, кто извлекает код, переименовать этот файл вручную. Если они помнят, конечно

☀️ Лучшая альтернатива: некоторые библиотеки конфигурации предоставляют готовое решение для всех этих потребностей. Они поощряют четкую схему и возможность ранней проверки и отказа при необходимости. См. сравнение вариантов здесь. Одной из лучших альтернатив является осужденный, внизу тот же пример, на этот раз с осужденным, надеюсь, теперь он лучше:

// config.js
export default {
  userService: {
    url: {
      // Hierarchical, documented and strongly typed 👇
      doc: "The URL of the user management service including a trailing slash",
      format: "url",
      default: "http://localhost:4001",
      nullable: false,
      env: "USER_SERVICE_URL",
    },
  },
  //more keys here
};
//start.js
import convict from "convict";
import configSchema from "config";
convict(configSchema);
// Fail fast!
convictConfigurationProvider.validate();
//blog-post.js
repository.savePost(post);
// Will never arrive here if the URL is not set
await axios.put(
  `${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`
);

2. Вызов жирного сервиса из контроллера API

💁‍♂️ О чем это: Рассмотрим читателя нашего кода, который хочет понять весь высокоуровневый процесс или углубиться в очень специфический часть. Сначала она приземляется на контроллере API, где начинаются запросы. В отличие от того, что следует из его названия, этот уровень контроллера представляет собой всего лишь адаптер и остается очень тонким и простым. Пока отлично. Затем контроллер вызывает большую «службу» с тысячами строк кода, представляющих всю логику.

// user-controller
router.post('/', async (req, res, next) => {
    await userService.add(req.body);
    // Might have here try-catch or error response logic
}

// user-service
exports function add(newUser){
    // Want to understand quickly? Need to understand the entire user service, 1500 loc
    // It uses technical language and reuse narratives of other flows
    this.copyMoreFieldsToUser(newUser)
    const doesExist = this.updateIfAlreadyExists(newUser)
    if(!doesExist){
        addToCache(newUser);
    }
    // 20 more lines that demand navigating to other functions in order to get the intent
}

📊 Насколько популярен: здесь трудно привести точные цифры, я могу с уверенностью сказать, что в большинстве приложений, которые я вижу, это так.

🤔 Почему это может быть неправильно? Мы здесь, чтобы справляться со сложностями. Одним из полезных методов является откладывание сложности на более позднюю стадию. Однако в этом случае читатель кода (надеюсь) начинает свое путешествие по тестам и контроллеру — в этих областях все просто. Затем, когда она приземляется на большой сервис — она получает массу сложностей и мелких деталей, хотя она сосредоточена на понимании общего потока или какой-то конкретной логики. Это ненужная сложность

☀️ Лучшая альтернатива: контроллер должен вызывать определенный тип службы, прецедент , который отвечает за обобщение потока деловым и простым языком. Каждый поток/функция описывается с помощью варианта использования, каждый содержит от 4 до 10 строк кода, которые рассказывают историю без технических подробностей. В основном он управляет другими небольшими службами, клиентами и репозиториями, в которых хранятся все детали реализации. С вариантами использования читатель может легко понять поток высокого уровня. Теперь она может выбрать, на чем она хотела бы сосредоточиться. Она теперь подвергается только необходимой сложности. Этот метод также поощряет разделение кода на меньший объект, который оркестрирует вариант использования. Бонус: просматривая отчеты о покрытии, можно сказать, какие функции охвачены, а не только файлы/функции.

Эта идея, кстати, формализована в книге о чистой архитектуре — я не большой поклонник причудливых архитектур, но, видите ли, стоит выбирать техники из каждого источника. Вы можете ознакомиться с нашим Началом с лучшими практиками Node.js, practica.js и изучить код вариантов использования.

// add-order-use-case.js
export async function addOrder(newOrder: addOrderDTO) {
  orderValidation.assertOrderIsValid(newOrder);
  const userWhoOrdered = await userServiceClient.getUserWhoOrdered(
    newOrder.userId
  );
  paymentTermsService.assertPaymentTerms(
    newOrder.paymentTermsInDays,
    userWhoOrdered.terms
  );

  const response = await orderRepository.addOrder(newOrder);

  return response;
}

3. Nest.js: подключите все с внедрением зависимостей

💁‍♂️ О чем это: Если вы делаете Nest.js, помимо мощной инфраструктуры в ваших руках, вы, вероятно, используете DI для всего и делаете каждый класс инъекционный. Скажем, у вас есть служба погоды, которая зависит от службы влажности, и нет необходимости обмениваться службой влажности с альтернативными поставщиками. Тем не менее, вы внедряете сервис влажности в сервис погоды. Это становится частью вашего стиля разработки, «почему бы и нет» вы думаете — мне может понадобиться заглушить его во время тестирования или заменить в будущем.

// humidity-service.ts - not customer facing
@Injectable()
export class GoogleHumidityService {
async getHumidity(when: Datetime): Promise<number> {
    // Fetches from some specific cloud service
  }
}
// weather-service.ts - customer facing
import { GoogleHumidityService } from './humidity-service.ts';
export type weatherInfo{
    temperature: number,
    humidity: number
}
export class WeatherService {
  constructor(private humidityService: GoogleHumidityService) {}
  async GetWeather(when: Datetime): Promise<weatherInfo> {
    // Fetch temperature from somewhere and then humidity from GoogleHumidityService
  }
}
// app.module.ts
@Module({
  providers: [GoogleHumidityService, WeatherService],
})
export class AppModule {}

📊 Насколько популярно: здесь нет цифр, но я могу с уверенностью сказать, что во всех приложениях Nest.js, которые я видел, это так. В популярном nestjs-realworld-example-ap[p]() все сервисы инжектируемые.

🤔 Почему это может быть неправильно: Внедрение зависимостей — это не бесценный стиль кодирования, а шаблон, который вы должны использовать в нужный момент, как и любой другой шаблон. Почему? Потому что любой узор имеет цену. Какая цена, спросите вы? Во-первых, нарушена инкапсуляция. Клиенты службы погоды теперь знают, что другие поставщики используются внутренне. У некоторых клиентов может возникнуть соблазн переопределить поставщиков, но это не входит в их обязанности. Во-вторых, это еще один уровень сложности для изучения, поддержания и еще один способ выстрелить себе в ногу. StackOverflow обязан частью своих доходов Nest.js DI — множество дискуссий пытаются решить эту загадку (например, знаете ли вы, что в случае циклических зависимостей порядок импорта имеет значение?). В-третьих, производительность — Nest.js, например, изо всех сил пытался обеспечить достойное время запуска для бессерверных сред и был вынужден ввести ленивые загружаемые модули. Не поймите меня неправильно, в некоторых случаях есть хороший случай для DI: когда возникает необходимость отделить зависимость от вызывающего объекта или разрешить клиентам внедрять пользовательские реализации (например, шаблон стратегии). В таком случае, когда есть ценность, вы можете подумать, стоит ли ценность внедрения зависимостей своей цены. Если у вас нет этого кейса, зачем платить ни за что?

Я рекомендую прочитать первые абзацы этого сообщения в блоге ‘Внедрение зависимостей — это ЗЛО’ (и абсолютно не согласен с этими смелыми словами)

☀️ Лучшая альтернатива: «экономизируйте» свой инженерный подход — избегайте использования каких-либо инструментов, если только они не отвечают реальным потребностям. Начните с простого, зависимый класс должен просто импортировать свою зависимость и использовать ее — да, используя простую систему модулей Node.js («требуется»). Столкнулись с ситуацией, когда необходимо факторизовать динамические объекты? Есть несколько простых шаблонов, более простых, чем DI, которые вы должны учитывать, например, if/else, фабричная функция и многое другое. Требуются ли синглтоны? Рассмотрите методы с меньшими затратами, такие как модульная система с фабричной функцией. Нужно заглушить/изобразить для тестирования? Исправление Monkey может быть лучше, чем DI: лучше немного загромождать тестовый код, чем загромождать рабочий код. У вас есть сильная потребность скрыть от объекта, откуда исходят его зависимости? Вы уверены? Используйте ДИ!

// humidity-service.ts - not customer facing
export async function getHumidity(when: Datetime): Promise<number> {
  // Fetches from some specific cloud service
}

// weather-service.ts - customer facing
import { getHumidity } from "./humidity-service.ts";
// ✅ No wiring is happening externally, all is flat and explicit. Simple
export async function getWeather(when: Datetime): Promise<number> {
  // Fetch temperature from somewhere and then humidity from GoogleHumidityService
  // Nobody needs to know about it, its an implementation details
  await getHumidity(when);
}

1 мин. пауза: Пару слов обо мне, авторе

Меня зовут Йони Голдберг, я разработчик и консультант Node.js. Я написал несколько кодовых книг, таких как Лучшие практики тестирования JavaScript и Лучшие практики Node.js (100 000 звезд ✨🥹). Тем не менее, мое лучшее руководство — это Практики тестирования Node.js, которые мало кто читает 😞. Скоро выпущу продвинутый курс тестирования Node.js, а также проведу мастер-классы для команд. Я также являюсь основным сопровождающим Practica.js, который является стартером Node.js, который создает готовый пример решения Node Monorepo, основанный на стандартах и ​​простоте. Это может быть вашим основным вариантом при запуске нового решения Node.js.

4. Passport.js для аутентификации по токену

💁‍♂️ О чем речь: Обычно вам нужно выпустить и/или аутентифицировать токены JWT. Точно так же вам может потребоваться разрешить вход из одной социальной сети, такой как Google/Facebook. Сталкиваясь с такими потребностями, разработчики Node.js спешат к славной библиотеке Passport.js, как бабочки притягиваются к свету.

📊 Популярность: 1 389 720 загрузок в неделю.

🤔 Почему это может быть неправильно: Когда вам нужно защитить ваши маршруты с помощью токена JWT, вам не хватает всего нескольких строк кода, чтобы поставить галочку. Вместо того, чтобы возиться с новым фреймворком, вместо того, чтобы вводить уровни косвенности (вы вызываете паспорт, затем он вызывает вас), вместо того, чтобы тратить время на изучение новых абстракций — используйте библиотеку JWT напрямую. Такие библиотеки, как jsonwebtoken или fast-jwt, просты и хорошо поддерживаются. Есть опасения по поводу усиления безопасности? Хороший вопрос, ваши опасения обоснованы. Но не станете ли вы лучше закаляться с непосредственным пониманием вашей конфигурации и потока? Поможет ли скрытие вещей за фреймворком? Даже если вы предпочитаете ужесточение проверенной в боевых условиях инфраструктуры, Passport не справляется с несколькими рисками безопасности, такими как секреты/токены, безопасное управление пользователями, защита БД и многое другое. Я хочу сказать, что вам, вероятно, в любом случае нужны полнофункциональные платформы управления пользователями и аутентификацией. Различные облачные сервисы и проекты OSS могут решить все эти проблемы безопасности. Зачем тогда начинать с фреймворка, который не удовлетворяет ваши потребности в безопасности? Похоже, что многие, выбравшие Passport.js, не до конца понимают, какие потребности удовлетворяются, а какие остаются открытыми. При всем при этом Passport определенно превосходен, когда вы ищете быстрый способ поддержки многих провайдеров входа через социальные сети.

☀️ Лучшая альтернатива: Аутентификация по токену в порядке? Эти несколько строк кода ниже могут быть всем, что вам нужно. Вы также можете заглянуть в Оболочку Practica.js вокруг этих библиотек. Реальному крупномасштабному проекту обычно требуется больше: поддержка асинхронного JWT (JWKS), безопасное управление и ротация секретов, и это лишь несколько примеров. В этом случае решение OSS, такое как [keycloak (https://github.com/keycloak/keycloak), или коммерческие варианты, такие как Auth0[https://github.com/auth0], являются альтернативами для рассмотрения.

// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases
const middleware = (req, res, next) => {
    if(!req.headers.authorization){
        res.sendStatus(401)
    }

jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {
      if (err) {
        return res.sendStatus(401);
      }
      req.user = jwtContent.data;
      next();
    });

5. Супертест для интеграции/тестирования API

💁‍♂️ О чем речь: При тестировании API (то есть компонентов, интеграции, E2E-тестов) библиотека supertest предоставляет приятный синтаксис, который может одновременно определять адрес веб-сервера, HTTP-вызов, а также подтверждение ответа. Три в одном

test("When adding invalid user, then the response is 400", (done) => {
  const request = require("supertest");
  const app = express();
  // Arrange
  const userToAdd = {
    name: undefined,
  };

// Act
  request(app)
    .post("/user")
    .send(userToAdd)
    .expect("Content-Type", /json/)
    .expect(400, done);
  // Assert
  // We already asserted above ☝🏻 as part of the request
});

📊 Популярность: 2 717 744 загрузки в неделю.

🤔 Почему это может быть неправильно: У вас уже есть ваша библиотека утверждений (Jest? Chai?), в ней отличное выделение и сравнение ошибок — вы ей доверяете. Зачем кодировать некоторые тесты, используя другой синтаксис утверждений? Не говоря уже о том, что ошибки утверждений Supertest не так наглядны, как Jest и Chai. Также неудобно смешивать HTTP-клиент + библиотеку утверждений вместо того, чтобы выбирать лучшее для каждой миссии. Говоря о лучших, есть более стандартные, популярные и лучше поддерживаемые HTTP-клиенты (такие как fetch, axios и другие). Нужна другая причина? Супертест может поощрять связывание тестов с Express, поскольку он предлагает конструктор, который получает объект Express. Этот конструктор автоматически выводит адрес API (полезно при использовании динамических тестовых портов). Это связывает тест с реализацией и не будет работать в случае, когда вы хотите запустить те же тесты для удаленного процесса (API не живет с тестами). Мой репозиторий Оптимальные методы тестирования Node.js содержит примеры того, как тесты могут определить порт и адрес API.

☀️ Лучшая альтернатива: популярная и стандартная клиентская библиотека HTTP, такая как Node.js Fetch или Axios. В Practica.js (стартер Node.js, в котором собрано множество лучших практик) мы используем Axios. Это позволяет нам настроить HTTP-клиент, который используется всеми тестами: мы запекаем токен JWT, заголовки и базовый URL-адрес. Еще один хороший шаблон, на который мы обращаем внимание, заключается в том, что каждая микрослужба создает клиентскую библиотеку HTTP для своих потребителей. Это дает клиентам строгий опыт, синхронизирует версии поставщика и потребителя и, в качестве бонуса, поставщик может тестировать себя с той же библиотекой, которую используют его потребители.

test("When adding invalid user, then the response is 400 and includes a reason", (done) => {
  const app = express();
  // Arrange
  const userToAdd = {
    name: undefined,
  };

// Act
  const receivedResponse = axios.post(
    `http://localhost:${apiPort}/user`,
    userToAdd
  );
  // Assert
  // ✅ Assertion happens in a dedicated stage and a dedicated library
  expect(receivedResponse).toMatchObject({
    status: 400,
    data: {
      reason: "no-name",
    },
  });
});

6. Ускорить оформление для незапрашиваемых/веб-утилит

💁‍♂️ О чем это: Fastify представляет отличные шаблоны. Лично я высоко ценю то, как он сохраняет простоту Express, но при этом дает больше батарей. Одна вещь, которая заставила меня задуматься, — это функция украсить, которая позволяет размещать общие утилиты/сервисы внутри широкодоступного объекта-контейнера. Я имею в виду конкретно случай, когда используется сквозная утилита/сервис. Вот пример:

// An example of a utility that is cross-cutting-concern. Could be logger or anything else
fastify.decorate('metricsService', function (name) {
  fireMetric: () => {
    // My code that sends metrics to the monitoring system
  }
})

fastify.get('/api/orders', async function (request, reply) {
  this.metricsService.fireMetric({name: 'new-request'})
  // Handle the request
})
// my-business-logic.js
exports function calculateSomething(){
  // How to fire a metric?
}

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

📊 Насколько он популярен: Fastify скачивают 696 122 раза в неделю, и этот показатель быстро растет. Концепция декоратора является частью ядра фреймворка.

🤔 Почему это может быть неправильно. Некоторые сервисы и утилиты служат сквозным потребностям и должны быть доступны из других уровней, таких как домен (например, бизнес-логика, DAL). При размещении инженерных сетей внутри этого объекта объект Fastify может быть недоступен для этих слоев. Вы, вероятно, не хотите связывать свою веб-инфраструктуру со своей бизнес-логикой: учтите, что некоторые из ваших бизнес-логики и репозиториев могут быть вызваны из клиентов, отличных от REST, таких как CRON, MQ и т. д. — в этих случаях Fastify не получит вообще не участвует, так что лучше не доверяйте этому сервисному локатору

☀️ Лучшая альтернатива: старый добрый модуль Node.js — это стандартный способ предоставления и использования функций. Нужен синглтон? Используйте модуль системного кэширования. Нужно создать экземпляр службы в соответствии с хуком жизненного цикла Fastify (например, соединение с БД при запуске)? Вызовите его из этого хука Fastify. В редких случаях, когда требуется высокодинамичная и сложная реализация зависимостей, DI также является (сложным) вариантом для рассмотрения.

// ✅ A simple usage of good old Node.js modules
// metrics-service.js
exports async function fireMetric(name){
  // My code that sends metrics to the monitoring system
}
import {fireMetric} from './metrics-service.js'
fastify.get('/api/orders', async function (request, reply) {
  metricsService.fireMetric({name: 'new-request'})
})
// my-business-logic.js
exports function calculateSomething(){
  metricsService.fireMetric({name: 'new-request'})
}

7. Логирование из предложения catch

💁‍♂️ О чем это: Вы ловите ошибку где-то глубоко в коде (не на уровне маршрута), затем вызываете logger.error, чтобы сделать эту ошибку наблюдаемой. Вроде просто и нужно

try{
    axios.post('https://thatService.io/api/users);
}
catch(error){
    logger.error(error, this, {operation: addNewOrder});
}

📊 Насколько популярен: Трудно понять цифры, но он довольно популярен, верно?

🤔 Почему это может быть неправильно. Во-первых, ошибки должны обрабатываться/записываться централизованно. Обработка ошибок является критическим путем. Различные предложения catch могут вести себя по-разному без централизованного и унифицированного поведения. Например, может возникнуть запрос пометить все ошибки определенными метаданными или помимо ведения журнала также активировать метрику мониторинга. Применение этих требований примерно в 100 местах — это не прогулка в парке. Во-вторых, предложения catch должны быть сведены к конкретным сценариям. По умолчанию естественный поток ошибок спускается к маршруту/точке входа — оттуда он будет перенаправлен обработчику ошибок. Предложения catch являются более подробными и подверженными ошибкам, поэтому они должны служить двум очень конкретным потребностям: Когда кто-то хочет изменить поток на основе ошибки или дополнить ошибку дополнительной информацией (что не так в этом примере).

☀️ Лучшая альтернатива: по умолчанию пусть ошибка распространяется вниз по уровням и перехватывается глобальной системой перехвата точки входа (например, ПО промежуточного слоя для ошибок Express). В тех случаях, когда ошибка должна вызвать другой поток (например, повторную попытку) или есть смысл дополнить ошибку дополнительным контекстом — используйте предложение catch. В этом случае убедитесь, что код .catch также сообщает обработчику ошибок.

// A case where we wish to retry upon failure
try{
    axios.post('https://thatService.io/api/users);
}
catch(error){
    // ✅ A central location that handles error
    errorHandler.handle(error, this, {operation: addNewOrder});
    callTheUserService(numOfRetries++);
}

8. Используйте логгер Morgan для экспресс-веб-запросов

💁‍♂️ О чем это: Во многих веб-приложениях вы, вероятно, найдете шаблон, который веками копировался — Использование Morgan logger для регистрации информации о запросах:

const express = require("express");
const morgan = require("morgan");
const app = express();
app.use(morgan("combined"));

📊 Популярность: 2 901 574 загрузки в неделю.

🤔 Почему это может быть неправильно: Подождите секунду, ведь у вас уже есть главный регистратор, верно? Это Пино? Уинстон? Что-то другое? Большой. Зачем заниматься и настраивать еще один регистратор? Я ценю доменный язык HTTP (DSL) Morgan. Синтаксис сладкий! Но оправдывает ли это наличие двух регистраторов?

☀️ Лучшая альтернатива: поместите выбранный вами регистратор в промежуточное ПО и зарегистрируйте желаемые свойства запроса/ответа:

// ✅ Use your preferred logger for all the tasks
const logger = require("pino")();
app.use((req, res, next) => {
  res.on("finish", () => {
    logger.info(`${req.url} ${res.statusCode}`); // Add other properties here
  });
  next();
});

9. Наличие условного кода на основе NODE_ENV значения

💁‍♂️ О чем речь: Чтобы различать конфигурацию разработки и производственной среды, обычно для переменной среды NODE_ENV устанавливается значение «production|test». Это позволяет различным инструментам действовать по-разному. Например, некоторые механизмы шаблонов будут кэшировать скомпилированные шаблоны только в рабочей среде. Помимо инструментов, пользовательские приложения используют это для указания поведения, уникального для среды разработки или рабочей среды:

if (process.env.NODE_ENV === "production") {
  // This is unlikely to be tested since test runner usually set NODE_ENV=test
  setLogger({ stdout: true, prettyPrint: false });
  // If this code branch above exists, why not add more production-only configurations:
  collectMetrics();
} else {
  setLogger({ splunk: true, prettyPrint: true });
}

📊 Насколько популярен: 5 034 323 кода выдают GitHub при поиске «NODE_ENV». Не похоже на редкую модель

🤔 Почему это может быть неправильно: каждый раз, когда ваш код проверяет, является ли он рабочим или нет, эта ветвь не будет срабатывать по умолчанию в некоторых средствах запуска тестов (например, Jest set NODE_ENV=test). В любом средстве запуска тестов разработчик должен не забывать проверять каждое возможное значение этой переменной среды. В приведенном выше примере collectMetrics() будет впервые протестировано в рабочей среде. Грустный смайлик. Кроме того, установка этих условий открывает дверь для добавления дополнительных различий между рабочей машиной и машиной разработчика — когда эта переменная и условия существуют, у разработчика возникает соблазн добавить некоторую логику только для рабочей среды. Теоретически это можно протестировать: можно поставить NODE_ENV = "production" в тестировании и покрыть ветки производства (если она помнит...). Но тогда, если можно тестировать с NODE_ENV='production', какой смысл в разделении? Просто считайте все «производством» и избегайте этой чреватой ошибками умственной нагрузки.

☀️ Лучшая альтернатива: Любой код, написанный нами, должен быть протестирован. Это подразумевает отказ от любых условий if(production)/else(development). В любом случае, не будет ли у машины разработчика окружающая инфраструктура, отличная от производственной (например, система ведения журнала)? Да, среда совсем другая, но мы чувствуем себя в ней комфортно. Эти инфраструктурные штучки проверены в бою, посторонние и не являются частью нашего кода. Чтобы сохранить один и тот же код между dev/prod и при этом использовать разную инфраструктуру — мы ставим разные значения в конфигурации (не в коде). Например, типичный регистратор выдает JSON в продакшене, но в машине для разработки он выдает цветные строки «красивого шрифта». Чтобы соответствовать этому, мы устанавливаем ENV VAR, который сообщает, к какому стилю ведения журнала мы стремимся:

//package.json
"scripts": {
    "start": "LOG_PRETTY_PRINT=false index.js",
    "test": "LOG_PRETTY_PRINT=true jest"
}
//index.js
//✅ No condition, same code for all the environments. The variations are defined externally in config or deployment files
setLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})

Закрытие

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

Некоторые другие мои статьи