Node.js — это серверная среда выполнения JavaScript, построенная на движке V8 в Chrome, который по своей природе является асинхронным и событийным. С помощью Node.js относительно несложно создать REST API и использовать такие фреймворки, как Express.js. Эта простота обеспечивает большую гибкость. Однако при создании масштабируемых сетевых приложений, управляемых сетью, можно запутаться в том, каким шаблонам следовать.

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

Стиль кодирования и лучшие практики Node.js

Ключевые слова const и let для объявления переменных

В JavaScript существуют различные способы объявления переменных: олдскульный var и более современные let и const.

var объявляет переменную, входящую в область видимости функции (когда она объявляется внутри функции) или переменную, входящую в глобальную область видимости (когда она объявляется вне функции).

let и const объявляют переменные, связанные с блочной областью видимости.

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

let myInt = 3;
myInt = 6;
console.log(myInt); // 6
let myArray = [0, 1, 2, 3];
console.log(myArray); // [ 0, 1, 2, 3 ]
let myOtherArray = ["one", "two", "three"];
myArray = myOtherArray;
console.log(myArray); // [ 'one', 'two', 'three' ]

Ключевое слово const может внести некоторую путаницу. Оно не обязательно определяет постоянное значение, оно определяет постоянную ссылку на значение. Оно создает ссылку на значение только для чтения, но это не означает, что значение, которое оно содержит, является иммутабельным, просто оно не может быть переназначено.

const myInt = 3;
myInt = 6; // TypeError: Assignment to constant variable.
 
const myArray = [0, 1, 2, 3];
console.log(myArray); // [ 0, 1, 2, 3 ]
myArray[0] = "eleven";
console.log(myArray); // [ 'eleven', 1, 2, 3 ]
let myOtherArray = ["one", "two", "three"];
myArray = myOtherArray; // TypeError: Assignment to constant variable

Как показано выше, если в нем хранится примитив, вы не можете присвоить ему другое значение. Когда он хранит объект/массив, то можно изменить значение этого объекта (его свойства/элементы), но нельзя присвоить ему другой объект.

Разобравшись с определениями, давайте рассмотрим, почему следует использовать let и const вместо var.

  1. Дублирование объявлений переменных с помощью var не вызовет ошибки.

В той же области видимости с помощью var вы объявляете новую переменную, как и подобную, которая уже существует и имеет такое же имя. Из-за этого вы можете неосознанно перезаписать значение такой переменной.

function thisFunction() {
  var x = 1;
 
  // In another part of the code, declare another variable x
  var x = 2;
 
  console.log(x); // 2
}
 
thisFunction();

И const, и let не могут объявлять повторно, поэтому у вас не получится случайно создать дубликат переменной в той же области видимости.

function thisFunction() {
  let x = 1;
 
  // In another part of the code, declare another variable x
  let x = 2;
 
  console.log(x);
}
 
thisFunction();

Если вы попытаетесь выполнить приведенный выше код, то получите следующую ошибку:

SyntaxError: Identifier 'x' has already been declared
  1. var позволяет читать переменную, которая не была объявлена.

Если вы попытаетесь получить доступ к переменной var до того, как она будет объявлена, она вернет undefined. Это может привести к ошибке, когда вы попытаетесь использовать в коде переменную, которая не была объявлена. Отследить ошибку может быть сложно, так как код не приводит к ошибкам, которые вызывают крэш, но в результате использования undefined он выдаст неожиданный результат.

Следующий код будет работать нормально.

console.log(bar); // undefined
var bar = 1;

С помощью let и const вы не сможете использовать переменную, которая не была объявлена.

console.log(foo); // ReferenceError
let foo = 2;

Попытка выполнить приведенный выше код приведет к следующей ошибке:

ReferenceError: Cannot access 'foo' before initialization
  1. Поскольку let и const работают с блочной областью видимости, они делают код более читабельным и простым, менее подверженным ошибкам. С переменными, связанными с блочной областью видимости, код становится более читабельным. Отслеживать область видимости, в которой действует переменная, также гораздо проще. Вам просто нужно взглянуть на самый внутренний блок, в котором она объявлена, чтобы узнать ее скоуп (область видимости).

Посмотрите на следующий код.

let x = 5;
 
function thisFunction() {
  let x = 1;
 
  if (true) {
    let x = 2;
  }
 
  console.log(x); // 1
}
 
thisFunction();
 
console.log(x); // 5

Поскольку let x = 2; объявлена внутри блока оператора if, вы понимаете, что она действует только внутри него. Как видите, она не влияет на переменные с аналогичными именами за пределами блока. Вы спокойно можете объявлять переменные внутри блоков, не беспокоясь о том, что это чревато повторным объявлением.

При использовании var все не так просто.

var x = 5;
 
function thisFunction() {
  var x = 1;
 
  if (true) {
    var x = 2;
  }
 
  console.log(x); // 2
}
 
thisFunction();
 
console.log(x); // 5

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

В приведенном выше примере мы объявляем переменную var x = 2; внутри оператора if. Скоуп x — это вся функция thisFunction(). Поскольку в функции уже есть переменная с аналогичным именем, мы повторно объявили x, и когда мы впоследствии используем переменную x функции, она имеет значение 2. Поэтому необходимо помнить о переменных, находящихся в области видимости, чтобы случайно не перезаписать их.

Общепринятые соглашения об именовании

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

Для именования локальных переменных и функций используйте lowerCamelCase.

const myFunction() {
  let someVariable;
}

Даже если вы определяете локальные переменные с помощью ключевого слова const, предпочтительнее использовать lowerCamelCase.

const myFunction() {
  const someVariable = "That holds a string value";
}

Существуют особые случаи использования, когда const будет называться по-другому. Если вы собираетесь объявить константу, значение которой (или вложенные значения, в случае объявления объекта) не будет меняться на протяжении всего жизненного цикла кодовой базы, используйте UPPER_SNAKE_CASE.

const ANOTHER_VAR = 3;

Определяйте классы в приложениях Node.js с помощью UpperCamelCase:

class MyClass() {
  // ...
}

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

ESLint и руководства по стилю (Style Guides)

Вместо того чтобы ломать голову над стилем написания кода в проекте, воспользуйтесь инструментом для линтинга, например ESLint. За годы своего существования он стал стандартом экосистемы JavaScript для автоматического исправления кодовой стилистики. ESLint проверяет возможные ошибки в коде, корректирует его стиль, например, аспекты расстановки интервалов, избегает антипаттернов и мелких ошибок, а также поддерживает единообразие кода проекта. Использование ESLint с таким инструментом, как Prettier, способствует исправлению проблем форматирования.

По умолчанию ESLint содержит стандартные правила для vanilla JavaScript. В нем есть система плагинов, специфичная для данного фреймворка. При работе с Node.js используйте плагины eslint-plugin-node и eslint-plugin-node-security.

Разобраться в большом проекте гораздо проще, если его код написан в едином стиле. Именно здесь на помощь приходят руководства по стилю (стайлгайды). Использование такого руководства повышает производительность команды и избавляет от споров о том, какой стайлгайд лучше для проектов Node.js. Кроме того, вы можете воспользоваться уже существующими стайлгайдами, созданными в таких компаниях, как Google и Airbnb, которые проверены временем.

Обработка ошибок в Node.js

Вы можете обрабатывать ошибки, используя синтаксис async/await и встроенный объект error (ошибки) в Node.js. Давайте рассмотрим оба варианта.

Синтаксис async/await для перехвата ошибок

Когда Node.js только появился, работа с асинхронным кодом подразумевала использование обратных вызовов  (коллбэков). Исходя из моего опыта, можно отметить, что вложенные коллбэки не сразу выходят из-под контроля. Такое известно как "ад обратных вызовов (callback hell)", и вот типичный пример:

function getData(err, function(err, res) {
  if(err !== null) {
    function(valueA, function(err, res) {
      if(err !== null) {
        function(valueB, function(err, res) {
          // it continues
        }
      }
    })
  }
})

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

Вы можете избежать вложенных коллбэков или ада обратных вызовов, используя синтаксис ES6 async/await (полностью поддерживается в Node.js версии 8 и выше). async/await — это способ работы с асинхронным кодом. Он предоставляет гораздо более компактный способ для написания кода и знакомый синтаксис. Чтобы обработать ошибки, вы можете использовать блоки try/catch вместе с синтаксисом async/await.

Используя async/await, мы можем переписать предыдущий пример следующим образом:

async function getData(err, res) {
  try {
    let resA = await functionA(res);
    let resB = await functionB(resA);
 
    return resB;
  } catch (err) {
    logger.error(err);
  }
}

Встроенный объект ошибки в Node.js

Ошибок невозможно избежать. Тем не менее, в большинстве случаев вы захотите выполнить обработку таких ошибок, как отклоненные промисы и выброшенные исключения.

Использование встроенного объекта ошибки в Node.js позволяет избежать сложностей при их обработке. Он поможет вам сохранить единообразие и предотвратить потерю информации. Кроме того, можно с помощью StackTrace извлечь пользу для поиска информации.

В качестве примера выбросим строку, как показано ниже:

if (!data) {
  throw "There is no data";
}

Это не содержит никакой информации о трассировке стека и является анти-паттерном.

Вместо этого используйте встроенный объект Error:

if (!data) {
  throw new Error("There is no data");
}

Логгеры для вашего проекта Node.js

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

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

Типичный логгер позволяет вам использовать правильные уровни логов, такие как fatal, warn, info, error, debug и trace. При помощи этих уровней можно идентифицировать и различать разнообразные критические события. Логгер также поможет предоставить контекстуальную информацию в объекте JSON с временными метками, чтобы вы могли определить, когда именно произошла запись в логе. Формат логирования должен быть читабельным для пользователя.

Хорошая библиотека логирования предоставляет фичи, облегчающие централизацию и форматирование логов. В экосистеме Node.js доступны следующие варианты:

  • Winston: Популярная библиотека логирования, которая легко конфигурируется.

  • Bunyan: еще одна популярная библиотека логирования, которая по умолчанию осуществляет вывод в JSON.

  • Log4js: Логгер для фреймворка Express, который поддерживает цветное консольное логирование из коробки.

  • Pino: Логгер, ориентированный на производительность. Считается, что он быстрее аналогов.

Пример конфигурирования Pino:

const app = require("express")();
const pino = require("pino-http")();
 
app.use(pino);
 
app.get("/", function (req, res) {
  req.log.info("something");
  res.send("hello world");
});
 
app.listen(3000);

Pino также поддерживает различные веб-фреймворки в экосистеме Node.js, такие как Fastify, Express, Hapi, Koa и Nest.

Написание тестов в Node.js

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

Написание тестов API

В приложении Node.js для начала неплохо написать тесты API. Они обеспечивают большее покрытие, чем юнит-тестирование. Для этого можно использовать такие фреймворки, как Supertest, Jest или любую другую библиотеку, предоставляющую высокоуровневую абстракцию для тестирования API.

Рассмотрим пример ниже. Это простое приложение Express, которое обслуживает один маршрут:

const express = require("express");
const bodyParser = require("body-parser");
 
const app = express();
 
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
 
// Other middlewares...
 
app.get("/", (req, res, next) => {
  res.json({ hello: "Hello World" });
});
 
module.exports = app;

Вот подходящий способ написать это с помощью Supertest:

const request = require("supertest");
const app = require("./index");
 
describe("hello test", () => {
  it("/ should return a response", async () => {
    const res = await request(app).get("/");
    expect(res.statusCode).toEqual(200);
    expect(res.body).toEqual({ hello: "Hello World" });
  });
});

Пишите понятные названия тестов

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

Проверяйте устаревшие пакеты

Для проверки устаревших пакетов можно использовать команды типа npm outdated или пакет npm-check. Это позволит избежать сбоев сборки, обусловленных наличием устаревших пакетов.

Проверка на наличие уязвимых зависимостей

Пакет может иметь уязвимости. Чтобы их обнаружить используйте инструменты сообщества, такие как npm audit, или коммерческие, такие как snyk. Если вы не пользуетесь этими инструментами, единственная альтернатива — поддерживать онлайн-контакты с техническими сообществами.

Подведение итогов: Пишите лучший код для своих приложений Node.js

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

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


Перевод статьи подготовлен в преддверии старта курса «Node.js Developer». Если вам интересно узнать свой уровень знаний для поступления на курс, пройдите вступительное тестирование по ссылке.

Комментарии (10)


  1. Suvitruf
    12.06.2022 16:05
    +8

    Не ожидал в 2022 увидеть статью про var vs let + const и про колбек хелл. На несколько лет припозднилась ????


    1. pingo
      12.06.2022 16:54
      +2

      приходите в 2032, думаю еще найдете и var и cb и jquery


      1. Suvitruf
        12.06.2022 18:04

        > jquery
        > nodejs


        1. napa3um
          12.06.2022 20:48
          +2

          var jsdom = require("jsdom"); const { JSDOM } = jsdom; const { window } = new JSDOM(); const { document } = (new JSDOM('')).window; global.document = document; var $ = jQuery = require('jquery')(window);

          Можете пользоваться, не благодарите :).


          1. Suvitruf
            12.06.2022 22:28

            D:


      1. napa3um
        12.06.2022 22:40
        +1

        Каждый день рождаются новые люди, человечество обречено проходить уже пройденные дороги снова и снова :).


    1. black1277
      12.06.2022 18:19
      +1

      Вот и я увидел название "Паттерны и антипаттерны в Node.js" - и подумал: "Ага! Сейчас узнаю что-то новенькое или новый взгляд на уже знакомое" - а тут детский сад какой-то... Потом, подумал может сам ошибся и статья старая, года 2017 может... - ан нет - 2022г. Полное разочарование.


  1. TotalAMD
    13.06.2022 06:24
    +3

    Статья дно, как и сам otus.


  1. Pest85
    13.06.2022 07:40
    +1

    На GitHub есть проект с почти 80 тысячами звёзд как раз по Node best practices.

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

    https://github.com/goldbergyoni/nodebestpractices


  1. wasil
    13.06.2022 09:36

    Ожидал увидеть паттерны специфичные для Node.js а увидел несколько советов из серии бэс-практис по JS :-)