tl;dr
С node-direct можно заливать серверные .js файлы и обращаться к ним так же, как к .php скриптам: example.com/foo.srv.js.
Установка.
npm install -g node-direct
Конфигурация nginx.
location ~ \.srv\.js$ { root <path_to_website_files>; proxy_pass http://localhost:<port>; proxy_set_header X-Requested-File-Path $document_root$uri; }
Запуск.
node-direct --port=<port>
Скрипт foo.srv.js, где
req
иres
созданы сервером express.
module.exports = function(req, res) { const someModule = require('some-module'); res.send('Hello world!'); }
Введение
Когда NodeJS стал более-менее популярным, мне было нелегко осознать, что с ним всё не так просто, как с PHP. Используя последний, можно было создать .php файл, залить его на сервер, обратиться по адресу example.com/путь/имяфайла.php и радоваться. Такая простота развертывания скриптов служила одной из причин, почему "пых" стал таким популярным.
В свою очередь, NodeJS, независимо от сложности приложения, заставляет очень многие вещи делать руками.
- Гоняй каждое приложение на собственном порту
- Определи роуты самостоятельно
- Настрой деплой
- Убедись, что приложение работает и после перезагрузки сервера
- Не забудь сделать так, чтоб при изменении файлов, NodeJS сервер перезагружался
pm2 несколько облегчает просесс обновления и запуска приложения после перезагрузки или после брошеных приложением исключений. Но с ним, во первых, нужно набить руку, во-вторых, запуск очередного приложения заставлыть повторять рутинные действия.
Всё вышесказанное является, скорее, преимуществом, чем недостатком, если вы разрабатываете крупное приложение. Но что если приложение очень простое? Что если это вовсе не приложение, а обычный сайт, с небольшой "апишкой" (например, прокси для кроссдоменных запросов)? Что если на VPS гоняется много сайтов, и для каждого настраивать NodeJS сервер — лень.
node-direct — тулза, которая позволяет разворачивать серверные JavaScript скрипты так же просто, как и .php:
- Залил файл
- Обратился к нему так: example.com/путь/имяфайла.srv.js
При этом, один инстанс node-direct можно использовать на разных сайтах.
Конфигурация
За большую часть "магии" отвечает конфиг nginx. Его нужно научить обрабатывать запросы к файлам .srv.js (можно юзать любое расширение).
location ~ \.srv\.js$ {
root <path_to_website_files>;
proxy_pass http://localhost:<port>;
proxy_set_header X-Requested-File-Path $document_root$uri;
}
path_to_website_files
— путь к файлам сайтаport
— порт, на котором живет node-direct (по умолчению, 8123)
Пример полного конфига:
server {
listen 80;
server_name example.com;
# Serve static files
location / {
root /var/web/example.com/public;
index index.srv.js index.html index.htm;
}
location ~ \.srv\.js$ {
root /var/web/example.com/public;
proxy_pass http://localhost:8123;
proxy_set_header X-Requested-File-Path $document_root$uri;
}
}
Т. е. статика сервится при помощи nginx (известно, что это в несколько раз быстрее статики, которую гоняет express
без кеширования), плюс, в список поддерживаемых серверных файлов можно добавить даже .php (юзабельно для легаси проектов).
Использование
node-direct --port=8000
node-direct использует старый добрый Express. Серверные файлы должны экспортировать функцию, принимающую request
и response
.
Hello world:
module.exports = function(req, res) {
const someModule = require('some-module');
res.send('Hello world!');
}
JSON API:
module.exports = function(req, res) {
if(req.method === 'POST') {
req.json({
message: 'Everything is awesome'
});
} else {
req.status(400).json({
message: 'Only POST requests are allowed'
});
}
}
Рендеринг
const fs = require('fs');
const ejs = require('ejs');
const template = ejs.compile(fs.readFileSync('./templates/index.html'));
module.exports = function(req, res) {
res.type('html').send(template({ foo: 'bar' }));
}
За подробной информацией обратитесь к документации Express.
Пример гипотетического приложения:
/package.json - содержит dependencies and devDependencies специально для текущего проекта
/index.html - главный HTML файл
/js/app.js - client-side JavaScript
/css/style.css - стили
/node_modules/ - локальные модули установленные с помощью "npm install" (к папке нужно закрыть доступ извне)
/foo/index.srv.js - JSON API, позволяющий делать запросы на /foo/
/bar/index.srv.js - динамическиая HTML страница, которая возвращается при обращении к /bar/
Флаги
Общие
--port
— порт сервера node-direct (8123 по умолчанию)
Режим standalone
В этом режиме создается HTTP сервер, раздающий статику и не ребующий nginx. Режим используется разработчиком для запуска на локальной машине.
--standalone
— включает режим standalone
--root
— путь к статичным файлам (process.cwd()
по умолчанию)
--ext
— расширение серверных JS файлов (.srv.js по умолчанию)
node-direct --port=8000 --standalone --root=./foo/bar --ext=.serverside.js
Запуск node-direct после перезагрузки сервера (требуется только один раз)
Можно добавить новое задание в crontab, вызовом crontab -e
и добавлением новой задачи в файл.
@reboot <path_to_node> <path_to_installed_module> [<flags>]
path_to_node
— абсолютный путь к бинарнику NodeJS (можно вызватьwhich node
)path_to_installed_module
— абсолютный путь до установленного node-directflags
— флаги
Пример:
@reboot /usr/local/bin/node /usr/local/lib/node_modules/node-direct/index.js --port=8000
Проблемы
Кеширование запрашиваемых модулей
Как известно, NodeJS кеширует значения, которые возвращает функция require
. Когда вызывается require('foo')
два или более раз, функция возвращает одно и то же значение. node-direct обновляет кеш автоматически, когда файл .srv.js изменен (например, вы залили новый файл на сервер) и перезагружать node-direct нет необходимости. Проблема может возникнуть тогда, когда .srv.js запрашивает другие модули.
// foo.srv.js
module.exports = function(req, res) {
const bar = require('./bar');
// ...
}
Когда меняется foo.srv.js, кеш обновляется, как и ожидается, но когда меняется ./bar, его значение остаётся прежним. node-direct мог бы обновлять все запрашиваемые модули самостоятельно, но это бы вызвало сайд-эффекты с непредсказуемым поведением в других модулях. Эту проблему можно решить созданием вотчера, котогрый чистит кеш, когда заданный модуль обновился.
В примере ниже для ./bar включена горячая замена, а для ./baz — нет.
// foo.srv.js
// для большего количества модулей с горяей заменой, нужно юзать цикл
const fs = require('fs');
const barPath = require.resolve('./bar');
const watcher = fs.watch(barPath, (eventType) => {
if (eventType === 'change') {
delete require.cache[barPath];
watcher.close();
}
});
module.exports = function(req, res) {
const bar = require('./bar');
const baz = require('./baz');
// ...
}
Если выглядит слишком сложно, юзайте небольшой модуль fresh-up, который делает то же самое.
// foo.srv.js
// для большего количества модулей с горяей заменой, нужно юзать цикл
const freshUp = require('fresh-up');
freshUp(require.resolve('./bar');
module.exports = function(req, res) {
const bar = require('./bar');
const baz = require('./baz');
// ...
}
Потенциальная уязвимость: хедер X-Requested-File-Path
Как вы, возможно, заметили, nginx передаёт адрес запрашиваемого файла node-direct серверу в виде хедера X-Requested-File-Path
. Используя этот хедер, злоумышленник может вызывать произвольные JavaScript файлы на вашем сервере. Для того, чтоб сделать что-то плохое, хакеру нужно знать адрес "опасного" файла, относительно рута. Для закрытия уязвимости, нужно запретить обращаться к порту node-direct извне.
Вот, как это можно сделать в Linux используя файервол ufw
sudo ufw deny 8123
justboris
У вас же есть
standalone
mode, в нем все хорошо, зачем вообще нужнен этотX-Requested-File-Path
?Finom
Сходу даже не пойму, как настроить тулзу для нескольких доменов.
justboris
Несколько доменов по-прежнему настраиваются в nginx. Потом он передает в node-direct что-то типа
Finom
Я думал, что вы имеете в виду, что можно как-то обойтись без настройки nginx вовсе. А так, не вижу смысла сёрфить статику нодой если есть возможность сёрфить её быстрее.
justboris
Ну и пусть nginx раздает статику, а все файлы *.srv.js завернем на node.
Мой пойнт был в том, что путь до файла можно передавать в url, который для этого и придуман, а не в отдельном заголовке.