Данный материал предназначен для читателей, которые уже имеют представление о:
- скриптовых языках типа JavaScript, Ruby, Python, Perl и других. Если вы только начинаете программировать, то вам стоит начать с прочтения JavaScript for Cats. 🐈
- git и github. Эти инструменты для совместной работы широко используются в сообществе, чтобы делиться своими модулями. Вам достаточно знать хотя бы их основы. По ним есть отличные самоучители для новичков: 1, 2, 3
- Изучи Ноду интерактивно
- Путь к пониманию Ноды
- Базовые модули
- [Колбэки] Callbacks
- События / Событийная модель
- Потоки в Ноде
- Модули и npm. Экосистема Ноды
- Разработка клиентской части с npm
- Правильный выбор инструмента
От переводчика
Несколько замечаний по переводу
В тексте будут встречаться английские слова и словосочетания. Такие термины я оставлял в скобках, чтобы читатель по мере прочтения привыкал к терминологии Ноды, и не привязывался к русским формулировкам, зачастую абсолютно без образным. Как правило это ключевые понятия типа "hard drive" или "event loop", которые опытный разработчик должен знать и так. А для начинающих так будет проще освоиться в тексте, чтобы у них не возникало двусмысленностей при чтении, когда одну и ту же вещь (образ) называют разными именами.
В некоторых местах я отклонялся от оригинального текста, меняя формулировку или же просто оставляя его без перевода. Часто это было по двум причинам
- Я не понимал что хотел сказать автор и как перевести так, чтобы сохранился тот смысл, который он хотел передать
- Я не видел большого смысла в написанном
С опытом вы всё чаще будете употреблять английские слова, произнося их по-русски, сокращая и даже коверкая. Со временем у вас выработается свой сленг, на котором вам будет удобно разговаривать с коллегами. Это нормально. Главное передать словом тот образ\суть, предмет разговора, а не вспоминать заученные формулировки на родном языке.
Благодарю авторов этой статьи http://frontender.info/art-of-node/ . В своем переводе я часто обращался к ней, чтобы сравнить или лучше понять смысл написанного. Это также перевод этой статьи, но авторы не стали выкладывать её на гитхаб.
В дополнение к чтению, очень важно параллельно писать код. Так вы скорее проникнетесь духом ноды и вникнете в её суть. Читать код в книге важно и нужно, но обучение через само написание кода - это ещё более лучший способ познания новых принципов программирования.
NodeSchool.io серия открытых интерактивных воркшопов, по которым можно обучиться основным принципам Ноды.
Learn You The Node.js представляет собой вступительный воркшоп NodeSchool.io. Здесь собраны несколько задач, решение которых поможет тебе усвоить основные принципы построения программ для Ноды. Устанваливается как консольная утилита.
Устанавливается через npm:
# install
npm install learnyounode -g
# start the menu
learnyounode
Node.js - опенсорсный проект, сделанный чтобы помочь тебе писать программы для работы с сетью, файловыми системами и другими I/O (input/output, reading/writing) на языке JavaScript. Вот и всё! Это простая и стабильная I/O платформа в которой удобно создавать свои модули.
Какие ещё есть примеры использования ввода/вывода (далее I/O)? Здесь показана схема приложения, к-ое я делал на Ноде; на ней видно какие могут быть I/O источники:
Если ты не знаешь все источники представленные на схеме, ничего страшного. Суть в том, чтобы показать, что один единственный процесс Ноды (шестигранник в центре) может выполнять роль брокера (диспетчера) между разными конечными пунктами (endpoints) I/O (оранжевым и фиолетовым обозначены каналы ввода/вывода).
Обычно, построение систем такого вида складывается по одному из путей:
- сложно для написания, но в результате получается супер-быстрая система (подобно написанию своих веб-серверов на чистом C)
- просты в написании но сильно страдает в скорости работы (особенно, когда кто-то пытается отправить на сервер 5Гб файл и твой сервер падает)
Задача Ноды - сохранить равновесие при достижении двух целей: быть достаточно простым для понимания и использования и настолько же быстрым для решения большинства задач.
Нода не является:
- веб-фреймворком (вроде Rails или Django, хотя и может использоваться для создания подобных вещей)
- языком программирования (Нода использует JS, но сама Нода языком НЕ является)
Нода - нечто среднее, можно сказать, что Нода:
- Сделана чтобы быть простой для понимания и использования
- Удобной при создании I/O программ, которые должны работать быстро и оставаться устойчивой к высоким нагрузкам
На более низком уровне, Ноду можно назвать инструментом для написания двух типов программ:
- Сетевые программы, использующие протоколы веба: HTTP, TCP, UDP, DNS и SSL
- Программы, для чтения и записи данных в файловую систему (далее ФС) или локальные процессы/память
Что означает "программы для I/O" ("I/O based program")? Рассмотрим несколько основых источников Ввода/Вывода (I/O sources):
- Базы данных (MySQL, PostgreSQL, MongoDB, Redis, CouchDB)
- Внешние API (Twitter, Facebook, Apple Push Notifications)
- HTTP/WebSocket соединения (от пользователей веб-приложений)
- Файлы (сжатие изображений, редактирование видео, интернет-радио)
Нода выполняет операции ввода/вывода способом, к-ый называют асинхронным asynchronous. Такой способ позволяет ей выполнять много разных операций одновременно (simultaneously). Приведу небольшой пример для большего понимания. Например, зайдя в какой-нибудь фаст-фуд и заказав чизбургер, ваш заказ примут сразу, и после небольшой задержки ваш заказ будет готов. Пока вы ждете, они могут принимать другие заказы и начать готовить чизбургеры для других людей. А теперь представьте ситуацию, когда остальным людям в очереди приходится ждать пока вам не принесут чизбургер. Они даже не смогут сделать заказ, пока вам его не приготовят! Технически такое поведение называется блокирующая очередь, ведь все операции ввода/вывода (по приготовлению чизбургеров) происходят строго по одной в 1 момент времени. Нода же, наоборот, реализует механизм неблокирующей очереди, что позволяет готовить много чизбургеров одновременно.
На Ноде такие вещи можно реализовать довольно легко, благодаря её неблокирующей сущности:
- Механизм управления летающими квадракоптерами
- Написать IRC чат-ботов
- Создать ходячих роботов
Во-первых, установите Ноду себе на компьютер. Брать её лучше отсюда nodejs.org
У ноды есть небольшая группа базовых модулей (которую обычно называют одним термином 'Ядро Ноды' ('node core')), которые предоставлены, как внешний API для написания программ. Каждый модуль предназначен для своих целей: для работы с файловой системой есть модуль 'fs', для работы с сетями net
(TCP), http
, dgram
(UDP).
Помимо модуля fs
и сетевых модулей, есть и другие базовые модули. Для асинхронной работы с DNS-запросами есть модуль dns
, os
- для получения данных об ОСи, для выделения бинарных фрагентов памяти (a module for allocating binary chunks of memory called) есть buffer
, модули для различного рода парсинга урлов, путей к файлам и вообще (url
, querystring
, path
). Большинство из базовых модулей, если не все, служат для одной общей цели - написание быстрых (!) программ для работы с ФС или сетью.
Нода обрабатывает I/O-операции используя: колбэки, события, потоки и модули. Если ты знаешь как они работают, то сможешь разобраться в любом базовом модуле и понять как его правильно использовать.
Это, пожалуй, самая важная часть всего гайда. Если хочешь понять как работает Нода - придется разобраться с колбэками. Колбэки используются в Ноде повсюду; это не открытие Ноды, они лишь часть языка JavaScript.
Итак, начнем с определения. Колбэки - функции, к-ые вызываются не сразу, по мере выполнения основного кода, а асинхронно (asynchronously), т.е. их выполнение (invoking) будет отложено. В отличие от привычного процедурного стиля написания и выполнения кода сверху вниз (top to bottom), асинхронные программы могут выполнять свои функции непоследовательно (не в порядке их написания), учитывая скорость выполнения предыдущих функций, например http-запросов или чтения с диска.
Поначалу такое отличие попросту сбивает с толку. Действительно, бывает трудно определить заранее, будет ли функция выполняться асинхронно или нет - во многом это зависит от контекста её выполнения. Разберем простой пример синхронного выполнения, где код будет выполняться последовательно сверху вниз:
var myNumber = 1
function addOne() { myNumber++ } // определяем функцию
addOne() // выполняем функцию
console.log(myNumber) // 2
В коде определяется функция и на след строке происходит её вызов, без задержек и пауз. Когда функция вызывается, myNumber сразу увеличится на 1. Мы уверены, что после вызова функции число станет равным 2. Это и есть предсказуемость синхронного кода - он всегда выполняется последовательно сверху вниз.
Нода же часто использует асинхронную модель выполнения кода. Давайте с помощью Ноды прочитаем число из файла number.txt
(файл находится на диске, а значит будем использовать модуль fs
- прим. перев.):
var fs = require('fs') // подключение модуля для работы с ФС
var myNumber = undefined // пока мы не знаем какое число записано в файле
function addOne() {
fs.readFile('number.txt', function doneReading(err, fileContents) {
myNumber = parseInt(fileContents)
myNumber++
})
}
addOne()
console.log(myNumber) // undefined -- эта строка выполнится до того, как будет прочитан файл!
Почему же после вызова функции мы получили undefined
? Обратите внимание, в коде мы используем асинхронный метод fs.readFile
. Обычно, такие функции, где идут операции чтения-записи на диск или работа с сетью, делают асинхронными. Когда же требуется обратиться к памяти напрямую или поиспользовать возможности процессора, то функции делают синхронными. Дело в том, что операции I/O невероятно медленные (reallyyy reallyyy sloowwww) (это относится не только к Ноде но и ко всем языкам и технологиям - прим. перев). Стоит сказать, что чтение с диска (hard drive) происходит медленнее чем из памяти (RAM) примерно в 100k раз.
Когда мы запустим эту программу, определение функций произойдет немедленно, но такая быстрота не относится к скорости их выполнения. Это ключевой принцип для понимания асинхронного программирования. Когда произойдет вызов addOne
она следом запустит функцию readFile
, но не будет ждать окончания её работы, а перейдет к следующей задаче. Если Ноде больше нечего выполнять она будет просто ждать окончания IO-операций чтобы закончить работу и выйти.
Далее, когда readFile
прочитает файл (это может занять некоторое время от нескольких миллисекунд до нескольких секунд или даже минут, в зависимости от того как быстро происходит чтение с диска), следом будет выполняться функция doneReading
, которая и выдаст содержимое файла (если чтение прошло успешно) или ошибку.
В нашей программе мы получили на выходе undefined
потому что в нашем коде нет никаких явных указаний функции console.log
дождаться окончания выполнения readFile
перед тем как выводить число.
Если вы хотите, чтобы какой-то код гарантированно выполнился последовательно, сперва поместите этот код в функцию! Только после этого ты сможешь вызвать функцию (выполнить блок кода) там где тебе надо. Это должно подтолкнуть тебя давать функциям точные и понятные имена.
Важно запомнить, что колбэки - просто функции, но которые выполнятся не сразу, по мере чтения кода, а тогда когда произойдет определенное событие. Ключ к пониманию механизма коллбэков лежит в том, что ты никогда не узнаешь когда (в какой момент времени) закончится асинхронная операция (I/O), но ты будешь уверен в том, где (после какого события) операция закончится - на последней строке асинхронной функции (т.е. колбэка)! Порядок объявления колбэков не имеет никакого значения и не влияет на последовательность выполнения. Значение имеет только их логическая вложенность, иерархичность если хотите. Сперва ты разбиваешь свой код на функции (как обособленные части кода) и только потом используешь колбэки, чтобы описать зависмости между их вызовами.
Снова вернемся к программе. Метод fs.readFile
, предлагаемый Нодой, выполняется асинхронно и требует много времени для своего выполнения. Рассмотрим происходящее детально: для выполнения функции требуется обратиться к ОСи, которой надо обратиться к ФС, которая живет на диске, который совершает тысячи оборотов в минуту. Диску надо задействовать магнитную головку (а это уже физический уровень, между прочим) чтобы прочитать данные и отправить их обратно по всем уровням нашей программе. Ты передаешь методу readFile
функцию-колбэк, которая и будет вызвана после того как данные от ФС будут получены. Колбэк поместит полученные данные в переменную и только теперь вызовет твою функцию-коллбэк уже с имеющей значение переменной (не undefined). В этом случае переменная называется fileContents
, т.к. в ней лежит содержимое всего файла.
Вспомните пример с заказом и очередью из 1 части. Во многих ресторанах вам ставят на стол номер, пока вы ждете свой заказ. Это очень похоже на коллбэк. Эти номера говорят официантам, что нужно сделать когда ваш заказ будет готов.
Вернемся к нашему примеру и вынесем выражение console.log
в отдельную функциию и передадим её как коллбэк:
var fs = require('fs')
var myNumber = undefined
function addOne(callback) {
fs.readFile('number.txt', function doneReading(err, fileContents) {
myNumber = parseInt(fileContents)
myNumber++
callback()
})
}
function logMyNumber() {
console.log(myNumber)
}
addOne(logMyNumber)
Теперь функцию logMyNumber
можно передать как аргумент, который станет "колбэчной" переменной уже внутри функции addOne
. После окончания выполнения readFile
будет вызвана переменная callback
(именно вызвана как функция: callback()
). Вызываться могут только фукнции, так что если передать туда что-то другое, то это приведет к ошибке.
В JS когда функция вызывается внутри другой функции (как callback()
), то она будет выполнена сразу. В таком контексте выражение console.log
выполнится как callback
-параметр, который на деле есть функция logMyNumber
. Запонмите важную вещь, когда вы определяете (define) функцию, это ещё ничего не говорит о том, когда она будет вызвана. Чтобы она сработала надо явно произвести её вызов (invoke).
Чтобы окончательно закончить разбор нашего пример, выпишем все программные действия в той последовательности, в которой они выполнятся при запуске программы:
- 1: Код "пропарсится" (The code is parsed), т.е. если в нем есть синтаксические ошибки, программа не запустится. В процессе "парсинга" будут определены переменные
fs
иmyNumber
и функцииaddOne
иlogMyNumber
. Заметьте, что на этом этапе идут только определения. Ни одна функция пока не вызвана. - 2: Когда выполнится последняя строка программы, будет вызвана функция
addOne
с функциейlogMyNumber
в качестве аргумента-колбэка. ВызовеaddOne
приведет к запуску асинхронную функциюfs.readFile
. Этой части программы нужно время, чтобы завершиться. - 3: Сейчас Нода будет бездействовать и ждать пока выполнится функция
readFile
. Если бы у неё были ещё какие-то задачи - она занялась бы ими. - 4: Как только
readFile
заканчивает работу, в дело вступает колбэк-функцияdoneReading
, которая парситfileContents
в поиске целого числа. РезультатparseInt
присваиваетсяmyNumber
-у, потом увеличивает его (myNumber
) значение на 1 и затем сразу вызывается функцияaddOne
, переданная как параметрcallback
вlogMyNumber
.
Пожалуй, самая непривычная часть программирования с колбэками - то как функции подобно объектам могут храниться в переменных и передаваться под разными именами. Давать простые и образные имена своим переменным - очень важное умение для программиста, особенно когда он пишет код, который будут читать другие люди. Читая Нода-программы, если ты видишь переменную с именем callback
или cb
- скорее всего здесь ожидается функция-колбэк.
Ты наверняка слышал такие понятия как событийно-ориентированное программирование ('evented programming') или "ивент луп" ('event loop') (умышленно не переводил чтобы не запутывать читателя абстрактными выражениями. если встретите где-нибудь понятие "событийный цикл" - знайте, это одно и то же. - прим. перев.). Они обозначают тот самый способ, которым реализован readFile
. Сначала Нода отправляет на выполнение метод readFile
, потом ждет, пока тот отправит ей "ивент" о своем окончании. В процессе ожидания Нода может проверять, есть ли ещё невыполненые операции. Внутри Ноды есть список запущенных, но ещё не законченных операций; Нода устроена так, что обходит этот список снова и снова пока какая-нибудь операция не завершится. По завершении, она считается обработанной (get 'processed') и все колбэки, которые были завязаны на её окончание будут вызваны.
Иллюстрация сказанного через псевдокод:
function addOne(thenRunThisFunction) {
waitAMinuteAsync(function waitedAMinute() {
thenRunThisFunction()
})
}
addOne(function thisGetsRunAfterAddOneFinishes() {})
Представьте, что у вас есть 3 асинхронные функции a
, b
и c
. Каждой из них на выполнение надо 1 минуту, после чего она передает управление своему колбэку (её первый аргумент). Если тебе понадобится вызвать их последовательно сначала a
, потом b
, потом c
, можно написать так:
a(function() {
b(function() {
c()
})
})
Когда код начнет выполняться, a
стартует сразу, затем через минуту она закончит выполнение и вызовется b
, затем, ещё через минуту она закончит и вызовется c
и наконец спустя 3 минуты, Нода остановится, потому что выполнять будет нечего. Есть и другие более выразительные способы чтобы описать приведенный пример, но суть в том, что если у тебя есть код который должен выполниться по окончании другого асинхронного кода, то тебе надо показать эту зависимость, поместив свой код в фукнцию и потом передать её как колбэк.
Такой способ построения программ требует не-линейного мышления. Рассмотрим список операций:
прочитать файл
обработать этот файл
Если перевести их в псевдокод, то мы получим:
var file = readFile()
processFile(file)
Такой тип линейного (последовательного, шаг-за-шагом) построения программ не работает в Ноде. Если код начнет выполняться в таком виде, то readFile
и processFile
будут выполняться одновременно. Так мы не сделаем зависимость на окончание выполнения readFile
. Вместо этого, тебе надо указать что processFile
должен дождаться окончания работы readFile
. И это как раз то, для чего и нужны колбэки! А благодаря возможностям JS ты можешь описывать такие зависимости разными способами:
var fs = require('fs')
fs.readFile('movie.mp4', finishedReading)
function finishedReading(error, movieData) {
if (error) return console.error(error)
// do something with the movieData
}
Но ты можешь написать код по-другому и он тоже сработает:
var fs = require('fs')
function finishedReading(error, movieData) {
if (error) return console.error(error)
// do something with the movieData
}
fs.readFile('movie.mp4', finishedReading)
Или даже так:
var fs = require('fs')
fs.readFile('movie.mp4', function finishedReading(error, movieData) {
if (error) return console.error(error)
// do something with the movieData
})
*События (Events) они же 'ивенты' - суть одно и то же, просто термины употребляются разными людьми в разных контекстах по-своему. Поэтому призываю не привязываться к словам, а зреть в корень. - прим. перев.
В Ноде, если тебе нужен модуль events ты можешь использовать т.н. "генератор событий" ('event emitter'), который сам используется Нодой для своих API, которые что-то генерируют.
События - основной паттерн в программировании, более известный как "Наблюдатель" 'observer pattern' или издатель\подписчик (publish/subscribe или совсем кратко 'pub/sub') . Поскольку колбэки реализуют модель отношений один-к-одному (one-to-one) между колбэком и тем кто его вызывает, события реализуют тот же паттерн для другого типа отношений - многие-ко-многим (many-to-many).
Принципы работы событий проще понять как некую подписку, они позволяют тебе "подписаться" на что-то, на совершение какого-то действия и твое гарантированное уведомление о нем. Ты можешь сказать "когда произойдет X сделать Y", в то время как простые колбэки (plain callbacks) понимали только "сделай X потом сделай Y". Т.о., подход событий более универсальный чем подход колбэков.
Несколько примеров использования (use cases) где события смогли бы заменить колбэки:
- Чат-комната (Chat room) где ты бы смог оповещать разных слушателей (listeners) о своих сообщениях
- Игровой сервер, которому нужно знать когда игроки подключились, отключились, переместились, ударили, прыгнули и т.п. (совершили игровые действия)
- Игровой движок где ты можешь позволить разработчикам подписываться на события примерно так:
.on('jump', function() {})
- Низкоуровневый веб-сервер, для которого нужен открытый API, чтобы перехватывать события, например так
.on('incomingRequest')
или так.on('serverError')
Если попробовать написать модуль, который подключается к чат-серверу используя только колбэки, то это будет выглядеть примерно так:
var chatClient = require('my-chat-client')
function onConnect() {
// have the UI show we are connected
}
function onConnectionError(error) {
// show error to the user
}
function onDisconnect() {
// tell user that they have been disconnected
}
function onMessage(message) {
// show the chat room message in the UI
}
chatClient.connect(
'http://mychatserver.com',
onConnect,
onConnectionError,
onDisconnect,
onMessage
)
Выглядит довольно неуклюже, поскольку все функции для вызова .connect
надо передавать в одном месте и в определенном порядке. Напишем то же самое, но с помощью событий:
var chatClient = require('my-chat-client').connect()
chatClient.on('connect', function() {
// have the UI show we are connected
})
chatClient.on('connectionError', function() {
// show error to the user
})
chatClient.on('disconnect', function() {
// tell user that they have been disconnected
})
chatClient.on('message', function() {
// show the chat room message in the UI
})
Похоже на вариант с чистыми колбэками (pure-callbacks), но вводит новый метод .on
, к-ый и подписывает функцию-колбэк на определенный тип событий. Это значит, что ты можешь выбирать на какие события ты хочешь подписаться из chatClient
. Ты даже можешь подписать на одно событие несколько разных колбэков:
var chatClient = require('my-chat-client').connect()
chatClient.on('message', logMessage)
chatClient.on('message', storeMessage)
function logMessage(message) {
console.log(message)
}
function storeMessage(message) {
myDatabase.save(message)
}
На ранних стадиях развития Ноды API для работы с ФС и сетью пользовались своими собственными приемами в работе с потоками ввода/вывода (streaming I/O). Например, для файлов в файловых системах применялись так называемые «файловые дескрипторы», соответственно, модуль fs был наделён дополнительной логикой, позволяющей их отслеживать, в то время, как для сетевых модулей такая концепция не использовалась. Несмотря на незначительные отличия в семантиках подобно этим, на самом низком уровне, где надо было считывать и записывать данные обе кодовые базы во многом повтряли друг друга.
Команда, работающая над Нодой, осознала, что такое положение дел будет только путать разработчиков, которым придется изучать две группы семантик, чтобы сделать по сути одно и тоже. Они сделали новый API, который назвали Потоком
(Stream
) и переписали весь код для работы с ФС и сетью уже на нем. Главная задача Ноды - сделать работу с ФС и с сетями простой и удобной, поэтому было разумно иметь единый общий подход, который использовался бы повсюду. Главный плюс заключается в том, что большинство паттернов подобных этим на данный момент уже реализованы и маловероятно, что Нода в будущем сиьно изменится.
Есть 2 отличных ресурса, которые можно использовать для изучения потоков в Ноде. Первый - stream-adventure (см. раздел "Изучи Ноду интерактивно") и другой - справочник, называемый Stream Handbook.
stream-handbook - гайд, похожий на этот, в котором есть ссылки на всё, что только может понадобиться при изучении потоков.
Ядро Ноды (Node core) включает в себя более 20 модулей, которые делятся на низкоуровневые, такие как events
и stream
и высокоуровневые типа http
and crypto
.
Такая структура выбрана неслучайно. Ядро изначально предполагалось сделать небольшим и независмым от платформы, а главная задача модулей - обеспечивать работу с основными I/O протоколами и форматами.
Для всего остального есть пакетный менеджер Node npm. Каждый может создать модуль и опубликовать его для npm. На момент написания этих строк на npm было около 34k модулей.
Представьте, вам надо сконвертить PDF файлы в текстовые. Начать стоит с команды npm search pdf
:
Он выдаст кучу результатов. npm, действительно, очень популярен и вы наверняка сможете найти здесь подходящее решение для своей задачи. Если внимательно рассматривать каждый модуль и фильтровать результаты поисков (убирая, например, модули PDF-генераторов) то в конце концов увидишь список:
- hummus - c++ pdf manipulator
- mimeograph - api on a conglomeration of tools (poppler, tesseract, imagemagick etc)
- pdftotextjs - wrapper around pdftotext
- pdf-text-extract - another wrapper around pdftotext
- pdf-extract - wrapper around pdftotext, pdftk, tesseract, ghostscript
- pdfutils - poppler wrapper
- scissors - pdftk, ghostscript wrapper w/ high level api
- textract - pdftotext wrapper
- pdfiijs - pdf to inverted index using textiijs and poppler
- pdf2json - pure js pdf to json
Есть много модулей, которые повторяют функционал друг друга, но предоставляют разные API и многие требует установки внешних зависимостей (как например, apt-get install poppler
).
Несколько примеров, на что стоит обращать внимание при выборе нужного модуля:
pdf2json
единственный модуль, написанный на чистом js, что означает его легкость в установке, особенно на маломощных устройствах типа raspberry pi или на Windows, у которого нативный код не может быть перенесен на другую платформу- модули типа
mimeograph
,hummus
иpdf-extract
объединяют в себе несколько низкоуровневых модулей чтобы предоставить к ним высокоуровневый API - много модулей используют под собой никсовские тулзы
pdftotext
/poppler
Давайте сравним pdftotextjs
и pdf-text-extract
, оба являются лишь оболочками вокруг утилиты pdftotext
.
Сходства:
Оба модуля:
- обновлены относительно недавно
- имеют свои репозитории на гитхабе (что очень важно)
- имеют README файлы
- каждую неделю скачиваются пользователями
- имеют открытую лицензией (т.е. может воспользоваться любой)
По данным package.json
и одной статистике модуля сделать правильный выбор совсем непросто. Давайте сравним файлы описаний README:
Оба имеют простые понятные описания, значки CI, инструкции по установке, примеры использования, инструкции по запуску тестов. Отлично! Но какой же выбрать? Сравним код внутри:
В pdftotextjs
примерно 110 строк кода, а в pdf-text-extract
около 40, но у обоих всё сводится по сути к одной строке:
var child = shell.exec('pdftotext ' + self.options.additional.join(' '));
Делает ли это одну лучше другой? Трудно сказать! Здесь важно самому прочитать код и сделать свои выводы. Если найдешь модуль, который тебе понравится, набери npm star modulename
. Так можно сказать npm, что тебе понравилось пользоваться этим модулем.
npm отличается от большинства пакетных менеджеров тем, что устанавливает модули в папку внутри других существующих модулей. Это может быть непонятно сразу, но это чуть ли не ключевой фактор успеха npm.
Многие пакетные менеджеры (далее ПМ) устанавливают их глобально (т.е. к пакету можно обратиться прямо из консоли из любой директории). Например, Если набрать apt-get install couchdb
на Debian Linux - он поставит последнюю стабильную версию (latest stable version) CouchDB. Теперь, если ты установишь CouchDB как зависмость от другого пакета или программы и эта программа требует более старой версии CouchDB, то тебе придется удалить свежую версию CouchDB и только после этого поставить более старую. У тебя не получится поставить две версии CouchDB потому что Debian устанавливает все пакеты в одно место.
Это относится не только к Debian. Многие ПМы языков программирования работают по тому же принципу. Чтобы избежать описанного выше конфликта зависимостей, было разработано виртуальное окружение (далее ВО) (virtual environment), похожее на virtualenv у Python или bundler из мира Ruby. Они разбивают твое привычное окружение на много виртуальных, по одному на каждый проект, но внутри каждое такое окружение ставит пакеты всё так же глобально для этого виртуального. Такие ВО не всегда решают проблему, иногда они только раздувают её, создавая новые уровни сложности.
Для npm установка глобальных модулей - антипаттерн (плохой подход) (anti-pattern).
Также как в программах на JS ты не станешь использовать глобальные переменные, ты также не станешь устанавливать модули глобально (пока тебе не понадобится модуль с исполняемым файлом чтобы обратиться к нему твоем глобальном PATH
, но тебе редко такое может понадобиться -- об этом позже).
When you call require('some_module')
in node here is what happens:
- Если вызываемый файл
some_module.js
существует в текущей папке, то Нода подгрузит его, иначе - Нода поищет в текущей папке папку с именем
node_modules
и внутри неё папку с именемsome_module
- Если она и её не найдет, то он поднимется на 1 уровень вверх и повторит шаг 2
Этот цикл повторится пока Нода не доберется до корневой папки ФС, оттуда он проверит все папки глобальных модулей (такие как /usr/local/node_modules
on Mac OS) и если так и не встретит some_module
, только тогда Нода выбросит "эксепшн" (исключение) (throw an exception).
Рассмотрим пример такого поиска:
Находясь в папке subsubfolder
и вызвав require('foo')
, Нода будет искать папку subsubfolder/node_modules
. Здесь он его не найдет -- папка здесь нарочно называется my_modules
. Тогда Нода поднимется вверх на 1 уровень и попробует искать снова, - на картинке это выглядело бы как subfolder_B/node_modules
, которой также не существует. Третья попытка окажется удачной - папка folder/node_modules
существует и имеет внутри себя папку foo
. Если бы foo
и здесь не было - Нода продолжила бы поиск в родительской директории.
Отметим, что если бы Нода была вызвана из subfolder_B
, то она бы никогда не попала в папку subfolder_A/node_modules
. Поднимаясь вверх по дереву папок она сможет попасть только в folder/node_modules
.
Одно из преимуществ подхода npm в том, что модули сами могут устанавливать свои зависимости, причем тех версий, которые актуальны для них самих. В нашем примере, модуль foo
оказался крайне популярен - 3 копии пакета, по одному на каждую родительскую папку самогО модуля. Причиной этому можеть быть то, что каждый родительский модуль нуждается в своей версии пакета foo
, т.е. folder
у нужен [email protected]
, subfolder_A
у нужен [email protected]
и т.д.
Посмотрим, что произойдет когда мы исправим ошибку имени директории, сменив его с my_modules
на правильное node_modules
:
Чтобы протестить какой модуль фактически загружен Нодой, вы можете вызвать команду require.resolve('some_module')
, которая выведет путь к тому модулю, который Нода нашла при обходе директорий. require.resolve
может оказаться полезной когда вам надо убедиться в том, что загружен именно тот модуль и той версии которую вы ожидаете -- бывает что версия подключенного модуля отличается от той, которую мы ожидаем увидеть - это значит, Нода нашла такой модуль быстрее, чем тот, который нам нужен.
Теперь, когда мы узнали как искать модули и как подключать их в программу вы можете начать писать свои.
Модули Ноды крайне легковесны (lightweight). Один из самых простых модулей выглядит так:
package.json
:
{
"name": "number-one",
"version": "1.0.0"
}
index.js
:
module.exports = 1
По умолчанию (By default), когда ты вызываешь require('module')
, то Нода пробует загрузить module/index.js
. С любым другим именем файла это не сработает, пока вы не укажете его явно в файле package.json
в поле main
.
Положите оба этих файла в папку number-one
(значение name
в package.json
должно совпадать с именем папки) и вы получите готовый рабочий модуль.
Вызывая функцию require('number-one')
вы получите то значение, которое установлено для module.exports
внутри модуля.
Для создания модуля есть ещё способ, даже более быстрый. Выполните эти команды:
mkdir my_module
cd my_module
git init
git remote add [email protected]:yourusername/my_module.git
npm init
Выполнив в консоли npm init
создастся валидный (valid) package.json
и если запустить его в существующем git
репозитории, то он автоматом проставит поле repositories
внутри package.json
.
У модуля может быть список других модулей из npm или GitHub в поле dependencies
в файле package.json
. Чтобы установить модуль request
как новую зависимость и сразу доавбить его в package.json
выполните следующую команду в корневой папке модуля:
npm install --save request
Этим вы устанавливаете копию request
в закрытую извне папку node_modules
, и наш package.json
будет похож на этот:
{
"id": "number-one",
"version": "1.0.0",
"dependencies": {
"request": "~2.22.0"
}
}
По умолчанию, npm install
подтягивает последнюю опубликованную версию модуля.
Основное заблуждение о npm - то что если в названии встречается слово 'Node.js', то это будет использоваться только на сервере. Совсем нет. npm - менеджер пакетов Ноды, он отвечает за те модули, которые Нода упаковывает. Сами же модули могут быть чем угодно -- это просто папка с файлами, собранная в архив и файлом package.json
, который описывает версию модуля и список своих зависимостей (вместе с версиями тех модулей, от которых он сам зависит, так что рабочие версии всех модулей поставятся автоматически). Эта цепочка очень длинная - модуль зависят от других модлуей, к-ые в свою очередь зависят от других и т.д.
Утилита browserify, написанная на Ноде, создана чтобы сконвертить любой Нодовский модуль так, чтобы его код можно было выполнять в браузере. Не со всеми модулями такое получится сделать (браузер, например, не может выступать в качестве HTTP сервера), но со многими модулями такое проходит.
Чтобы попробовать возмоности Npm в браузере используйте модуль RequireBin, это приложение, которое я сделал, вобрало в себя плюсы Browserify-CDN, который сам внутри использует browserify, но результат выдает через HTTP (а не на консоль, как это делает browserify).
Скопируем этот код в окно RequireBin нажмем "Run Code":
var reverse = require('ascii-art-reverse')
// makes a visible HTML console
require('console-log').show(true)
var coolbear =
" ('-^-/') \n" +
" `o__o' ] \n" +
" (_Y_) _/ \n" +
" _..`--'-.`, \n" +
" (__)_,--(__) \n" +
" 7: ; 1 \n" +
" _/,`-.-' : \n" +
" (_,)-~~(_,) \n"
setInterval(function() { console.log(coolbear) }, 1000)
setTimeout(function() {
setInterval(function() { console.log(reverse(coolbear)) }, 1000)
}, 500)
Или другой пример (смело меняйте код, чтобы увидеть что будет):
Как любой хороший инструмент, Нода как никто лучше справляется с тем кругом задач, для решения которых она была сделана. К примеру, фреймворк Rails отлично подходит для построения сложной бизнес-логики business logic, где код используется для представления реальных бизнес-объектов. И хотя чисто технически такая задача Ноде под силу, но решая её вы у вас возникнут проблемы, потому что Нода создавалась для решения задач ввода/вывода, а не для написания 'бизнес-логики'. Каждый инструмент создается под свои задачи. Надеюсь, этот гайд (guide) поможет вам понять и прочувствовать сильные стороны Ноды, чтобы у вас выработалось понимание того, в каких случаях она будет вам полезна.
Приципиально, Нода - лишь инструмент для управления потоками ввода/вывода в ФС и сетях, сама Нода не затрагивает возможности других частей системы, это делают уже сторонние модули. Здесь описаны несколько вещей которые ошибочно приписывают Ноде:
Существуют фреймворки, построенные на Ноде (фреймворк здесь понимается как пакет для решения какой-то высоскоуровневой задачи, например моделирование бизнес-логики), но сама Нода не веб-фреймворк. Веб-фреймворки, написанные на Ноде не всегда соблюдают те принципы и правила, которые закладывались в архитектуру Ноды.
Нода использует JS и не собирается что-то менять. У Felix Geisendörfer есть отличное описание своего видения стиля Ноды here.
Всегда когда это возможно, Нода будет использовать самый простой способ для выполнения задачи, перед которой её поставили. Пограммирование вещь непростая, особенно в JS, где на каждую проблему найдется 1000 возможных решений! Эта та причина, по которой Нода старается всегда находить самое простое и универсальное решение. Но если ты сталкиваешься с задачей которая приводит к запутанному решению и тебе не нравятся те скорые решения, которые предлагает Нода, ты в праве самостоятельно решить её в своем приложении - выбрать модуль, который тебе понравится, или абстракции которые тебе подойдут.
Хорошей иллюстрацией этих слов служит использование колбэков. В ранних версиях Ноды был популярен прием с использованием промисов ('promises'), которые позволяли писать асинхронный код так, чтобы выглядел он как линейный. Но эту фичу исключили из ядра Ноды по нескольки причинам:
- они гораздо сложнее в использовании чем колбэки
- их можно использовать, установив специальный модуль
Рассмотрим пример с чтением файла. Когда ты читаешь файл, тебе надо знать, какие ошибки произошли, например, отказал жесткий диск прямо во время чтения файла. Если бы Нода использовала промисы, то приходилось бы "ветвить" свой код почти как здесь:
fs.readFile('movie.mp4')
.then(function(data) {
// do stuff with data
})
.error(function(error) {
// handle error
})
Это добавляет ненужную сложность, что понравится не каждому. Вместо двух отдельных функций в Ноде используется единая колбэк-функция. Для неё действуют правила:
- Когда ошибки нет, первым аргументом идет null
- Когда ошибка есть, передавать её первым аргументом
- Остальные аргументы могут использованы как угодно (обычно, это будут данные или ответы на запросы, ведь Нода по большей части работает с вводом/выводом)
Отсюда и такой стиль написания колбэков:
fs.readFile('movie.mp4', function(err, data) {
// handle error, do stuff with data
})
Замечание: Если вы ещё не сталкивались с этими терминами, возможно, вам будет проще освоить Ноду, ведь забыть что-то так же сложно как и запомнить.
Чтобы делать всё быстро Нода использует внутри себя потоки (threads), но сама скрывает их от пользователя. Если вы технарь, и вам интересно как устроена Нода внутри, вам совершенно точно надо прочитать об архитектуре библиотеки libuv, C++ I/O слое, на котором держится сама Нода.
Creative Commons Attribution License (do whatever, just attribute me) http://creativecommons.org/licenses/by/2.0/
Donate icon is from the Noun Project