Добрый день.
Эта статья ориентирована на разработчиков, имеющих представление о node.js.
Недавно готовил материал по фактам, которые полезно знать разработчикам под node.js в нашей конторе. Проекты, над которыми мы работаем — это API сервисы, использующие модуль node.js express в качестве веб-сервера. Материал основан на реальных случаях, в которых код работал неправильно или логика в нём была тщательно скрыта, или он провоцировал ошибки при расширении. На основе этого материала был проведён семинар по повышению квалификации сотрудников.
Вот, решил поделиться. Пока только первая часть, это около 30%. Если будет интересно, последует продолжение!
Я старался предоставить возможность быстрого ознакомления, поэтому примеры, рассуждения и комментарии спрятал в спойлерах. Если утверждения очевидны, «воду» можно пропустить. Хотя наши «грабли» в спойлерах тоже могут быть интересны.
Один коллега при проведении семинара задал мне вопрос, зачем об этом говорить, если всё и так есть в той или иной документации. Мой ответ был следующий. Несмотря на то, что посыл верен, всё действительно есть в документации, мы по-прежнему делаем досадные ошибки, связанные с непониманием или незнанием базовых вещей.
Приступим!
В отличие от javavm, nodejs-vm является однопоточной**.
Источник
Масштабирование производится путём запуска ещё одного процесса node.js или, если ресурсы сервера подходят к концу, путём запуска ещё одного сервера.
Начиная с версии 10.5.0 в node.js появилась экспериментальная поддержка многопоточности.
Источник
Сердцем nodejs-vm является цикл обработки событий. Когда выполнение кода должно быть приостановлено или код, вроде-бы, кончился, управление переходит именно к нему.
package.json — файл описания нашего пакета. В данном контексте речь идёт именно о нашем приложении, а не о зависимостях. Ниже перечислены поля и объяснения, почему же их всё-таки стоит заполнять.
На этом можно пока поставить точку. Не вошедшая информация является приёмами упрощения кода, используемым нашей командой.
При обнаружении ошибок буду стараться их быстро исправлять!
Эта статья ориентирована на разработчиков, имеющих представление о node.js.
Недавно готовил материал по фактам, которые полезно знать разработчикам под node.js в нашей конторе. Проекты, над которыми мы работаем — это API сервисы, использующие модуль node.js express в качестве веб-сервера. Материал основан на реальных случаях, в которых код работал неправильно или логика в нём была тщательно скрыта, или он провоцировал ошибки при расширении. На основе этого материала был проведён семинар по повышению квалификации сотрудников.
Вот, решил поделиться. Пока только первая часть, это около 30%. Если будет интересно, последует продолжение!
Я старался предоставить возможность быстрого ознакомления, поэтому примеры, рассуждения и комментарии спрятал в спойлерах. Если утверждения очевидны, «воду» можно пропустить. Хотя наши «грабли» в спойлерах тоже могут быть интересны.
Один коллега при проведении семинара задал мне вопрос, зачем об этом говорить, если всё и так есть в той или иной документации. Мой ответ был следующий. Несмотря на то, что посыл верен, всё действительно есть в документации, мы по-прежнему делаем досадные ошибки, связанные с непониманием или незнанием базовых вещей.
Приступим!
Виртуальная машина node.js
Однопоточность
В отличие от javavm, nodejs-vm является однопоточной**.
Источник
подробнее
При этом существует пул вспомогательных потоков, которые используются самóй виртуальной машиной, например, для организации ввода-вывода. Но весь пользовательский код выполняется только в одном, «главном» потоке.
Это серьёзно упрощает жизнь, так как конкуренция отсутствует. Выполнение кода не может быть прервано в произвольном месте и продолжено в другом. Код просто выполняется, пока не встанет необходимость ожидания чего-либо, например, готовности данных при чтении из файла. Пока происходит ожидание, может выполняться другой обработчик, либо пока он не закончит работать, либо пока не начнёт тоже чего-то ждать.
То есть, если есть внутренняя структура данных, то не надо заботиться о синхронизации доступа к ней!
Что же делать, если «главный» поток не успевает обрабатывать данные?
Это серьёзно упрощает жизнь, так как конкуренция отсутствует. Выполнение кода не может быть прервано в произвольном месте и продолжено в другом. Код просто выполняется, пока не встанет необходимость ожидания чего-либо, например, готовности данных при чтении из файла. Пока происходит ожидание, может выполняться другой обработчик, либо пока он не закончит работать, либо пока не начнёт тоже чего-то ждать.
То есть, если есть внутренняя структура данных, то не надо заботиться о синхронизации доступа к ней!
Что же делать, если «главный» поток не успевает обрабатывать данные?
Масштабирование производится путём запуска ещё одного процесса node.js или, если ресурсы сервера подходят к концу, путём запуска ещё одного сервера.
следствия и наши "грабли"
Тут тоже всё понятно. Нужно всегда быть готовым к тому, что процессов node.js может быть (и скорее всего будет) больше чем один. Да и серверов иногда тоже может быть несколько.
«Грабли», которые были
плохо не очень хорошо, а в данной ситуации и подавно. Без привлечения стороннего сервиса эта задача представляется мне не имеющей решения.
Коллега, который этим занимался, очень-очень хотел реализовать это без привлечения собственно базы данных. В конце концов, после нескольких «подходов к снаряду», это было реализовано… путём привлечения SharePoint.
«Грабли», которые были спрятаны найдены у нас в коде
Параллельные прямые в бесконечности пересекаются. Доказать нельзя, но я видел.Была сделана попытка обеспечить уникальность экземпляров сущностей в базе данных исключительно средствами приложения. В общем-то, это и в отрыве от контекста выглядит
Жан Эффель, «Роман Адама и Евы».
Коллега, который этим занимался, очень-очень хотел реализовать это без привлечения собственно базы данных. В конце концов, после нескольких «подходов к снаряду», это было реализовано… путём привлечения SharePoint.
**Многопоточность или «если очень хочется»
Начиная с версии 10.5.0 в node.js появилась экспериментальная поддержка многопоточности.
Источник
Но парадигма при этом осталась прежней
Поэтому старый код будет продолжать работать и при использовании рабочих потоков.
Подробнее почитать можно тут.
- Для каждого нового рабочего потока создаётся свой изолированный экземпляр окружения виртуальной машины node.js.
- У рабочих потоков отсутствуют общие изменяемые данные. (Есть пара оговорок, но в основном утверждение справедливо.)
- Коммуникация осуществляется с помощью сообщений и SharedArrayBuffer.
Поэтому старый код будет продолжать работать и при использовании рабочих потоков.
Подробнее почитать можно тут.
Жизненный цикл приложения
Сердцем nodejs-vm является цикл обработки событий. Когда выполнение кода должно быть приостановлено или код, вроде-бы, кончился, управление переходит именно к нему.
Скрытый текст
В цикле обработки событий проверяется, не случилось ли (ох) событий, для которых мы зарегистрировали обработчики. Если случилось, то обработчики будут вызваны. А если нет, то будет проверено, а нет ли «генераторов» событий, для которых мы зарегистрировали обработчики. Открытое tcp соединение или таймер могут быть такими генераторами. Если же таковых обнаружить не удалось, то происходит выход из программы. Иначе происходит ожидание одного из таких событий, вызываются обработчики, и всё повторяется.
Следствием такого поведения является тот факт, что когда код вроде-бы закончился, выход из nodejs-vm не происходит, например потому, что мы зарегистрировали обработчик таймера, который должен быть вызван через какое-то время.
Это показано в следующем примере.
результат:
Подробнее почитать можно тут.
В результате, если администратор побывал в системе, любой пользователь, обратившийся к этому экземпляру сервиса, воспринимался как администратор.
Мне стоило неких усилий, показать коллеге, что в логике была ошибка. Коллега был уверен, что на каждый http запрос создаётся полностью новое окружение.
Следствием такого поведения является тот факт, что когда код вроде-бы закончился, выход из nodejs-vm не происходит, например потому, что мы зарегистрировали обработчик таймера, который должен быть вызван через какое-то время.
Это показано в следующем примере.
console.log('registering timer callbacks');
setTimeout( function() {
console.log('Timer Event 1');
}, 1000);
console.log('Is it the end?');
результат:
registering timer callbacks
Is it the end?
Timer Event 1
Подробнее почитать можно тут.
Ещё одни «грабли» в нашем коде
Управлять государством может каждый!Признак того, является ли пользователь администратором, сохранялся в глобальной переменной. Эта переменная инициализировалась значением false в начале работы программы. В дальнейшем, когда администратор регистрировался, этой переменной присваивалось значение true.
В результате, если администратор побывал в системе, любой пользователь, обратившийся к этому экземпляру сервиса, воспринимался как администратор.
Мне стоило неких усилий, показать коллеге, что в логике была ошибка. Коллега был уверен, что на каждый http запрос создаётся полностью новое окружение.
package.json — поля, которые стоит заполнять
package.json — файл описания нашего пакета. В данном контексте речь идёт именно о нашем приложении, а не о зависимостях. Ниже перечислены поля и объяснения, почему же их всё-таки стоит заполнять.
Скрытый текст
Пока мы не публикуем пакет в репозитории, поле можно и «зюками» забить. Вопрос в том, что это поле удобно использовать для именования файла инсталляции или, например, для показа имени продукта на его веб-странице. В общем «как вы яхту назовёте,..»
Основная идея — не забывать увеличивать номер версии при расширении функциональности, исправлении ошибок,… К сожалению, у нас в конторе ещё можно встретить продукты с неизменной версией 0.0.0. А потом поди догадайся, какая именно функциональность работает у клиента…
Это поле сообщает, какой файл будет запущен при старте нашего приложения (`npm start`). Если пакет используется в качестве зависимости, то какой файл будет импортирован при использовании нашего модуля другим приложением. Текущим каталогом считается каталог, где находится файл `package.json`.
А ещё, если мы, например, используем vscode, то файл, указанный в этом поле, будет запущен при вызове отладчика или при запуске команды «выполнить».
Расширение ".js" может быть опущено. Это скорее следствие всех возможных вариантов использования, поэтому в документации напрямую не прописано.
Это поле содержит кортеж: { «node»: version, «npm»: version,… }.
Мне известны поля «node» и «npm». Они определяют версии node.js и npm, необходимые для работы нашего приложения. Версии проверяются при выполнении команды «npm install».
Поддерживается стандартный синтаксис определения версий пакетов зависимостей: без префикса (единственная версия), префикс "~" (два первых числа версии должны совпадать) и префикс "^" (только первое число версии должно совпадать). При наличии префикса, версия должна быть больше или равна указанной в этом поле. Просто список версий; явное указание больше, меньше,… etc. тоже работает.
Оговорка. «npm install» проверяет версии, указанные в «engines», только если включён режим «engine-strict». Мы его включаем для каждого проекта, добавляя файл .npmrc со строкой: «engine-strict = true». Когда-то давно «npm install» делал эту проверку по умолчанию.
Некоторые контейнеры, как минимум в документации, пишут, что подходящие версии будут использованы по умолчанию. В данном случае речь идёт об Azure.
Пример:
С клиентом было неоднократно оговорено, что требуемая версия `node.js` должна быть не меньше 8. При поставке начальных версий приложения всё работало. «В один прекрасный день» после поставки новой версии у клиента приложение перестало запускаться. В наших тестах всё работало.
Проблема была в том, что в этой версии мы стали использовать функциональность, которая поддерживалась только, начиная с версии 8 node.js. Поле «engines» не было заполнено, поэтому раньше никто не замечал, что у клиента была установлена древняя версия node.js. (Azure web services default).
Поле содержит кортеж вида: { «script1»: script1, «script2»: script2,… }.
Существуют стандартные скрипты, которые выполняются в той или иной ситуации. Например скрипт «install» выполнится после отработки «npm install». Очень удобно, например, чтобы проверить наличие программ, необходимых для работы приложения. Или, скажем, для сжатия всех статических файлов, доступных через наш веб-сервис, чтобы их не приходилось сжимать налету.
При этом можно не ограничиваться только стандартными именами. Для того, чтобы выполнить произвольный скрипт, нужно запустить «npm run script-name».
Это удобно, чтобы собрать все используемые скрипты в одном месте.
Пример:
P.S. Расширение ".js" можно в большинстве случаев опускать.
name
Пока мы не публикуем пакет в репозитории, поле можно и «зюками» забить. Вопрос в том, что это поле удобно использовать для именования файла инсталляции или, например, для показа имени продукта на его веб-странице. В общем «как вы яхту назовёте,..»
version
Основная идея — не забывать увеличивать номер версии при расширении функциональности, исправлении ошибок,… К сожалению, у нас в конторе ещё можно встретить продукты с неизменной версией 0.0.0. А потом поди догадайся, какая именно функциональность работает у клиента…
main
Это поле сообщает, какой файл будет запущен при старте нашего приложения (`npm start`). Если пакет используется в качестве зависимости, то какой файл будет импортирован при использовании нашего модуля другим приложением. Текущим каталогом считается каталог, где находится файл `package.json`.
А ещё, если мы, например, используем vscode, то файл, указанный в этом поле, будет запущен при вызове отладчика или при запуске команды «выполнить».
Расширение ".js" может быть опущено. Это скорее следствие всех возможных вариантов использования, поэтому в документации напрямую не прописано.
engines
Это поле содержит кортеж: { «node»: version, «npm»: version,… }.
Мне известны поля «node» и «npm». Они определяют версии node.js и npm, необходимые для работы нашего приложения. Версии проверяются при выполнении команды «npm install».
Поддерживается стандартный синтаксис определения версий пакетов зависимостей: без префикса (единственная версия), префикс "~" (два первых числа версии должны совпадать) и префикс "^" (только первое число версии должно совпадать). При наличии префикса, версия должна быть больше или равна указанной в этом поле. Просто список версий; явное указание больше, меньше,… etc. тоже работает.
Оговорка. «npm install» проверяет версии, указанные в «engines», только если включён режим «engine-strict». Мы его включаем для каждого проекта, добавляя файл .npmrc со строкой: «engine-strict = true». Когда-то давно «npm install» делал эту проверку по умолчанию.
Некоторые контейнеры, как минимум в документации, пишут, что подходящие версии будут использованы по умолчанию. В данном случае речь идёт об Azure.
Пример:
"engines": {
"node": "~8.11", // require node version 8.11.* starting from 8.11.0
"npm": "^6.0.1" // require npm version 6.* starting from 6.0.1
},
очередные «грабли»
А король-то голый!
С клиентом было неоднократно оговорено, что требуемая версия `node.js` должна быть не меньше 8. При поставке начальных версий приложения всё работало. «В один прекрасный день» после поставки новой версии у клиента приложение перестало запускаться. В наших тестах всё работало.
Проблема была в том, что в этой версии мы стали использовать функциональность, которая поддерживалась только, начиная с версии 8 node.js. Поле «engines» не было заполнено, поэтому раньше никто не замечал, что у клиента была установлена древняя версия node.js. (Azure web services default).
scripts
Поле содержит кортеж вида: { «script1»: script1, «script2»: script2,… }.
Существуют стандартные скрипты, которые выполняются в той или иной ситуации. Например скрипт «install» выполнится после отработки «npm install». Очень удобно, например, чтобы проверить наличие программ, необходимых для работы приложения. Или, скажем, для сжатия всех статических файлов, доступных через наш веб-сервис, чтобы их не приходилось сжимать налету.
При этом можно не ограничиваться только стандартными именами. Для того, чтобы выполнить произвольный скрипт, нужно запустить «npm run script-name».
Это удобно, чтобы собрать все используемые скрипты в одном месте.
Пример:
"scripts": {
"install": "node scripts/install-extras",
"start": "node src/well/hidden/main/server extra_param_1 extra_param_2",
"another-script": "node scripts/another-script"
}
P.S. Расширение ".js" можно в большинстве случаев опускать.
package-lock.json — помогает инсталлировать конкретные версии зависимостей, а не «самые свежие»
Скрытый текст
Этот файл появился в npm сравнительно недавно. Его цель — организовать повторяемость сборки.
На машине коллеги приложение прекрасно работало. На другом компьютере в идентичном окружении, в приложении, помещённом из git в новый каталог, после выполнения 'npm install', 'npm start' появлялись доселе невиданные ошибки.
Проблема была вызвана тем, что файл 'package-lock.json' отсутствовал в git-репозитории. Поэтому, при инсталляции пакетов, все зависимости второго и более уровня (естественно, не прописанные в package.json), инсталлировались максимально свежими. На компьютере коллеги всё было хорошо. На тестируемом компьютере подобралась несовместимая совокупность версий.
Возвращаясь от лирического отступления. Файл 'package-lock.json' содержит список всех модулей, инсталлированных локально для нашего приложения. Наличие этого файла позволяет воссоздать один-в-один набор версий модулей.
Резюме: не забываем класть в git и вкючать в файл поставки (инсталляции) приложения!
Полезно: если файл 'package-lock.json' отсутствует, но есть каталог 'node_modules' со всеми необходимыми модулями, файл 'package-lock.json' можно воссоздать:
To git or not to git?..
Этот файл появился в npm сравнительно недавно. Его цель — организовать повторяемость сборки.
и ещё одни «грабли»
Но я же ничего не менял в моей программе! Ещё вчера она работала!
На машине коллеги приложение прекрасно работало. На другом компьютере в идентичном окружении, в приложении, помещённом из git в новый каталог, после выполнения 'npm install', 'npm start' появлялись доселе невиданные ошибки.
Проблема была вызвана тем, что файл 'package-lock.json' отсутствовал в git-репозитории. Поэтому, при инсталляции пакетов, все зависимости второго и более уровня (естественно, не прописанные в package.json), инсталлировались максимально свежими. На компьютере коллеги всё было хорошо. На тестируемом компьютере подобралась несовместимая совокупность версий.
package-lock.json — to git!
Возвращаясь от лирического отступления. Файл 'package-lock.json' содержит список всех модулей, инсталлированных локально для нашего приложения. Наличие этого файла позволяет воссоздать один-в-один набор версий модулей.
Резюме: не забываем класть в git и вкючать в файл поставки (инсталляции) приложения!
Полезно: если файл 'package-lock.json' отсутствует, но есть каталог 'node_modules' со всеми необходимыми модулями, файл 'package-lock.json' можно воссоздать:
npm shrinkwrap
rename npm-shrinkwrap.json package-lock.json
На этом можно пока поставить точку. Не вошедшая информация является приёмами упрощения кода, используемым нашей командой.
При обнаружении ошибок буду стараться их быстро исправлять!