Skip to content

Latest commit

 

History

History
2221 lines (1499 loc) · 141 KB

full_book.md

File metadata and controls

2221 lines (1499 loc) · 141 KB

Node Hero

RisingStack: Node Hero
Перевод Андрея Мелихова
Редактор-корректор Роман Пономарёв

Содержание

  1. Начало работы с Node.js
  2. Использование NPM
  3. Понимание асинхронного программирования
  4. Ваш первый сервер на Node.js
  5. Работа с базами данных
  6. Модуль Request
  7. Файловая структура проекта
  8. Аутентификация в Node.js с использованием Passport.js
  9. Модульное тестирование
  10. Отладка
  11. Безопасность
  12. Деплой вашего приложения
  13. Мониторинг Node.js-приложений

Начало работы с Node.js

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

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

Node.js в двух словах

Официальное лого Node.js

Node.js — это среда выполнения JavaScript, построенная на JavaScript-движке V8 из Chrome. В основе Node.js лежит событийно-управляемая модель с неблокирующими операциями I/O, что делает её легкой и эффективной.

Другими словами: Node.js предлагает вам возможность писать невероятно производительный серверный код с использованием JavaScript. Как говорится в официальном описании: Node.js — это среда выполнения, использующая тот же JavaScript-движок V8, который вы можете найти в браузере Google Chrome. Но этого недостаточно для успеха Node.js. В Node.js используется libuv - кросс-платформенная библиотека поддержки с акцентом на асинхронный ввод-вывод.

Официальное лого libuv

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

Установка Node.js для старта

Последнюю версию Node.js вы можете найти на официальном сайте: https://nodejs.org/en/download/.

При таком подходе довольно легко начать работу, но если позже вы захотите добавить в систему больше версий Node.js, лучше начать использовать nvm (node version manager) - диспетчер версий Node.js.

После его установки вы сможете использовать очень простой CLI API для смены версии Node.js:

Установка различных версий Node.js

nvm install 4.4

Затем, если вы хотите проверить в работе экспериментальную версию:

nvm install 5

Чтобы убедиться, что у вас установлена и запущена Node.js, выполните:

node --version

Если все в порядке, эта команда вернет номер версии текущего активного бинарного файла Node.js.

Использование нескольких версий Node.js

Если вы работаете над проектом, поддерживающим Node.js v4, вы можете начать использовать эту версию с помощью следующей команды:

nvm use 4

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

nvm use 5

Хорошо, теперь мы знаем, как устанавливать Node.js и переключаться между её версиями, но в чём смысл?

С тех пор как был сформирован Node.js Foundation, Node.js имеет план релизов. Это очень похоже на другие проекты Linux Foundation. Это означает, что есть два релиза: стабильный и экспериментальный. В Node.js стабильными версиями с долговременной поддержкой (LTS) являются те, которые начинаются с четных чисел (4, 6, 8, ...). Экспериментальные версии нумеруются нечетными числами (5, 7, ...).

Мы рекомендуем использовать версию LTS в продакшене и пробовать новые возможности с экспериментальной версией.

Если вы используете Windows, здесь можно скачать альтернативу для nvm: nvm-windows.

Hello World

Чтобы начать работу с Node.js, давайте попробуем её в консоли! Запустите Node.js, просто набрав node:

$ node
>

Хорошо, давайте попробуем что-то напечатать:

$ node
> console.log('hello from Node.js')

После нажатия Enter вы получите следующее:

> console.log('hello from Node.js')
hello from Node.js
undefined

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


Пришло время создать наше приложение Hello Node.js!

Начнем с создания файла index.js. Откройте свою IDE (Atom, Sublime, Code — выбор за вами), создайте новый файл и сохраните его с именем index.js. Если вы закончили, скопируйте в него следующий фрагмент кода:

// index.js
console.log('hello from Node.js')

Чтобы запустить этот файл, вы должны снова открыть свой терминал и перейти в каталог, в котором размещён index.js.

Как только вы успешно переместитесь в нужное место, запустите файл, используя команду node index.js. Вы увидите, что эта команда будет выдавать тот же результат, что и раньше, выводя строку непосредственно в терминале.

Модульность для вашего приложения

Теперь у вас есть файл index.js, поэтому пришло время перейти на следующий уровень! Давайте создадим что-то более сложное, разделив наш исходный код на несколько JavaScript-файлов с целью удобочитаемости и поддерживаемости. Чтобы начать работу, вернитесь в свою IDE и создайте следующую структуру каталогов (с пустыми файлами), но пока не трогайте package.json, мы сгенерируем его автоматически на следующем шаге:

├── app
|  ├── calc.js
|  └── index.js
├── index.js
└── package.json

Каждый проект Node.js начинается с создания файла package.json. Вы можете думать о нем как о JSON-представлении приложения и его зависимостей. Он содержит имя вашего приложения, автора (вас) и все зависимости, необходимые для запуска приложения. Мы рассмотрим раздел зависимостей позже в главе «Использование NPM».

Вы можете интерактивно генерировать файл package.json с помощью команды npm init в терминале. После запуска команды вас попросят ввести некоторые данные, например имя вашего приложения, версию, описание и так далее. Не нужно беспокоиться, просто нажимайте Enter, пока не получите сформированный JSON и вопрос is it ok?. Нажмите Enter в последний раз и вуаля: ваш package.json был автоматически сгенерирован и помещен в папку вашего приложения. Если вы откроете этот файл в своей IDE, он будет очень похож на фрагмент кода ниже.

// package.json
{
  "name": "@risingstack/node-hero",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "", "license": "ISC"
}

Хорошей практикой является добавление стартового скрипта в ваш пакет package.json. Как только вы это сделаете, как показано в примере выше, вы можете запустить приложение с помощью команды npm start. Это очень удобно, когда вы хотите развернуть свое приложение у PaaS-провайдера: они могут распознать команду start и использовать её для запуска приложения.

Теперь давайте вернемся к первому созданному вами файлу под названием index.js. Я рекомендую оставить этот файл очень компактным: только подключение самого приложения (файл index.js из подкаталога /app, созданного ранее). Скопируйте следующий код в свой файл index.js и сохраните:

// index.js
require('./app/index')

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

Вставьте этот код в файл index.js в каталоге /app.

// app/index.js
const calc = require('./calc')
const numbersToAdd = [
    3,
    4,
    10,
    2
]

const result = calc.sum(numbersToAdd)
console.log(`The result is: ${result}`)

Теперь вставьте фактическую бизнес-логику в файл calc.js, который можно найти в той же папке.

// app/calc.js
function sum (arr) {
    return arr.reduce(function(a, b) {
        return a + b
    }, 0)
}

module.exports.sum = sum

Чтобы проверить, всё ли вы сделали правильно, сохраните эти файлы, откройте терминал и введите npm start или node index.js. Если вы все сделали правильно, вы получите ответ: 19. Если что-то пошло не так, внимательно просмотрите лог в консоли и найдите проблему на его основе.


В следующей главе под названием «Использование NPM» мы рассмотрим, как использовать NPM - менеджер пакетов для JavaScript.

Использование NPM

В этой главе вы узнаете, что такое NPM и как его использовать. Давайте приступим к изучению!

NPM в двух словах

NPM — это менеджер пакетов, используемый Node.js-приложениями. В нём вы можете найти массу готовых модулей, поэтому вам не нужно изобретать колесо. Это похоже на Maven для Java или Composer для PHP. Существует два основных интерфейса, с которыми вы будете взаимодействовать: сайт NPM и набор инструментов командной строки (CLI).

И веб-сайт, и CLI используют один и тот же реестр, чтобы искать и отображать модули.

Вебсайт

Сайт NPM можно найти по адресу https://npmjs.com. Здесь вы можете зарегистрироваться как новый пользователь или поискать нужные пакеты.

Интерфейс командной строки

Чтобы запустить CLI, достаточно написать:

npm

Обратите внимание, что NPM поставляется вместе с бинарным файлом Node.js, поэтому вам не нужно его устанавливать, однако если вы хотите использовать определенную версию NPM, вы можете его обновить. Если вы хотите установить NPM версии 3, вы можете сделать это с помощью: npm install npm@3 -g.

Использование NPM: Учебное пособие

Вы уже встречались с NPM в предыдущей главе, когда создавали файл package.json. Давайте расширим наши знания!

Добавление зависимостей

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

Когда у вас есть файл package.json, вы можете добавить зависимости к вашему приложению. Давайте добавим одну! Попробуйте следующее:

npm install lodash --save

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

Это может быть хорошей отправной точкой для вашего .gitignore (ссылка на GitHub).


Давайте посмотрим, что происходит в файле package.json! Появилось новое свойство, называемое dependencies:

"dependencies": {
  "lodash": "4.6.1"
}

Это означает, что lodash версии 4.6.1 теперь установлен и готов к использованию. Обратите внимание, что NPM следует правилам SemVer для версионирования пакетов.

Используя нумерацию версий вида MAJOR.MINOR.PATCH, повышайте MAJOR-версию, когда вы делаете несовместимые изменения API, MINOR-версию, когда вы добавляете функциональность обратно-совместимым образом, и PATCH-версию, когда вы делаете исправления ошибок с обратной совместимостью. Для получения дополнительной информации: http://semver.org/

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

// index.js
const _ = require('lodash')

_.assign({ 'a': 1 }, { 'b': 2 }, { 'c': 3 });
// → { 'a': 1, 'b': 2, 'c': 3 }

Добавление зависимостей для разработки

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

Когда вы собираете веб-приложения, вам может потребоваться минимизировать ваши JavaScript-файлы, объединить CSS-файлы и так далее. Модули, которые это сделают, будут выполняться только во время создания ресурсов, поэтому работающее приложение не нуждается в них.

Вы можете установить такие скрипты с помощью:

npm install mocha --save-dev

После этого в вашем файле package.json появится новый раздел, называемый devDependencies. Все модули, которые вы устанавливаете с помощью --save-dev, будут описаны там, а также они будут помещены в тот же самый каталог node_modules.

NPM-скрипты

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

Наиболее распространенными являются скрипты start и test. С помощью start вы можете описать, как нужно запускать приложение, а test используется для запуска тестов. В вашем package.json они могут выглядеть примерно так:

"scripts": {
  "start": "node index.js",
  "test": "mocha test",
  "your-custom-script": "echo npm"
}

Что следует здесь отметить:

  • start: просто описывает начальную точку для запуска вашего приложения, этот скрипт можно вызвать с помощью npm start.
  • test: цель этого скрипта заключается в том, чтобы запускать ваши тесты: одно из удобств такого запуска тестов заключается в том, что в этом случае mocha не нужно устанавливать глобально, так как NPM будет искать её в папке node_modules/.bin, а mocha будет размещена там же. Запуск тестов может быть вызван с помощью npm test.
  • your-custom-script: всё, что вы захотите (вы можете выбрать любое имя). Его можно вызвать с помощью npm run your-custom-script — не забывайте про run в этом случае!

Ограничение области видимости и приватные пакеты

Первоначально у NPM было глобальное пространство имен для названий модулей, и с более чем 250 000 модулями в реестре большинство простых имен уже заняты. Кроме того, глобальное пространство имен содержит только общедоступные модули.

В NPM урегулировали эту проблему внедрением пакетов с ограниченной областью видимости (scoped packages), они имеют следующий шаблон именования:

@myorg/mypackage

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

npm install @myorg/mypackage --save-dev

Они будут отображаться в вашем package.json следующим образом:

"dependencies": {
  "@myorg/mypackage": "^1.0.0"
}

Подключения пакетов с ограниченной областью видимости работает должным образом:

require('@myorg/mypackage')

Для получения большей информации обратитесь к документации NPM-модулей с ограниченной областью видимости.

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

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

Синхронное программирование

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

try(FileInputStream inputStream = new FileInputStream("foo.txt")) {
    Session IOUtils;
    String  fileContent = IOUtils.toString(inputStream);
}

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

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

(Красные полосы отображают промежутки времени, в которые процесс ожидает ответа от внешнего ресурса и блокируется, чёрные полосы показывают, когда ваш код работает, зелёные полосы отображают остальную часть приложения)

Для решения этой проблемы Node.js предлагает модель асинхронного программирования.

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

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

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

Начнем с простого примера: синхронное чтение файла с использованием Node.js:

const fs = require('fs')
let content
try {
    content = fs.readFileSync('file.md', 'utf-8')
} catch (ex) {
    console.log(ex)
}
console.log(content)

Что здесь происходит? Мы читаем файл, используя синхронный интерфейс модуля fs. Он работает ожидаемым образом: в переменную content сохраняется содержимое file.md. Проблема с этим подходом заключается в том, что Node.js будет заблокирована до завершения операции, то есть, пока читается файл, она не может сделать ничего полезного.

Посмотрим, как мы можем это исправить!

Асинхронное программирование, в том виде, в каком мы знаем его в JavaScript, может быть реализовано только при условии, что функции являются объектами первого класса: они могут передаваться как любые другие переменные другим функциям. Функции, которые могут принимать другие функции в качестве аргументов, называются функциями высшего порядка.

Один из самых простых примеров функций высшего порядка:

const numbers = [2,4,1,5,4]
function isBiggerThanTwo (num) {
    return num > 2
}
numbers.filter(isBiggerThanTwo)

В приведенном выше примере мы передаем функцию isBiggerThanTwo в функцию filter. Таким образом, мы можем определить логику фильтрации.

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

В основе Node.js лежит принцип «первым аргументом в колбеке должна быть ошибка». Его придерживаются базовые модули, а также большинство модулей, найденных в NPM.

const fs = require('fs')
fs.readFile('file.md', 'utf-8', function (err, content) {
    if (err) {
        return console.log(err)
    }

    console.log(content)
})

Что следует здесь выделить:

  • обработка ошибок: вместо блока try-catch вы проверяете ошибку в колбеке
  • отсутствует возвращаемое значение: асинхронные функции не возвращают значения, но значения будут переданы в колбеки

Давайте немного изменим этот файл, чтобы увидеть, как это работает на практике:

const fs = require('fs')

console.log('start reading a file...')

fs.readFile('file.md', 'utf-8', function (err, content) {
    if (err) {
        console.log('error happened during reading the file')
        return console.log(err)
    }
    console.log(content)
})

console.log('end of the file')

Результатом выполнения этого кода будет:

start reading a file...
end of the file
error happened during reading the file

Как вы можете видеть, как только мы начали читать наш файл, выполнение кода продолжилось, а приложение вывело end of the file. Наш колбек вызвался только после завершения чтения файла. Как такое возможно? Встречайте цикл событий (event loop).

Цикл событий

Цикл событий лежит в основе Node.js и JavaScript и отвечает за планирование асинхронных операций.

Прежде чем погрузиться глубже, давайте убедимся, что мы понимаем, что такое программирование с управлением по событиям (event-driven programming).

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

На практике это означает, что приложения реагируют на события.

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

Для более глубокого понимания работы цикла событий рекомендуем посмотреть это видео:

https://www.youtube.com/watch?v=8cV4ZvHXQL4

Асинхронный поток управления

Поскольку теперь у вас есть общее представление о том, как работает асинхронное программирование в JavaScript, давайте рассмотрим несколько примеров того, как вы можете организовать свой код.

Async.js

Чтобы избежать так называемого Callback-Hell, вы можете начать использовать async.js.

Async.js помогает структурировать ваши приложения и упрощает понимание потока управления.

Давайте рассмотрим короткий пример использования Async.js, а затем перепишем его с помощью промисов.

Следующий фрагмент перебирает три файла и выводит системную информацию по каждому:

async.parallel(['file1', 'file2', 'file3'],
    fs.stat,
    function (err, results) {
        // results теперь содержит массив системных данных для каждого файла
})

Примечание переводчика: если вы пользуетесь Node.js версии 7 и выше, лучше воспользоваться встроенными конструкциями языка, такими как async/await.

Промисы

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

На практике предыдущий пример можно переписать следующим образом:

function stats (file) {
    return new Promise((resolve, reject) => {
        fs.stat(file, (err, data) => {
            if (err) {
                return reject (err)
            }
            resolve(data)
        })
    })
}

Promise.all([
    stats('file1'),
    stats('file2'),
    stats('file3')
])
.then((data) => console.log(data))
.catch((err) => console.log(err))

Конечно, если вы используете метод, возвращающий промис, то пример будет заметно компактнее.


В следующей главе вы узнаете, как запустить ваш первый Node.js HTTP-сервер.

Ваш первый сервер на Node.js

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

Модуль http для вашего Node.js-сервера

Когда вы начинаете создавать HTTP-приложения в Node.js, встроенные модули http/https - это то, с чем вы будете взаимодействовать.

Давайте создадим ваш первый HTTP-сервер на Node.js! Нам будет нужно подключить модуль http и привязать наш сервер к порту 3000.

// содежимое index.js
const http = require('http')
const port = 3000

const requestHandler = (request, response) => {
    console.log(request.url)
    response.end('Hello Node.js Server!')
}

const server = http.createServer(requestHandler)

server.listen(port, (err) => {
    if (err) {
        return console.log('something bad happened', err)
    }

    console.log(`server is listening on ${port}`)
})

Затем запускаем этот скрипт:

$ node index.js

Что нужно здесь отметить:

  • requestHandler: эта функция будет вызываться каждый раз, когда на сервер придёт запрос. Если вы откроете в своём браузере адрес localhost:3000, два сообщения появятся в консоли: одно для / и одно для favicon.ico.
  • if (err): обработка ошибок: если порт уже занят или есть какие-то другие причины, по которым сервер не может быть запущен, мы получим уведомление об этом.

Модуль http крайне низкоуровневый: создание сложного веб-приложения с использованием вышеприведенного фрагмента кода очень трудоемко. Именно по этой причине мы обычно выбираем фреймворки для работы над нашими проектами. Есть множество фреймворков, вот самые популярные:

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

Express

Быстрый, гибкий, минималистичный веб-фреймворк для Node.js — http://expressjs.com/

Добавление Express в ваш проект - это просто установка через NPM:

$ npm install express --save

После того, как вы установили Express, давайте посмотрим, как создать приложение аналогичное тому, что мы написали ранее:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (request, response) => {
    response.send('Hello from Express!')
})

app.listen(port, (err) => {
    if (err) {
        return console.log('something bad happened', err)
    }

    console.log(`server is listening on ${port}`)
})

Самое большое различие, которое вы можете здесь заметить, заключается в том, что Express по умолчанию даёт вам роутер. Вам не нужно вручную разбирать URL, чтобы решить, что делать, вместо этого вы определяете маршрутизацию приложения с помощью app.get, app.post, app.put и так далее, а они уже транслируются в соответствующие HTTP-запросы.

Одна из самых мощных концепций, которую реализует Express — это паттерн Middleware.

Middleware — промежуточный обработчик

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

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

На практике вы можете сделать это следующим образом:

const express = require('express')
const app = express()

app.use((request, response, next) => {
    console.log(request.headers)
    next()
})

app.use((request, response, next) => {
    request.chance = Math.random()
    next()
})

app.get('/', (request, response) => {
    response.json({
        chance: request.chance
    })
})

app.listen(3000)

Что следует здесь отметить:

  • app.use: это то, как вы можете описать middleware. Этот метод принимает функцию с тремя параметрами, первый из которых является запросом, второй — ответом, а третий — коллбеком next. Вызов next сигнализирует Express о том, что он может переходить к следующему промежуточному обработчику.
  • Первый промежуточный обработчик только логирует заголовки и мгновенно вызывает следующий.
  • Второй добавляет дополнительное свойство к запросу - это одна из самых мощных функций шаблона middleware. Ваши промежуточные обработчики могут добавлять дополнительные данные к объекту запроса, который могут считывать/изменять middleware, расположенные ниже.

Обработка ошибок

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

const express = require('express')
const app = express()

app.get('/', (request, response) => {
    throw new Error('oops')
})

app.use((err, request, response, next) => {
    // логирование ошибки, пока просто console.log
    console.log(err)
    response.status(500).send('Something broke!')
})

Что следует здесь отметить:

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

Рендеринг HTML

Ранее мы рассмотрели, как отправлять JSON-ответы. Пришло время узнать, как отрендерить HTML простым способом. Для этого мы собираемся использовать пакет handlebars с обёрткой express-handlebars.

Сначала создадим следующую структуру каталогов:

├── index.js
└── views
    ├── home.hbs
    └── layouts
        └── main.hbs

После этого заполните index.js следующим кодом:

// index.js
const path = require('path')
const express = require('express')
const exphbs = require('express-handlebars')

app.engine('.hbs', exphbs({
    defaultLayout: 'main',
    extname: '.hbs',
    layoutsDir: path.join(__dirname, 'views/layouts')
}))
app.set('view engine', '.hbs')
app.set('views', path.join(__dirname, 'views'))

Приведенный выше код инициализирует движок handlebars и устанавливает каталог шаблонов в views/layouts. Это каталог, в котором будут храниться ваши шаблоны.

После того, как вы сделали эту настройку, вы можете поместить свой начальный html в main.hbs. Чтобы всё было проще, давайте сразу перейдём к этому:

<html>
    <head>
        <title>Express handlebars</title>
    </head>
    <body>
        {{{body}}}
    </body>
</html>

Вы можете заметить метку {{{body}}} — здесь будет размещен ваш контент. Давайте создадим home.hbs!

<h2>Hello {{name}}<h2>

Последнее, что мы должны сделать, чтобы заставить всё это работать, - добавить обработчик маршрута в наше приложение Express:

app.get('/', (request, response) => {
    response.render('home', {
        name: 'John'
    })
})

Метод render принимает два параметра:

  • Первый — это имя шаблона.
  • Второй — данные, необходимые для рендеринга.

Как только вы сделаете запрос по этому адресу, вы получите что-то вроде этого:

<html>
    <head>
        <title>Express handlebars</title>
    </head>
    <body>
        <h2>Hello John</h2>
    </body>
</html>

Это всего лишь верхушка айсберга. Чтобы узнать, как добавить больше шаблонов (и даже частичных), обратитесь к официальной документации express-handlebars.

Отладка Express

В некоторых случаях вам может потребоваться выяснить, что происходит с Express, когда приложение работает. Для этого вы можете передать следующую переменную окружения в Express: DEBUG=express*.

Вы должны запустить свой Node.js HTTP-сервер, используя:

$ DEBUG=express* node index.js

Резюме

Вот как вы можете настроить свой первый HTTP-сервер на Node.js с нуля. Я рекомендую Express для начала, а затем поэкспериментируйте.


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

Работа с базами данных

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

Хранение данных в глобальной переменной

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

Возьмём простой пример: регистрация пользователя. Вы можете отдавать пользовательский контент для отдельных пользователей или сделать его доступным для них только после идентификации.

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

const users = []
app.post('/users', function (req, res) {
  // извлекаем данные пользователя из тела запроса
  const user = req.body
  users.push({
    name: user.name,
    age: user.age
  })
  res.send('successfully registered')
})

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

Использование этого метода может быть проблематичным по нескольким причинам:

  • оперативная память дорогая
  • память очищается каждый раз, когда вы перезапускаете ваше приложение
  • если вы не будете чистить память, иногда вы можете получить переполнение памяти

Хранение данных в файле

Следующее решение, которое может прийти вам в голову, это хранить данные в файлах.

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

На практике этот метод выглядит следующим образом:

const fs = require('fs')

app.post('/users', function (req, res) {
  const user = req.body
  fs.appendToFile('users.txt', JSON.stringify({name: user.name, age: user.age }), (err) => {  
    res.send('successfully registered')
  })
})

Таким образом, мы не потеряем пользовательские данные даже после перезагрузки сервера. Это решение также экономически выгодно, так как увеличение дискового пространства дешевле, чем покупка ОЗУ.

К сожалению, хранение пользовательских данных таким образом все ещё имеет несколько недостатков:

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

Здесь выходят на сцену настоящие базы данных.

Возможно, вы уже слышали, что существуют два основных типа баз данных: SQL и NoSQL.

SQL

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

Сами данные хранятся в таблицах. Каждая добавленная часть будет представлена в виде строки в таблице, как в Google Sheets или Microsoft Excel.

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

NoSQL

С другой стороны, в последнее десятилетие стали весьма популярны NoSQL базы данных. С NoSQL вам не нужно определять схему и вы можете хранить любой произвольный JSON. Это хорошо сочетается с JavaScript, потому что мы можем легко превратить любой объект в JSON. Будьте осторожны, потому что вы никогда не можете гарантировать, что данные консистентны, и вы никогда не сможете узнать, какая структура находится в базе данных.

Node.js и MongoDB

Существует распространённое заблуждение о Node.js, которое можно услышать довольно часто:

«Node.js можно использовать только с MongoDB (самая популярная NoSQL база данных)».

По моему опыту, это не так. У большинства баз данных имеются драйверы для Node.js и библиотеки в NPM. По моему мнению, они такие же простые и лёгкие в использовании, как MongoDB.

Node.js и PostgreSQL

Для простоты мы будем использовать SQL в следующем примере. Мой выбор — PostgreSQL.

Чтобы запустить PostgreSQL, вам необходимо установить его на свой компьютер. Если вы используете Mac, вы можете использовать Homebrew для установки PostgreSQL. В противном случае, если вы работаете в Linux, вы можете установить его с помощью своего диспетчера пакетов.

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

Если вы планируете использовать инструмент для просмотра базы данных, я бы рекомендовал утилиту для командной строки — psql. Она поставляется вместе с сервером PostgreSQL. Вот небольшая инструкция, которая пригодится, если вы начнёте использовать psql.

Если вам не нравится интерфейс командной строки, вы можете использовать pgAdmin, который является инструментом с открытым исходным кодом и предназначен для администрирования PostgreSQL.

Обратите внимание, что SQL — это сам по себе язык программирования. Мы не будем рассматривать все его возможности, а только наиболее простые. Если вам потребуется глубже изучить SQL, то в интернете есть много отличных онлайн-курсов, охватывающих все основы PostgreSQL.

Взаимодействие Node.js с базой данных

Во-первых, мы должны создать базу данных, которую мы будем использовать. Для этого введите следующую команду в терминал: createdb node_hero.

Затем мы должны создать таблицу для наших пользователей.

CREATE TABLE users(
  name VARCHAR(20),
  age SMALLINT
);

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

'use strict'

const pg = require('pg')
const conString = 'postgres://username:password@ localhost/node_hero' // Убедитесь, что вы указали данные от вашей базы данных

pg.connect(conString, function (err, client, done) {
  if (err) {
    return console.error('error fetching client from pool', err)
  }
  client.query('SELECT $1::varchar AS my_first_query', ['node hero'], function (err, result) {
    done()

    if (err) {
      return console.error('error happened during query', err)
    }
    console.log(result.rows[0])
    process.exit(0)
  })
})

Это был простой пример — "hello world" в PostgreSQL. Обратите внимание, что первым параметром является строка, которая является нашей SQL-командой, второй параметр представляет собой массив значений, которыми мы хотели бы параметризовать наш запрос.

Большой ошибкой с точки зрения безопасности был бы ввод данных, пришедших от пользователя, в том виде, в котором они были переданы. Приведённая выше функция client.query защищает вас от SQL-инъекций, являющихся распространённым видом атаки, когда злоумышленник пытается внедрить в запрос произвольный SQL-код. Всегда учитывайте это при создании любого приложения, в котором возможен ввод данных со стороны пользователя. Чтобы узнать больше, ознакомьтесь с нашим контрольным списком безопасности Node.js-приложений.

Примечание переводчика: обычно никто не пишет SQL-запросы руками, вместо этого используют так называемые конструкторы запросов (query builder), например sequelize и knex.

Давайте продолжим наш предыдущий пример.

app.post('/users', function (req, res, next) {
  const user = req.body

  pg.connect(conString, function (err, client, done) {
    if (err) {
      // Передача ошибки в обработчик express
      return next(err)
    }
    client.query('INSERT INTO users (name, age) VALUES ($1, $2);', [user.name, user.age], function (err, result) {
      done() // Этот коллбек сигнализирует драйверу pg, что соединение может быть закрыто или возвращено в пул соединений
      if (err) {
        // Передача ошибки в обработчик express
        return next(err)
      }
      res.send(200)
    })
  })
})

Достижение разблокировано: пользователь сохранён в базе данных! :) Теперь давайте попробуем прочитать эти данные. Затем добавим в наше приложение новый роут для поиска пользователей.

app.get('/users', function (req, res, next {
  pg.connect(conString, function (err, client, done) {
    if (err) {
      // Передача ошибки в обработчик express
      return next(err)
    }
    client.query('SELECT name, age FROM users;', [], function (err, result) {
      done()
      if (err) {
        // Передача ошибки в обработчик express
        return next(err)
      }
      res.json(result.rows)
    })
  })
})

Это было не так сложно, не так ли?

Теперь вы можете запустить любой сложный SQL-запрос, который вы только сможете вообразить, в вашем Node.js-приложении.

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

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


Пробуйте всё и экспериментируйте, потому что это лучший способ стать настоящим героем Node.js! Практикуйтесь и будьте готовы к следующей главе о том, как общаться со сторонними API!

Модуль Request

В следующей главе вы узнаете основы HTTP и как вы можете получать ресурсы из внешних источников с помощью модуля Node.js request.

Что такое HTTP?

HTTP - это протокол передачи гипертекста. HTTP функционирует как протокол запроса-ответа в модели клиент-сервер.

Коды состояния HTTP

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

  • 1xx — Информационный
  • 2xx — Успех: Эти коды состояния говорят о том, что наш запрос был получен и обработан правильно. Наиболее распространённые коды успеха - 200 OK, 201 Created и 204 No Content.
  • 3xx — Редирект: Эта группа кодов показывает, что клиент должен будет выполнить дополнительные действия для завершения запроса. Наиболее распространёнными кодами перенаправления являются 301 Moved Permanently, 304 Not Modified.
  • 4xx — Ошибка клиента. Этот класс кодов состояния используется, когда запрос, отправленный клиентом, содержит какую-то ошибку. Ответ сервера обычно содержит объяснение ошибки. Наиболее распространёнными кодами ошибок клиента являются 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict.
  • 5xx - Ошибка сервера: эти коды отправляются, когда сервер не смог выполнить корректный запрос из-за какой-то ошибки. Причиной может быть ошибка в коде или некоторая временная или постоянная неисправность. Наиболее распространённые коды ошибок сервера: 500 Internal Server Error, 503 Service Unavailable.

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

Отправка запросов внешним API

Подключение к внешним API-интерфейсам — простая задача в Node. Вы можете просто подключить базовый модуль HTTP и начать отправку запросов.

Конечно, есть способы обращения к внешним API намного лучше. В NPM вы можете найти несколько модулей, которые облегчат вам этот процесс. Например, двумя наиболее популярными являются модули request и superagent.

Оба этих модуля имеют интерфейс, построенный на колбеках, который может привести к проблемам (я уверен, вы слышали о Callback-Hell), но, к счастью, у нас есть доступ к версиям, обёрнутым в промисы.

Использование модуля Request

Использование модуля request-promise — это очень просто. После установки из NPM вам нужно только подключить его к программе:

const request = require('request-promise')

Отправка GET-запроса:

const options = {
    method: 'GET',
    uri: 'https://risingstack.com'
}

request(options)
    .then(function (response) {
        // Запрос был успешным, используйте объект ответа как хотите
    })
    .catch(function (err) {
        // Произошло что-то плохое, обработка ошибки
    })

Если вы вызываете JSON API, вам может потребоваться, чтобы request-promise автоматически распарсил ответ. В этом случае просто добавьте это в параметры запроса:

json: true

POST-запросы работают аналогичным образом:

const options = {
    method: 'POST',
    uri: 'https://risingstack.com/login',
    body: {
       foo: 'bar'
    },
    json: true
    // Тело запроса приводится к формату JSON автоматически
}

request(options)
    .then(function (response) {
        // Обработка ответа
    })
    .catch(function (err) {
        // Работа с ошибкой
    })

Чтобы добавить параметры строки запроса, вам просто нужно добавить свойство qs к объекту options:

const options = {
    method: 'GET',
    uri: 'https://risingstack.com',
    qs: {
        limit: 10,
        skip: 20,
        sort: 'asc'
    }
}

Этот код соберёт следующий URL для запроса: https://risingstack.com?limit=10&skip=20&sort=asc. Вы также можете назначить любой заголовок так же, как мы добавили параметры запроса:

const options = {
    method: 'GET',
    uri: 'https://risingstack.com',
    headers: {
        'User-Agent': 'Request-Promise',
        'Authorization': 'Basic QWxhZGRpbjpPcGVuU2VzYW1l'
    }
}

Обработка ошибок

Обработка ошибок - это неотъемлемая часть запросов на внешние API, поскольку мы никогда не можем быть уверены в том, что с ними произойдёт. Помимо наших ошибок клиента сервер может ответить с ошибкой или просто отправить данные в неправильном или непоследовательном формате. Помните об этом, когда вы пытаетесь обработать ответ. Кроме того, использование catch для каждого запроса - хороший способ избежать сбоя на нашем сервере по вине внешнего сервиса.

Объединяем всё вместе

Поскольку вы уже узнали, как развернуть HTTP-сервер на Node.js, как отрисовать HTML и как получить данные из внешних API, пришло время собрать эти знания вместе!

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

(Чтобы получить ключ для API AccuWeather, посетите их сайт для разработчиков)

const express = require('express')
const rp = require('request-promise')
const exphbs = require('express-handlebars')
const path = require('path')
const app = express()

app.engine('.hbs', exphbs({
    defaultLayout: 'main',
    extname: '.hbs',
    layoutsDir: path.join(__dirname, 'views/layouts')
}))
app.set('view engine', '.hbs')
app.set('views', path.join(__dirname, 'views'))

app.get('/:city', (req, res) => {
    rp({
        uri: 'http://dataservice.accuweather.com/locations/v1/cities/search',
        qs: {
            q: req.params.city,
            apikey: 'api-key'
            // Используйте ваш ключ для accuweather API
                
        },
        json: true
    })
    .then((data) => {
        res.render('home', {
            res: JSON.stringify(data)
        })
    })
    .catch((err) => {
        console.log(err)
        res.render('error')
    })
})

app.listen(3000)

Пример выше делает следующее:

  • создаёт Express-сервер
  • устанавливает handlebars в качестве шаблонизатора
  • отправляет запрос к внешнему API
    • если все в порядке, то приложение отображает страницу
    • в противном случае приложение показывает страницу неудачи и регистрирует ошибку

В следующей главе Node Hero вы узнаете, какая файловая структура предпочтительней в Node.js проектах.

Файловая структура проекта

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

5 основных правил структурирования проектов

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

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

Мы сформулировали пять простых правил, которые мы применяем во время разработки на Node.js. Если вы будете им следовать, ваши проекты будут в порядке.

Правило 1: Организуйте ваши файлы вокруг функций, а не ролей

Представьте, что у вас есть следующая структура каталогов:

// Неправильно
.
├── controllers
|   ├── product.js
|   └── user.js
├── models
|   ├── product.js
|   └── user.js
├── views
|   ├── product.hbs
|   └── user.hbs

Проблемы с этим подходом:

  • Чтобы понять, как работает страница product, вам нужно открыть три разных каталога с большим количеством переключений контекста
  • В конечном итоге вы пишете длинные пути при подключении модулей: require('../../controllers/user.js')

Вместо этого вы можете структурировать Node.js-приложения вокруг функций продукта / страниц / компонентов. Это облегчает понимание:

// Правильно
.
├── product
|   ├── index.js
|   ├── product.js
|   └── product.hbs
├── user
|   ├── index.js
|   ├── user.js
|   └── user.hbs

Правило 2: Не помещайте логику в файлы index.js

Используйте эти файлы только для экспорта, например:

// product/index.js
var product = require('./product')

module.exports = {
   create: product.create
}

Правило 3: Поместите тестовые файлы рядом с реализацией

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

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

.
├── test
|   └── setup.spec.js
├── product
|   ├── index.js
|   ├── product.js
|   ├── product.spec.js
|   └── product.hbs
├── user
|   ├── index.js
|   ├── user.js
|   ├── user.spec.js
|   └── user.hbs

Правило 4: Используйте директорию config

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

├── config
|   ├── index.js
|   └── server.js
├── product
|   ├── index.js
|   ├── product.js
|   ├── product.spec.js
|   └── product.hbs

Правило 5: Положите свои большие npm-скрипты в каталог scripts

Создайте отдельный каталог для ваших дополнительных скриптов в package.json.

.
├── scripts
|   ├── syncDb.sh
|   └── provision.sh
├── product
|   ├── index.js
|   ├── product.js
|   ├── product.spec.js
|   └── product.hbs

В следующей главе Node Hero вы узнаете, как аутентифицировать пользователей, используя Passport.js.

Аутентификация в Node.js с использованием Passport.js

В этой главе вы узнаете, как реализовать стратегию локальной аутентификации в Node.js приложении с использованием Passport.js и Redis.

Технологии, которые мы будем использовать

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

Что такое Passport.js?

Простая ненавязчивая аутентификация для Node.js - passportjs.org

Passport.js - это middleware для проверки подлинности. Мы будем исполььзовать её для управления сессиями.

Что такое Redis?

Redis это опенсорс (лицензии BSD) хранилище структур данных в оперативной памяти, используемое как база данных, кэш и брокер сообщений — redis.io

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

Демонстрационное приложение

Для демонстрационных целей создадим приложение, которое умеет только следующее:

  • предоставляет форму входа
  • предоставляет две защищённые страницы:
    • страницу профиля
    • безопасные заметки

Структура проекта

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

Мы собираемся использовать следующую структуру:

├── app
|   ├── authentication
|   ├── note
|   ├── user
|   ├── index.js
|   └── layout.hbs
├── config
|   └── index.js
├── index.js
└── package.json

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

(Вы можете скачать исходный код по ссылке)

Процесс аутентификации в Node.js

Наша цель - реализовать в нашем приложении следующий процесс аутентификации:

  1. Пользователь вводит имя и пароль
  2. Приложение проверяет, являются ли они корректными
  3. Если имя и пароль корректны, приложение отправляет заголовок Set-Cookie, который будет использоваться для аутентификации дальнейших страниц
  4. Когда пользователь посещает страницы в том же домене, ранее установленный cookie добавляется ко всем запросам
  5. Аутентификация на закрытых страницах происходит с помощью этого файла cookie

Чтобы настроить такую стратегию аутентификации, выполните следующие три действия:

  1. Настройте Express
  2. Настройте Passport
  3. Добавьте защищённые разделы на сайте

Шаг 1: Настройка Express

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

// file:app/index.js
const express = require('express')
const passport = require('passport')
const session = require('express-session')
const RedisStore = require('connect-redis')(session)

const app = express()
app.use(session({
    store: new RedisStore({
        url: config.redisStore.url
    }),
    secret: config.redisStore.secret,
    resave: false,
    saveUninitialized: false
}))
app.use(passport.initialize())
app.use(passport.session())

Что мы тут делаем?

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

Для хранения сессий мы используем Redis, но вы можете использовать любые другие, такие как MySQL или MongoDB.

Шаг 2: Настройка Passport

Passport.js — отличный пример библиотеки, использующей плагины. В этом уроке мы добавляем модуль passport-local, который добавляет простую локальную стратегию аутентификации с использованием имён пользователей и паролей.

Для простоты в этом примере (см. ниже) мы не используем базу данных, вместо неё используется экземпляр объекта пользователя в памяти. В реальных приложениях findUser будет искать пользователя в базе данных.

file:app/authenticate/init.js
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy

const user = {
    username: 'test-user',
    password: 'test-password', id: 1
}
passport.use(new LocalStrategy(
    function(username, password, done) {
        findUser(username, function (err, user) {
            if (err) {
                return done(err)
            }
            if (!user) {
                return done(null, false)
            }
            if (password !== user.password ) {
                return done(null, false)
            }
            return done(null, user)
        })
    }
))

Как только findUser возвращается с нашим объектом пользователя, остаётся только сравнить введённый пользователем и реальный пароли, чтобы увидеть, есть ли совпадение.

Если они совпадают, мы разрешаем пользователю войти (возвращая объект пользователя в passport - return done(null, user)), если нет — возвращаем ошибку авторизации (путём возврата null - return done(null)).

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

Шаг 3: Добавляем защищённые разделы на сайте

Чтобы добавить защищенные разделы на сайт, мы используем шаблон middleware Express. Для этого сначала создадим middleware для аутентификации:

// file:app/user/init.js
const passport = require('passport')

app.get('/profile', passport.authenticationMiddleware(), renderProfile)

Она имеет только одну задачу: если пользователь аутентифицирован (имеет правильные cookie-файлы), она просто вызывает следующую middleware. В противном случае пользователь перенаправляется на страницу, где он может войти в систему.

Использование этой middleware также просто, как добавление любой другой middleware в определение роута.

// file:app/authentication/middleware.js
function authenticationMiddleware () {
    return function (req, res, next) {
        if (req.isAuthenticated()) {
            return next()
        }
        res.redirect('/')
    }
}

Резюме

В этом разделе учебника по Node.js вы узнали, как добавить базовую аутентификацию в ваше приложение. Позже вы можете расширить его с помощью различных стратегий аутентификации, таких как Facebook или Twitter. Вы можете найти больше стратегий по адресу http://passportjs.org/.

Полный рабочий пример нашего демонстрационного приложения вы можете найти на GitHub.


Следующая глава Node Hero будет посвящена юнит-тестированию Node.js приложений. Вы узнаете такие концепции, как юнит-тестирование, пирамида тестирования, дублёры и многое другое!

Модульное тестирование

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

Тестирование Node.js-приложений

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

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

Вы можете спросить: что я должен протестировать в своём приложении? Сколько тестов у меня должно быть?

Ответ варьируется, но, как правило, вы можете следовать рекомендациям, установленным пирамидой тестирования.

По сути, тестовая пирамида описывает, что вы должны писать модульные тесты, интеграционные тесты и e2e тесты. У вас должно быть больше интеграционных тестов, чем e2e и ещё больше модульных тестов.

Давайте посмотрим, как вы можете добавить модульные тесты для своих приложений!

Обратите внимание, что здесь мы не собираемся говорить об интеграционных и e2e тестах, поскольку они выходят далеко за рамки этого учебника.

Модульное тестирование Node.js-приложений

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

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

Анатомия модульного теста

Каждый модульный тест имеет следующую структуру:

  1. Настройка теста
  2. Вызов тестируемого метода
  3. Утверждение

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

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

Для модульного тестирования мы собираемся использовать следующие библиотеки:

  • запуск тестов: mocha, альтернативно tape
  • библиотека утверждений: chai, альтернативно assert
  • шпионы, стабы и моки: sinon (для настройки тестов).

Шпионы, стабы и моки - что использовать и когда?

Прежде чем приступить к практике модульного тестирования, давайте разберёмся, что такое шпионы, стабы (заглушки) и моки!

Шпионы

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

it('calls subscribers on publish', function () {
    var callback = sinon.spy()
    PubSub.subscribe('message', callback)

    PubSub.publishSync('message')
    assertTrue(callback.called)
})
// Пример взят из документации Sinon: http://sinonjs.org/docs/

Стабы

Стабы похожи на шпионов, но они заменяют целевую функцию. Вы можете использовать стабы для управления поведением метода, чтобы форсировать какие-то события в коде (например, выброс ошибки) или предотвратить вызовы внешних ресурсов (таких как HTTP API).

it('calls all subscribers, even if there are exceptions', function (){
    var message = 'an example message'
    var error = 'an example error message'
    var stub = sinon.stub().throws()
    var spy1 = sinon.spy()
    var spy2 = sinon.spy()

    PubSub.subscribe(message, stub)
    PubSub.subscribe(message, spy1)
    PubSub.subscribe(message, spy2)

    PubSub.publishSync(message, undefined)

    assert(spy1.called)
    assert(spy2.called)
    assert(stub.calledBefore(spy1))
})
// Пример взят из документации Sinon: http://sinonjs.org/docs/

Моки

Моки — это поддельные методы с заранее запрограммированным поведением и соглашениями.

it('calls all subscribers when exceptions happen', function () {
    var myAPI = {
        method: function () {}
    }

    var spy = sinon.spy()
    var mock = sinon.mock(myAPI)
    mock.expects("method").once().throws()

    PubSub.subscribe("message", myAPI.method)
    PubSub.subscribe("message", spy)
    PubSub.publishSync("message", undefined)

    mock.verify()
    assert(spy.calledOnce)
})
// Пример взят из документации Sinon: http://sinonjs.org/docs/

Как вы можете видеть, для моков вы должны заранее определить соглашения.

Представьте, что вы хотите протестировать следующий модуль:

const fs = require('fs')
const request = require('request')

function saveWebpage (url, filePath) {
    return getWebpage(url,  lePath)
        .then(writeFile)
}

function getWebpage (url) {
    return new Promise (function (resolve, reject) {
        request.get(url, function (err, response, body) {
            if (err) {
                return reject(err)
            }

            resolve(body)
        })
    })
}

function writeFile (fileContent) {
    let  lePath = 'page'
    return new Promise (function (resolve, reject) {
        fs.writeFile(filePath, fileContent, function (err) {
            if (err) {
                return reject(err)
            }
            resolve(filePath)
        })
    })
}

module.exports = {
    saveWebpage
}

Этот модуль делает одну вещь: он сохраняет веб-страницу (основываясь на переданном URL) в файл на локальном компьютере. Чтобы протестировать этот модуль, мы должны «застабить» как модуль fs, так и модуль request.

Прежде чем начинать писать модульные тесты, в RisingStack мы обычно добавляем файл test-setup.spec.js для создания базовой настройки тестов, например создания песочниц Sinon. Это избавит вас от написания sinon.sandbox.create() и sinon.sandbox.restore() после каждого теста.

// test-setup.spec.js
const sinon = require('sinon')
const chai = require('chai')

beforeEach(function () {
    this.sandbox = sinon.sandbox.create()
})

afterEach(function () {
    this.sandbox.restore()
})

Кроме того, обратите внимание, что мы всегда ставим файлы тестов рядом с реализацией и даём им имя вида .spec.js. В нашем package.json вы можете найти следующие строки:

{
    "test-unit": "NODE_ENV=test mocha '/**/*.spec.js'",
}

Как только у нас появились эти настройки, пришло время написать сами тесты!

const fs = require('fs')
const request = require('request')

const expect = require('chai').expect

const webpage = require('./webpage')

describe('The webpage module', function () {
    it('saves the content', function * () {
        const url = 'google.com'
        const content = '<h1>title</h1>'
        const writeFileStub = this.sandbox.stub(fs, 'writeFile', function ( filePath, fileContent, cb) {
            cb(null)
        })

        const requestStub = this.sandbox.stub(request, 'get', function (url, cb) {
            cb(null, null, content)
        })

        const result = yield webpage.saveWebpage(url)

        expect(writeFileStub).to.be.calledWith()
        expect(requestStub).to.be.calledWith(url)
        expect(result).to.eql('page')
    })
})

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

Покрытие кода

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

Этот отчёт будет включать следующие метрики:

  • покрытие строк кода,
  • покрытие инструкций,
  • покрытие ветвлений,
  • и покрытие функций.

В RisingStack мы используем istanbul для анализа покрытия кода. Вы должны добавить следующий скрипт к вашему package.json, чтобы использовать istanbul с mocha:

istanbul cover _mocha $(find ./lib -name \"*.spec.js\" -not -path \"./node_modules/*\")

Как только вы это сделаете, вы получите что-то вроде:

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


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

Отладка

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

Баги и дебаггинг

Термины «баг» и «дебаггинг» - часть инженерного жаргона в течение многих десятилетий. Одно из первых упоминаний о багах:

Так было со всеми моими изобретениями. Первый шаг — интуиция, приходящая, как вспышка. Затем возникают трудности: устройство отказывается работать и именно тогда проявляются «жучки» — как называют эти мелкие ошибки и трудности — и требуются месяцы пристального наблюдения, исследований и усилий, прежде чем дело дойдёт до коммерческого успеха или неудачи. Томас Эдисон

Отладка Node.js-приложений

Одним из наиболее часто используемых методов поиска проблем в приложениях Node.js является интенсивное использование console.log для отладки.

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

Давайте посмотрим на них!

Модуль debug

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

Чтобы использовать модуль debug, вы должны установить переменную среды DEBUG при запуске вашего приложения. Вы также можете использовать символ * для имён модулей. Следующая настройка будет выводить все логи, связанные с express, в стандартный поток вывода.

DEBUG=express* node app.js

Вывод в консоль будет выглядеть так:

Встроенный отладчик Node.js

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

Чтобы запустить встроенный отладчик, вы должны запустить приложение следующим образом:

node debug app.js

Как только вы это сделаете, вы увидите что-то вроде этого:

Базовое использование отладчика Node.js

Для навигации по интерфейсу вы можете использовать следующие команды:

  • c => продолжить выполнение кода
  • n => выполнить эту строку и перейти к следующей строке
  • s => войти в эту функцию
  • o => закончить выполнение функции и выйти наружу
  • repl => удалённо выполнить код

Вы можете добавить точки останова в свои приложения, вставив инструкцию debugger в код.

function add (a, b) {
    debugger
    return a + b
}

var res = add('apple', 4)

Вотчеры

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

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

watch('expression')

Чтобы получить список активных вотчеров, наберите watchers, а чтобы перестать отслеживать выражение, используйте unwatch('expression').

Подсказка для профессионалов: вы можете переключать процессы Node.js в режим отладки, отправив им команду SIGUSR1. После этого вы можете подключить отладчик с помощью debug -p <pid>.

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

Отладчик Chrome

Когда вы начинаете отладку сложных приложений, визуализация может неплохо помочь. Было бы хорошо использовать знакомый пользовательский интерфейс Chrome DevTools для отладки Node.js-приложений, не правда ли?

Хорошие новости! Протокол отладки Chrome уже портирован в Node.js и может использоваться для отладки Node.js-приложений.

Чтобы начать его использовать, вам необходимо сначала установить node-inspector:

npm install -g node-inspector

После его установки вы можете начать отладку своих приложений, запустив их следующим образом:

node-debug index.js --debug-brk

(-debug-brk приостанавливает выполнение на первой строке)

node-inspector откроет Chrome DevTools и вы можете приступить к отладке своего Node.js-приложения.

Примечание переводчика: начиная с Node.js 6, интеграция с Chrome DevTools встроена в ядро. Для отладки необходимо запустить приложение с ключом --inspect.


В следующей главе Node Hero вы узнаете, как защитить ваши Node.js-приложения.

Безопасность

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

Node.js и угрозы безопасности

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

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

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

Начало руководства по безопасности Node.js

Давайте начнём и защитим наше приложение Node.js путём правильного отношения к написанию кода, использованию утилит и эксплуатации!

Правило 1: Не используйте eval

Использование eval может сделать приложение уязвимым для атак с инъекцией кода. Старайтесь не использовать его, но если вам нужно, никогда не отправляйте непровалидированный пользовательский ввод в eval.

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

  • setInterval(String, 2)
  • setTimeout(String, 2)
  • new Function(String)

Правило 2: Всегда используйте строгий режим

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

'use strict'
delete Object.prototype
// TypeError
var obj = {
    a: 1,
    a: 2
}
// Синтаксическая ошибка

Правило 3: Обращайтесь с ошибками аккуратно

Во время различных ошибочных сценариев ваше приложение может выдавать конфиденциальные сведения о внутренней инфраструктуре, например: X-Powered-By: Express.

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

Правило 4: Используйте статический анализ вашего кода

Статический анализ кода вашего приложения может поймать много ошибок. Для этого мы предлагаем использовать ESLint с JavaScript Standard Style.

Безопасное использование ваших сервисов в продакшене

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

Правило 5: Не запускайте свои процессы с правами суперпользователя

К сожалению, мы это встречаем довольно часто: разработчики запускают своё Node.js-приложение с правами суперпользователя, так как они хотят, чтобы приложение слушало порт 80 или 443.

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

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

Правило 6: Настройте обязательные HTTP-заголовки

Существуют определённые HTTP-заголовки, связанные с безопасностью, которые должен установить ваш сайт. Эти заголовки:

  • Strict-Transport-Security обеспечивает безопасные подключения к серверу (HTTP через SSL/TLS)
  • X-Frame-Options обеспечивает защиту от кликджекинга
  • X-XSS-Protection включает XSS-фильтр, встроенный в большинство современных веб-браузеров
  • X-Content-Type-Options содержит инструкции по определению типа файла по content-type и не допускает MIME-сниффинг контента
  • Content-Security-Policy предотвращает широкий спектр атак, включая межсайтовый скриптинг и другие межсайтовые инъекции

В Node.js их легко установить с помощью модуля Helmet:

var express = require('express')
var helmet = require('helmet')

var app = express()
app.use(helmet())

Helmet также доступен для Koa: koa-helmet.

Правило 7: Правильное управление сессией

Для каждого cookie-файла должен быть указан следующий список флагов:

  • secure — этот атрибут говорит браузеру отправлять cookie, только если запрос отправляется через HTTPS
  • HttpOnly — этот атрибут используется для предотвращения атак, таких как межсайтовый скриптинг, поскольку он не позволяет получить доступ к файлу cookie через JavaScript

Правило 8: установка области видимости cookie-файлов

  • domain — этот атрибут используется для сравнения с доменом сервера, на котором запрашивается URL; если домен соответствует или является субдоменом, тогда будет проверен атрибут path
  • path — в дополнение к домену может быть указан путь URL, по которому действителен файл cookie; если домен и путь совпадают, cookie будет отправлен в запросе
  • expires — этот атрибут используется для установки постоянных файлов cookie, поскольку cookie не истекает, пока не будет превышена установленная дата

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

var cookieSession = require('cookie-session')
var express = require('express')

var app = express()

app.use(cookieSession({
    name: 'session',
    keys: [
        process.env.COOKIE_KEY1,
        process.env.COOKIE_KEY2
    ]
}))

app.use(function (req, res, next) {
    var n = req.session.views || 0
    req.session.views = n++
    res.end(n + ' views')
})

app.listen(3000)

(Этот пример взят из документации модуля cookie-session)

Полезные инструменты

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

Правило 9: Ищите уязвимости с Retire.js

«Всегда ищите уязвимости в своих модулях Node.js. Вы то, что вы реквайрите.»

Цель Retire.js - помочь вам определить использование версий модулей с известными уязвимостями.

Просто установите:

npm install -g retire

После этого запуск с помощью команды retire будет искать уязвимости в вашем каталоге node_modules. Также обратите внимание, что Retire.js работает не только с Node.js-модулями, но и с фронтенд-библиотеками.

Правило 10: Проводите аудит ваших модулей с помощью CLI Node Security Platform

nsp является основным интерфейсом командной строки для Node Security Platform. Он позволяет производить аудит файлов package.json или npm-shrinkwrap.json с помощью API NSP для выявления уязвимых модулей.

npm install nsp --global
# Внутри директории вашего проекта
nsp check

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

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


В следующей главе Node Hero вы узнаете, как развернуть защищённое Node.js-приложение, чтобы люди могли начать его использовать!

Деплой вашего приложения

В этой главе о деплое Node.js вы узнаете, как развернуть Node.js-приложения на сервер PaaS-провайдера (Heroku), либо с помощью Docker.

Деплой Node.js на PaaS

Поставщики платформы как услуги (Platform-as-a-Service, PaaS) могут отлично подойти для команд, которые находятся на начальном этапе разработки или создают небольшие приложения.

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

Подготовка для Heroku

Для развертывания в Heroku нам нужно отправить код в удалённый git-репозиторий. Для этого добавьте свой публичный ключ в Heroku: после регистрации перейдите к своей учётной записи и сохраните его там (в качестве альтернативы вы можете сделать это с помощью CLI).

Нам также нужно будет загрузить и установить Heroku toolbelt. Чтобы убедиться, что ваша установка прошла успешно, выполните следующую команду в своём терминале:

heroku --version
heroku-toolbelt/3.40.11 (x86_64-darwin10.8.0) ruby/1.9.3

После того, как toolbelt запущен и работает, пройдите процедуру логина:

heroku login
Enter your Heroku credentials.
Email: [email protected]
Password:

(Для получения большей информации о тулките, посетите Heroku Devcenter)

Деплой на Heroku

Нажмите «Create New App», добавьте новое приложение и выберите регион. Через несколько секунд ваше приложение будет готово, и вы увидите следующий экран:

Перейдите на страницу настроек приложения и скопируйте git-url. В своём терминале добавьте удаленный url-адрес Heroku:

git remote add heroku HEROKU_URL

Вы готовы развернуть свое первое приложение в Heroku, это действительно просто git push:

git push heroku master

Как только вы это сделаете, Heroku начинает собирать ваше приложение и развернёт его. После развертывания ваш сервис будет доступен на странице https://YOUR-APP-NAME.herokuapp.com.

Heroku Add-ons

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

Чтобы опробовать работу с дополнениями, установите Trace, наше решение для мониторинга Node.js. Чтобы сделать это, найдите Add-ons на странице своего приложения и начните вводить Trace, затем кликните на ссылку для установки. Легко, правда?

(Чтобы закончить интеграцию с Trace, следуйте нашей инструкции)

Деплой Node.js-приложения с помощью Docker

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

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

Основы Docker

Чтобы начать работу с Docker, загрузите и установите его с официального веб-сайта Docker.

Помещаем Node.js-приложение в Docker

Во-первых, мы должны выучить два понятия:

  • Dockerfile: вы можете представить Dockerfile в качестве рецепта — он содержит инструкции по созданию Docker-образа
  • Docker-образ: результат выполнения Dockerfile — это исполняемый юнит

Чтобы запустить приложение внутри Docker, мы должны сначала написать Dockerfile.

Dockerfile для Node.js

В корневой папке вашего проекта создайте пустой текстовый файл Dockerfile, а затем вставьте в него следующий код:

FROM risingstack/alpine:3.3-v4.2.6-1.1.3

COPY package.json package.json
RUN npm install

# Add your source files
COPY . .
CMD ["npm", "start"]

Что можно здесь отметить:

  • FROM: описывает базовый образ, используемый для создания нового образа — в данном случае он будет получен из публичного репозитория Docker Hub
  • COPY: эта команда копирует файл package.json в Docker-образ, чтобы мы могли запускать npm install внутри
  • RUN: это команда запуска, в данном случае она запускает npm install
  • COPY снова: обратите внимание, что мы сделали копии в двух отдельных шагах. Причина в том, что Docker создаёт слои из результатов выполнения команд, поэтому, если наш package.json не меняется, он не будет вызывать npm install снова
  • CMD: Docker-образ может иметь только одну команду CMD — она определяет, какой процесс должен быть запущен образом

После того, как вы создали Dockerfile, вы можете создать из него Docker-образ:

docker build .

Используете приватные NPM-модули? Прочитайте нашу инструкцию по установке приватных NPM-модулей в Docker!

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

docker images

Для запуска образа:

docker run IMAGE_ID

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

Деплой Docker-образов

Одна из замечательных особенностей Docker заключается в том, что, когда у вас есть собранный образ, вы можете запускать его повсюду: в большинстве окружений достаточно вызвать docker pull вашего образа и запустить его.

Некоторые провайдеры, которые вы можете попробовать:

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


В следующей главе Node Hero вы узнаете, как мониторить свои приложения Node.js, так, чтобы они могли быть онлайн 24/7.

Мониторинг Node.js-приложений

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

Важность мониторинга Node.js

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

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

  • Профилирование на уровне кода: Вы должны понимать, сколько времени требуется для запуска каждой функции в продакшен-среде, а не только локально.
  • Мониторинг сетевых подключений: Если вы строите микросервисную архитектуру, вы должны контролировать сетевые подключения и снизить задержки в обмене данными между вашими службами.
  • Дашборд отслеживания производительности: Знание и постоянное наблюдение за важнейшими метриками производительности вашего приложения необходимы для быстрой и стабильной продакшен-системы.
  • Предупреждения в режиме реального времени: По понятным причинам, если что-то ломается, вам необходимо немедленно получить уведомление. Это означает, что вам нужны инструменты, которые могут интегрироваться с Pagerduty или Opsgenie, и ваша DevOps-команда не пропустит ничего важного.

«Получение информации о работе системы в продакшене имеет решающее значение при создании Node.js-приложений»

Мониторинг сервера против мониторинга приложения

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

Рассмотрим основные различия!

Мониторинг сервера

Мониторинг сервера отвечает за хост-машину. Он должен помочь вам ответить на следующие вопросы:

  • У моего сервера достаточно места на диске?
  • У моего сервера достаточно ресурсов процессора?
  • У моего сервера достаточно памяти?
  • Доступен ли мой сервер по сети?

Для мониторинга сервера вы можете использовать такие инструменты, как zabbix.

Мониторинг приложения

Мониторинг приложения, с другой стороны, отвечает за здоровье конкретного экземпляра приложения. Он должен дать вам ответы на следующие вопросы:

  • Может ли экземпляр приложения попасть в базу данных?
  • Как много запросов обрабатывается?
  • Каково время ответа для отдельных случаев?
  • Может ли моё приложение обрабатывать запросы? Оно запущено?

Для мониторинга приложения я рекомендую использовать наш инструмент под названием Trace. Что же ещё? :)

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

Как использовать Trace для мониторинга Node.js

Чтобы начать работу с Trace, перейдите на https://trace.risingstack.com и создайте свою бесплатную учётную запись!

После регистрации выполните следующие действия, чтобы добавить Trace в ваше Node.js-приложение. Это займёт всего минуту:

Легко, правда? Если всё пойдёт хорошо, вы увидите, что подключённый сервис только что начал отправлять данные в Trace:

#1: Измерьте свою производительность

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

  • Вы можете использовать панель времени ответа для проверки медианы и 95-го перцентиля. Это поможет вам понять, когда и почему ваше приложение замедляется и как оно влияет на ваших пользователей.
  • График пропускной способности показывает количество запросов в минуту (rpm) для категорий кодов состояния (200-299 // 300-399 // 400-499 //> 500). Таким образом, вы сможете легко отделить здоровые и проблемные HTTP-запросы в своём приложении.
  • График использования памяти показывает, сколько памяти использует ваш процесс. Это очень полезно для распознавания утечек памяти и предотвращения сбоев.

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

#2: Настройте уведомления

Как я упоминал ранее, для вашего приложения в продакшене вам нужна надлежащая система оповещения.

Перейдите на страницу оповещения Trace и нажмите Create a new alert.

  • Самое важное, что нужно сделать, это настроить оповещения о простоях и проблемах с памятью. Trace уведомит вас по электронной почте / Slack / Pagerduty / Opsgenie, и вы также можете использовать Webhooks.
  • Я рекомендую настроить оповещение, которое мы называем «Ошибка по коду состояния», чтобы узнать о HTTP-запросах с кодами состояния 4XX или 5XX. Это ошибки, о которых вам следует позаботиться.
  • Также может быть полезно создать оповещение о времени отклика и получить уведомление, когда ваше приложение начнёт работать медленней.

#3: Исследуйте дампы памяти

Перейдите на страницу Profiler и запросите новый дамп памяти, подождите 5 минут и запросите другой. Загрузите их и откройте на странице Profiles Chrome DevTool. Выберите вторую (самую последнюю) и нажмите Comparison.

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

#4:Профайлинг CPU

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

Все, что вам нужно сделать, перейти на вкладку CPU Profiles на странице * Profiler*. Здесь вы можете запросить и скачать профиль, который вы можете загрузить в Chrome DevTool. После того, как вы его загрузили, вы сможете увидеть 10-секундный фрейм своего приложения и просмотреть все свои функции с таймингами и URL-адресами.

С помощью этих данных вы сможете выяснить, что замедляет ваше приложение и справиться с этим!


Конец

Вот и всё.

За время чтения 13 глав Node Hero вы изучили основы построения отличных приложений с помощью Node.js.

Надеюсь, вам понравилось и вы стали лучше разбираться в Node.js! Пожалуйста, поделитесь этим учебником с друзьями, если вы считаете, что им это тоже нужно, и покажите им Trace. Это отличный инструмент для разработки на Node.js!

Если вы хотите продолжить совершенствование своих навыков Node.js-разработчика, рекомендуем наш новый учебник Node.js at Scale!