Impress: многоцелевой сервер приложений для Node.js

    Несмотря на заметные успехи, Node.js все еще остается специализированной технологией, которой преимущественно закрывают узкие места в системах, написанных в другом стеке технологий. Причина такого положения кроется в том, что сама по себе нода не имеет многих библиотек, к которым мы привыкли на других языках и которые обеспечивают быструю разработку именно прикладного ПО. Например, для того, чтобы разделить в коде обработчики разных URL, отдавать статические файлы, организовывать сессии, запускать нескольких потоков, иметь доступа к БД, кешировать данные в памяти, разграничивать права пользователей, иметь логи и ротировать их, создавать сетевое API, рендерить шаблоны, настраивать URL-реврайтинг, обеспечивать быструю доставку событий с сервера на клиенты, для всего этого, и многих других задач, используются отдельные библиотеки (модули). Разные модули написаны разными разработчиками, сложно стыкуются, конфликтуют. В общем, мы решили, весь этот набор обязательного функционала, необходимого практически в каждом веб-приложении, объединить в один сервер приложений и повысить, таким образом, связанность кода, сделать ядро сервера приложений монолитным и более согласованным, чем решения, собранные из отдельных библиотек. Проект Impress уже анонсировался как прототип, а сейчас предоставляет весь необходимый арсенал для быстрой разработки приложений, что протестировано на десятке живых проектов. Impress значительно отличается от другой широко распространенной платформы так же, как импрессионизм отличается от экспрессионизма, то есть, производит целостное, хорошо продуманное эстетическое впечатление, в противоположность внезапному выбросу эмоций. Но мы, не вовлекаясь в критику чужого кода, перейдем к демонстрации конструктивных особенностей Impress.

    Немного метрик кода


    • Размер ядра Impress (с максимальной связанностью кода) /lib/impress.js — 44Кб
    • Размер всего кода Impress (с высокой связанностью, но не обязательным подключением) /lib/* — 110Кб
    • Размер внешних библиотек (с необязательной загрузкой), все что в /node_modules — 56Мб
    • Из них модуль geoip-lite со своей базой данных — 40Мб
    • Способ загрузки библиотек ядра: экзотический (описан тут — Паттерны JavaScript модулей в Impress для node.js и браузеров).
    • Возраст проекта: 3½ месяца (до этого еще месяц в качестве прототипа, до публикации в npm и github).
    • Интенсивность развития: за это время вышло 47 версий, т.е. что-то доделывается каждые 2-3 дня.
    • Зависимости (на текущий момент): async, cluster, colors, mkdirp, mongodb, mysql, memcached, nodemailer, geoip-lite, uglify-js, multiparty, iconv-lite.

    Возможности и сферы применения


    • Создание многостраничных веб-приложений, то есть, с серверным шаблонизатором (встроен в ядро) и с перегрузкой страниц.
    • Создание одностраничных веб-приложения с обменом данными между браузером и серверным API при помощи AJAX, с передачей фрагментов HTML или JSON, для динамической перестройки клиентского экрана из оных.
    • Создание гибридных решений, где смешаны одностраничный и многостраничный подходы.
    • Разработка сетевого API для межсерверного и клиент-серверного взаимодействия, в том числе, с браузерами, мобильными приложениями для iOS, Android и т.д.
    • Оффлайн приложения HTML5 с cache.manifest, локальным хранилищем в IndexedDB или WebSQL и возможностью работать как в онлайне, так и в оффлайне, в автономном режиме. Это не задача Impress, конечно, но есть опыт применения даже в таком нетипичном случае.
    • Обслуживание множества доменов одним сервером приложений (в том числе по маске), т.е. механизм настройки виртуалхостов составляющих одно приложение или запуск на них разных приложений (как на одном, так и на разных портах).
    • Проброс вызовов на другие серверы и порты (reverse-proxy) с поддержкой url-rewriting, настройка маршрутизации URL в config.js в формате JSON и при помощи шаблонов и регулярных выражений. Проброс можно совмещать с обработкой части вызовов в сервере приложений. При помощи проброса можно собрать одно приложение из нескольких языков, серверов и технологических стеков.
    • Отдача статики с кешированием в оперативной памяти, минификацией статических браузерных js-файлов, со сжатием gzip для сжимаемых форматов и правильной отдачей HTTP 304 (Not Modified) при получении заголовка «if-modified-since».
    • Есть встроенная система аутентификации и провайдер для хранения пользователей в MongoDB.
    • Есть встроенная система сессий с хранением их в оперативной памяти, сохранением в MongoDB, восстановлением сессий при оперезагрузке. Есть возможность делать не аутентифицированные сессии, т.е. идентифицировать и хранить состояние пользователя без регистрации.
    • Возможность работать в несколько процессов с поддержкой нескольких стратегий распределения задач по процессам и с реализацией взаимодействия между ними через IPC (в дальнейшем предполагается использование ZeroMQ для этих целей и прозрачное масштабирование на несколько серверов). Обмен сообщениями применяется если нужно наладить взаимодействие между клиентами, подключенными к разным процессам сервера.
    • Реализация двух способов приклеивания сессий к процессам: «IP sticky» (приклеивание по IP) или «cookie sticky» (приклеивание по кукизу, применяется в паре с внешним балансировщиком и мультиплексированием по портам). Это позволяет все соединения с одного IP или c одним и тем же кукизом при повторных запросах (после аутентификации) опять направлять в тот же процесс, который хранит их сессию (состояние).
    • Логирование запросов с заведением нового файла каждые сутки и удалением старых файлов (устанавливается лимит хранимой истории).
    • Сейчас заканчивается работа над CMS, которая уже встроена в ядро Impress и скоро получит админ-интерфейс для редактирования страниц.

    Ну и те возможности, о которых не буду подробно, т.к. уже писал о них и их лучше смотреть на примерах:
    • Маршрутизация URL на базе файловой системы (мапинг URL в каталоги).
    • Кеширование серверного JavaScript и шаблонов в оперативной памяти.
    • Возможность изменения кода приложений без перезагрузки основного приложения (процесс следит за изменением файлов на диске.
    • Несколько стратегий запуска: multiple, single, specializatio, sticky.
    • Возможность изменять конфиг без полной перезагрузки основного приложения (процесс следит за изменением файла config.js).
    • Поддержка SSE (Server-Sent Events) с системой трансляции событий с сервера (в стиле PUSH) через открытое соединение на клиентскую часть без постоянных запросов с клиента (в стиле PULL).

    Надстройка над драйвером доступа к MySQL:

    Так же разработан метаязык на базе синтаксиса JSON, который позволяет удобно и кратко описывать структуры реляционных БД и транслировать потом эти структуры в SQL скрипты. См. примеры в каталоге /node_modules/impress/schemas/ Для трансляции можно использовать такой код:
    var schemaCore = require('./schemas/impress.core.schema.js');
    var scriptCore = db.schema.mysql.generateScript(schemaCore, true).script;
    console.log(scriptCore);
    

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

    Примеры


    Лучше всего понимать на примерах, которых в Impress достаточно. Сразу после установки из NPM-репозитория (npm install impress) мы можем развернуть примеры, скопировав их из папки /node_modules/impress/examples/copyContentToProjectFolder в корень проекта. Для запуска желателен MongoDB для хранения сессий (в ближайшее время будут реализованы и другие провайдеры хранения сессий). Но можно запустить примеры и без БД. Если же MongoDB все же есть, то нужно в config.js установить «session.persist» в true, раскомментировать «databases.impress», в том же конфиге, и в секции «plugins.require» раскомментировать модули: «db», «db.mongodb», «impress.security.mongodb». После этого создать необходимые коллекции и индексы запустив: node setup.js и потом запустить сервер приложений: node server.js

    API (RPC): STATEful и STATEless


    Одна из основных задач, для чего разрабатывался Impress — это создание серверов приложений как на принципе STATEless (т.е. REST серверов), так и на более интересном принципе STATEful. Нужно напомнить, что REST, это когда между парой запрос/ответ ни на сервере, ни на клиенте, не сохраняется состояние объекта. В противоположность RPC, на котором основаны клиент-серверные приложения, в которых принято создавать модель в клиенте и создавать модель в сервере, связывая их интерфейсы по сети и транслируя между этими моделями события и вызовы. Вот нода позволяет развернуть модель на двух концах провода и синхронизировать через AJAX/JSON вызовы, что конечно более удобно для прикладных приложений. REST пришел в ноду из каменного века тяжеловесных веб серверов (как Apache и IIS), которые каждый раз запускали внешние (по отношению к ним) приложения, передавая им запросы HTTP протокола через CGI. Такое приложение порождает новый процесс, он должен провести инициализацию рабочей среды, т.е. установить соединения с базой, развернуть все свои данные, прочитать себе из файловой системы что-то (если нужно) и т.д. и все это лишь для того, чтобы через несколько мил миллисекунд завершить работу и освободить память, отключиться от базы. До веба я писал на языках, в которых принято STATEful API как RPC (COM, DCOM, Corba...), и для меня концепции REST всегда не хватало. И вот, наконец, после перехода в вероисповедание ноды, мне было счастье. Теперь опять можно разворачивать в памяти данные и они никуда не деваются от запроса к запросу, можно хранить увесистые сессии в оперативной памяти и не делать сериализацию/десериализацию оных при завершении и повторном запуске процессов. И мне было видение, что REST ушел в прошлое вместе с костылями типа viewstate и серверами состояний. Понял я, что STATEful API есть величайшее благо, дарованное Всевышним каждому живому существу, познавшему ноду.

    Чтобы сделать новый обработчик API-урла, нужно всего-то:
    1. Создаем папку /api/myAPI/getSomething.json/
    2. Кладем туда файл post.js и в нем пишем:
    module.exports = function(req, res, callback) {
        db.impress.collectionName.find({ fieldName: req.post.fieldValue }).toArray(function(err, nodes) {
            res.context.data = nodes;
            callback();
        });
    }
    

    3. В каталоге /api/myAPI делаем файл access.js и в нем пишем:
    module.exports = {
        guests: false, logged: true, http: true, https: true
    }
    

    Все готово, для https просто в config.js настройки ставим и в access.js запрещаем http для этой папки. Более того, создавать обработчики можно безе перезапуска сервера, просто создаем еще папку и пишем там код в файле. При первом обращении код попадает в память, при изменении файла на винте код подгружается новый в память и там сидит и ждет вызова.

    Экраны из демонстрационного приложения


    После установки и разворачивания примеров, можно увидеть такие экраны. На первом — форма регистрации пользователей, она работает при подключенной MongoDB, как и вся функциональность, связанная с аккаунтами и хранимыми сессиями (Create account, Sign In, Sign out).


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


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


    На Github: https://github.com/tshemsedinov/impress
    В npm: https://npmjs.org/package/impress
    Поделиться публикацией
    Комментарии 15
      0
      А зачем в коде добавлена «возможность» запуска этого добра в браузере?
        0
        Вы бы еще подумали над разбивкой файлов на более меньшие части. У вас в github.com/tshemsedinov/impress/blob/master/lib/impress.js и работа с печеньками и работа с кластером, sse, и куча всего еще.
          0
          1. Это только один файл global.js заточен под запуск на клиенте и на сервере. Почти все, что он содержит, действительно нужно и там и там.
          2. Ядро не очень большое (44кб), и я вижу, как его еще оптимизировать, но SSE действительно лучше вынести. Тем более, что паттерн модулей в Impress, позволяет писать код с высокой связанностью даже в разных файлах, что значительно удобнее для ядра:
          (function(impress) {
          	impress.methodName = function(callback)
          	}
          } (global.impress = global.impress || {}));
          
        +1
        Я совсем немного пишу на NodeJS, но понимаю что такой комбайн очень узкоспециализированный. Я, например, не представляю свою жизнь без шаблонизартора swig. Я уверен, что если копнуть глубже, то многие библиотеки не совсем универсальны. Вот за все остальное реально спасибо, можно хотя бы увидеть код реального проекта на NodeJS.
        • НЛО прилетело и опубликовало эту надпись здесь
            +1
            Решает, но это альтернативный подход и он позволяет писать быстрее и кода меньше, например, очень показательно в этом плане создание новых обработчиков URL для API или страниц. На Impress не нужно даже перестартовывать систему, просто создается папка, в ней делается файл get.js (или post.js, в зависимости от типа http запроса) и кода пару строк. Этот файл можно изменять и удалять без перезапуска системы, можно выложить половину приложения просто скопировав папки без перезапуска. Impress следит за файловой системой и все подтянет в память, при старый код завершит свою работу корректно, если он в момент изменения исполняется.

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

            В общем, вопрос сравнения — это отдельная статья, для меня он более эстетический, но могу предоставить и сравнительные тесты производительности.
              +2
              Было бы очень интересно взглянуть на такое сравнение.
                0
                Я думаю, что правильнее, если я закажу такое сравнение сторонним экспертам.
            0
            Я думал использовать внешние шаблонизаторы, но разочаровался в их производительности и концептуальности. Например, если посмотреть в код EJS, то там куча синхронных операций, в том числе с файловой системой, что для неблокирующей ноды вообще смертельно. Я не знаю как его используют, разве что, вовсе не смотрят код подключаемых модулей. И встроенный в Impress шаблонизатор очень сильно структурно срощен с кешем в оперативной памяти, не знаю, можно ли будет какой-то внешний шаблонизатор так встроить. Мне насоветовали несколько, но ни один не приглянулся. А начав использовать Impress для своих же проектов, мы с коллегами обнаружили, что очень мало используем серверный шаблонизатор, в основном пишем API и передаем JSON на клиент, где уже рендерим в зависимости от платформы (браузер, мобильное приложение или даже встроенная система на контроллере, где та же нода работает). Использование же клиентских шаблонизаторов не регламентируется и очень просто реализуется.
              0
              Вообще-то шаблон компилируется единожды. Потом постоянное переиспользуется.
              А поповоду, где рендерить на клиенте или сервере, споров было много. Фейсбук и твиттер походив по граблям вернулись к серверному.
            +1
            Есть такой вопрос провокационный — а почему Вы не пишите на node.js?

            Зачем Вы изобрели собственный механизм импорта\экспорта?
            Почему не используете сторонние клиентские библиотеки — для работы с датой, строками и т.д? Всякие lodash-underscore уже стандарт де-факто.

            Я уж молчу про то, что использование global — жуткий моветон, который еще как-то можно простить в тестах, но в продакшен-коде — ни-ни.
              0
              Я знаком с мнением, о том, что глобальные объекты это плохо, но считаю, что плохи они только в глубоко прикладном коде (при моделировании предметной области, при очень специфического кода для конкретных задач), а вот при разработке абстрактного кода, это дает серьезные преимущества. Давайте не мыслить шаблонами, не будем делать из программирования догматическую веру, лучше рассмотрим аргументы и примеры. Сначала статья про этот подход к модульности: Паттерны JavaScript модулей в Impress для node.js и браузеров. Понятно, что это не серебряная пуля и он имеет свою сферу эффективного применения и свои ограничения. А вот почему в Impress такой подход себя оправдывает:
              • Позволяет разбивать один модуль на несколько файлов, не теряя высокой связанности кода, его монолитности. Широко распространенный способ импорта/экспорта через require и exports не позволяет этого, он наоборот нужен для того, чтобы делать слабосвязанный код, это не хорошо и не плохо, просто это инструменты для разных целей.
              • Дает возможность делать многослойные абстракции, то есть, сначала определять общий для модуля код и структуры данных, а потом их выборочно расширять. Например, сначала загружается db.js, а потом один или несколько драйверов БД: db.mongodb.js, db.mysql.js, db.memcached.js, но все они встраиваются в структуры нижнего слоя, так, что основные методы драйверов этих БД совместимы и соединения они хранят в одном хеше. А потом опционально можно расширить интерфейс для MySQL, подгрузив db.mysql.introspection.js с методами интроспекции и db.mysql.schema.js с транслятором схем и т.д.
              • Ну и основная причина: этот метод значительно сокращает код при создании обработчиков для страниц и AJAX-обработчиков. Каждый обработчик хранится в отдельном файле и если из них всех начать делать кучу require, то это бы раздувало код. Например, в любом модуле можно сразу использовать глобальный объект async, который очень часто нужен, сразу пишем async.eachSeries(...)
                0
                Ну, многослойные абстракции нам дает DI, за связанностью следит ООП, а сокращать код помогают mixin-ы.

                Я как бы не настаиваю, но есть некие best practice, игнорировать которые черевато боком, мне ли не знать.

                Но в любом случае — успехов, хоть я лично за unix-way и держусь подальше от комбайнов.
              0
              Вопрос: не возникают ли у вас проблемы с автодополнением в редакторе кода в связи с использованием глобальных переменных? Ну и если автодополнение работает — чем пользуетесь?
                0
                Дело в том, что в этом вопросе я не могу подсказать ничего дельного, на меня ориентироваться нельзя, я экстремист, фанатик и фундаменталист, работаю исключительно без автодополнения в FAR. Но я спрашу наших ребят, как у них в саблайме и вебшторме.

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

              Самое читаемое