Вопросы для собеседования для прикладного инженера Node.js, версия 2023 — Часть 2

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

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

Какой шаблон проектирования реализует EventEmitter?

EventEmitter в Node.js реализует шаблон проектирования Observer (также известный как шаблон публикации-подписки). Этот шаблон характеризуется объектами (известными как «наблюдатели» или «подписчики»), которые «прослушивают» события и «реагируют» (или «выполняют функцию обратного вызова»), когда эти события («сообщения») «испускаются» (или «опубликовано») другими объектами (известными как «субъекты» или «издатели»).

Как связаны контракты EventEmitter и Readable?

Node.js широко известен своей событийно-ориентированной архитектурой. Этому в первую очередь способствует использование класса EventEmitter, который является одним из основных классов Node.js. Он позволяет объектам генерировать именованные события, которые вызывают вызов «слушателей» функциональных объектов. Но это только первая часть нашей истории.

Теперь давайте поговорим о контракте Readable, также известном как Readable Stream. В node.js потоки являются экземплярами EventEmitter. Это означает, что большая часть логики и поведения, которые вы видите в потоках (например, в читаемом потоке), происходит из класса EventEmitter. Итак, каждый поток является источником событий.

Теперь контракт Readable (поток) использует контракт EventEmitter, чтобы информировать, когда есть данные, доступные для чтения (событие data), когда больше нет данных для чтения (событие end) или когда возникает ошибка (событие error).

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

Какие антипаттерны (или примеры плохого стиля программирования) вы можете привести для Node.js?

Ад обратных вызовов и пирамида гибели. Это происходит, когда разработчики вкладывают несколько обратных вызовов, в результате чего код становится трудным для чтения и отладки. Чтобы исправить это, вы можете использовать Promises или async/await.

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

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

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

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

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

Не используется соответствующее ведение журнала. Правильное ведение журнала важно для эффективной отладки и отслеживания потока кода, особенно в рабочей среде.

Подробнее о логировании далее

Использование неопределенной переменной в качестве объекта. Это распространенная проблема JavaScript. Обязательно проверьте, что объект определен, прежде чем пытаться получить доступ к его свойствам, чтобы предотвратить «неопределенные» ошибки.

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

Зачем нам нужны следующие поля ошибок: error.cause, error.code, error.message и error.stack?

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

  1. error.cause — Это поле обычно содержит другой объект ошибки, который фактически вызвал рассматриваемую ошибку. Это позволяет вам отследить причину ошибки и помогает узнать основную проблему, вызвавшую ее.
  2. error.code — Коды ошибок — это уникальные значения, присвоенные разным типам ошибок. Они предоставляют возможность программно различать различные типы ошибок.
  3. error.message — Сообщение об ошибке обычно содержит краткое описание ошибки. Это можно использовать для регистрации ошибки или информирования пользователей о том, что пошло не так.
  4. error.stack — это строка, описывающая точку кода, в которой возникла ошибка, и имеет форму трассировки стека. Он предоставляет последовательность вызовов функций, которые привели к возникновению ошибки, что может быть очень полезно для отладки.

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

Как скопировать папку с вложенными файлами и папками с помощью узла: fs?

const fs = require('fs').promises;
const path = require('path');

async function copyFolderRecursiveAsync(source, target) {
  // Check if source folder exists, else throw error.
  // fs.stat() is used to get statistical information about a file.
  const exists = await fs.stat(source).catch(() => false);
  if (!exists) {
    throw new Error(`Source folder does not exist: ${source}`);
  }
  // Check if source is a directory, else throw error
  if (!exists.isDirectory()) {
    throw new Error(`Provided source is not a folder: ${source}`);
  }

  // Append source folder name to target path
  let targetFolder = path.join(target, path.basename(source));

  // Check if target folder exists; if not, create it
  try {
    // fs.access() is used to test user's permissions of a given target (in this case a directory)
    await fs.access(targetFolder); 
  } catch {
    // fs.mkdir() is used to create a new directory
    await fs.mkdir(targetFolder, { recursive: true }); 
  }

  // Read the files/directories from source directory
  const files = await fs.readdir(source);

  // Iterate over each file/directory
  for (let file of files) {
    // Get full path for current file/directory
    const curSource = path.join(source, file);

    // Check if current item is a directory
    if ((await fs.stat(curSource)).isDirectory()) {
      // If it is a directory, we recursively called the function to copy contents
      // from this directory to corresponding directory in target
      await copyFolderRecursiveAsync(curSource, targetFolder);
    } else {
      // If it is a file, copy the file from source to target folder
      // fs.copyFile() is used to copy a file from source to destination
      await fs.copyFile(curSource, path.join(targetFolder, file)); 
    }
  }
}

Можем ли мы создавать приложения реального времени на Node.js?

Node.js предоставляет отличную среду выполнения для разработки приложений реального времени благодаря таким функциям, как неблокирующие операции ввода-вывода и постоянные соединения в WebSockets.

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

Базовым примером связи в реальном времени является сокет TCP (net):

const net = require('net');

const server = net.createServer((socket) => {
  socket.on('data', (data) => {
    console.log('Data received from client: ', data.toString());
    // Echo back to the client
    socket.write(data);
  });

  socket.on('end', () => {
    console.log('Closing connection with the client');
  });
});

server.listen(8080, () => {
  console.log('Server is listening on port 8080');
});

Каковы подходы к логированию? Их отличия, плюсы и минусы.

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

Ведение журнала консоли

Это простейшая форма ведения журнала, в которой используются console.log(), console.error(), console.info() и т. д.

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

Регистрация файлов

Теперь вы записываете данные в файл. Для этой цели можно использовать такие библиотеки, как winston или bunyan.

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

Структурированное ведение журнала

Всякий раз, когда данные находятся в файле(ах), вам необходима гибкая структура для выполнения всех видов действий с вашими журналами, поиска, анализа и т. д.

  • Плюсы: это обеспечивает лучшую возможность поиска и масштабируемость. Если журналы уже находятся в структурированном формате, дальнейшая обработка (агрегация, аналитика и т. д.) может выполняться более эффективно.
  • Минусы: это может повлечь за собой более высокие затраты на хранение и требует особого внимания для просмотра журналов, поскольку простое текстовое отображение может быть не идеальным.

Удаленная регистрация

При таком подходе журналы отправляются на удаленный сервер.

  • Плюсы: это предотвращает потерю журналов в случае сбоев системы и позволяет использовать мощные инструменты для запросов и анализа. Такие инструменты, как Loggly, Papertrail или Graylog, могут предоставлять расширенные функции, такие как текстовый поиск, фильтрация, оповещение и т. д.
  • Минусы: это может быть медленнее из-за задержки в сети и требует настройки и управления удаленным сервером или службой.

Распределенная трассировка

Это предполагает отслеживание и регистрацию пути запроса через распределенную систему.

  • Плюсы: это помогает понять путь сообщения или запроса через различные микросервисы. Такие инструменты, как OpenTracing, Jaeger или Zipkin, могут помочь визуализировать этот поток, помогая точно определить службу, вызывающую проблему.
  • Минусы: это требует тщательной настройки и интеграции и увеличивает сложность. Это также может повлиять на производительность приложения из-за дополнительных накладных расходов.

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

Вы всегда должны помнить не только о сторонних инструментах, но и о сторонних системах, которые будут использовать журналы ваших приложений. Это может быть AWS Cloudwatch, Datadog, Loggly, Splunk или даже надежный стек ELK. Следовательно, выбор кодовой реализации ведения журнала будет неизбежно связан с тем, как вы используете журналы.

Где хранить секреты? (ключи API, токены и пароли базы данных)

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

Файлы .env используются для установки переменных среды. Пакет dotenvв Node.js дает вам возможность загружать переменные из файла .env в процесс.env. Будьте осторожны и не сохраняйте файлы .env в своих системах контроля версий, поскольку это приводит к потенциальным угрозам безопасности.

Безопасные системы управления секретами. Такие системы, как хранилище параметров AWS, Vault от HashiCorp или Azure Key Vault, специально предназначены для безопасного хранения цифровых секретов и управления ими. Они предлагают надежный, безопасный и масштабируемый способ управления секретами.

Зашифрованные файлы. Хотя это и менее распространено, секреты также могут храниться в зашифрованном файле в вашем репозитории кода и расшифровываться во время развертывания. Например, для этой цели можно использовать набор инструментов GPG.

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

Почему необходимо возвращать await внутри асинхронных функций и методов, а не возвращать промис?

Всегда следует return await при возврате обещания использовать полную трассировку стека ошибок. Если функция возвращает обещание, эта функция должна быть объявлена ​​как async функция и явно await обещание перед ее возвратом.

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

Как не заблокировать обслуживание других пользователей при обработке запроса от одного из них?

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

Асинхронное программирование

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

Порождение процесса

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

Кластеризация

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

Очередь и кэширование

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

Оптимизация базы данных

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

Помните, что ключевая концепция заключается в том, чтобы избежать блокировки единственного потока, в котором работает Node.js, и обеспечить ему возможность как можно быстрее отвечать на другие запросы.

Дополнительные ресурсы

Вопросы, затронутые здесь, взяты отсюда. Некоторые ответы были даны с использованием репозитория Лучшие практики Node.js на GitHub.

Ознакомьтесь с Частью 1 вопросов для собеседования ✌️