Пишем компилируемый сервер на JS
Ситуация прямо сейчас
Согласитесь, то, что JS подходит не только для браузера - уже не новость. С появлением Node.js мы все поняли, что JS, как и любой другой язык, всего лишь инструмент, а как его использовать - дело фантазии, ну и, пожалуй, API.
Спустя десять с лишним лет ситуация на поле серверного JS изменилась несущественно. Основные пути использования - Node и Deno, для любителей особых ощущений - fpm-решения.
Болячки Node известны всем. Deno частично решает их, но остаётся всё тем же наследником, перенося некоторые непонятные решения. К примеру, когда я смотрел последний релиз Deno, в котором появилась команда deno compile
, я был удивлён тем, что на выходе мы получаем бинарник размером ~36 мегабайт. В Node же есть только костыльные сторонние решения, которые чаще всего приводят к тому, что ваше приложение либо не собирается, либо не работает после сборки, из-за того, что какие-то модули не найдены.
Почему так происходит? Потому что все решения для ноды сейчас работают по принципу упаковки всего необходимого в одну “корзину”. В итоговый файл включается рантайм ноды, нужные модули и ваши исходные коды. Естественно, на этапе этой, с позволения сказать, “компиляции”, резолвится далеко не всё, поэтому если у вас работает собранное таким образом приложение - это большая удача.
Какие есть решения? Пожалуй, первое и самое логичное - писать не на JS. Но… как? Давайте признаем, что большинство из тех, кто пишут на клиентском или серверном JS хорошо знают только этот язык. По крайней мере, я буду говорить за себя.
И вот, в 2019 году появился человек, который решил помочь таким, как я. Уж не знаю, из побуждений сочувствия или саморазвития, но получилось у него суперски.
Фабрис Беллар и QuickJS
Вы можете не знать это имя, но вы знаете его дела. Фабрис Беллар, французский математик и программист, прежде всего известен четырьмя вещами:
- Эмулятор QEMU;
- C-компилятор Tiny C Compiler;
- Конечно же, FFmpeg;
- Конечно же, Git.
В 2019 году к этому списку прибавилась ещё одна интересная вещица: рантайм для JavaScript, написанный на C, не использующий AST и обеспечивающий практически бесшовную интеграцию со всякими POSIX-штуками — QuickJS.
На своем сайте Беллар называет QJS “маленьким встраиваемым движком JavaScript”. Это действительно так: полный размер движка всего 620 килобайт, при этом он поддерживает практически все стандарты ES2020.
Всё это здорово, но давайте трезвым взглядом посмотрим на то, чем QJS является прямо сейчас:
- Это довольно быстрый маленький JS-движок, способный составить конкуренцию даже V8 (но без JIT);
- Это полная поддержка ES2020, но не без странностей. К примеру, вывод объектов в консоль покажет вам только
[object Object]
; - Это очень скудная стандартная библиотека. Фактически, всё, что вы можете - взаимодействовать с файлами;
- Это полное отсутствие методов для взаимодействия с сетью.
Последний пункт расстроил меня больше всего. Мне очень понравились идеи легковесности и компилируемости, которые несёт в себе QJS, но серверный JS без самого сервера — штука довольно-таки бесполезная.
Исходные коды QJS доступны на гитхабе всем желающим. Я решил взглянуть на них и понял, что даже несмотря на то, что на C я никогда всерьёз не писал, понять, что там происходит, оказалось не так сложно. И именно тогда во мне зародилась идея добавить в этот движок чуть-чуть сервера.
А как работать-то будет
QuickJS предполагает следующий процесс:
- Сам функционал какого-либо модуля пишется на C;
- На C же пишется “прослойка”, использующая QJS-api, которая говорит интерпретатору или компилятору, что вот тут-то мы и вызываем C-функции;
- Внутри JS-скриптов мы импортируем JS-переменные из скомпилированной shared-библиотеки.
В итоге мы получаем простое решение: если мы хотим добавить что-то в QJS, нужно найти или написать новый функционал на C, а затем просто написать API-прослойку.
Если честно, в этой работе я не сделал ничего особо важного. Я нашёл сервер на C, который показался мне идеально подходящим для моей задумки — библиотеку httpserver.h. Взяв его за основу, я написал немного связующего кода, и получился QuickWebServer — динамическая библиотека, привносящая в QJS функционал веб-сервера с Express-подобным API. Давайте же посмотрим, как оно работает!
Первый сервер на QJS
Прежде всего, рекомендую вам скачать, собрать и установить QuickJS. Лишним точно не будет. Скачать можно из этого репозитория. Далее, для пользователей Mac и Linux проблем никаких - сперва запускаете make
и ждёте, пока проект соберётся, а затем make install
, чтобы все файлы поместились в нужные папки. Для пользователей Windows тоже можно собрать, но я, к сожалению, не проверял.
Также давайте убедимся, что у вас установлен Node.js и NPM. Это выглядит нелогичным, ведь мы не будем использовать Node, но установка пакетов удобнее с помощью NPM.
Давайте создадим тестовый проект, к примеру, в папке first-server
.
# ~/first-server npm init -y
Теперь нам нужно скачать и собрать QuickWebServer (далее - QWS). Он опубликован в NPM:
npm i -S @lyohaplotinka/quickwebserver
Появилась папка node_modules
, а также началась сборка shared-библиотеки. Это может занять определённое время, так что можем отвлечься.
Пока что начнём писать код нашего сервера. Первым делом импортируем библиотеку:
import QuickWebServer from './node_modules/@lyohaplotinka/quickwebserver/src/QuickWebServer.js';
Важное замечание: QuickJS не Node.js, поэтому он:
- Не ищет модули автоматически в node_modules;
- не умеет загружать index.js из папки;
- требует всегда указывать расширение файла после точки.
Двигаемся дальше. Создадим экземпляр:
import QuickWebServer from './node_modules/@lyohaplotinka/quickwebserver/src/QuickWebServer.js'; const server = new QuickWebServer();
Ну а следующий шаг - написание обработчика маршрута - вызовет явное дежа-вю у тех, кто хоть раз писал серверы на Express:
import QuickWebServer from './node_modules/@lyohaplotinka/quickwebserver/src/QuickWebServer.js'; const server = new QuickWebServer(); server.get('/', async (request, response) => { response.type('text/html'); response.send('Hello from QuickJS!'); });
Осталось лишь начать слушать соединения:
import QuickWebServer from './node_modules/@lyohaplotinka/quickwebserver/src/QuickWebServer.js'; const server = new QuickWebServer(); server.get('/', async (request, response) => { response.type('text/html'); response.send('Hello from QuickJS!'); }); server.listen(3000);
Код выше - минимальный пример работы. О, кажется, у нас завершилась сборка. Запустим!
# ~/first-server qjs ./server.js
Открываем в браузере http://localhost:3000
и видим:
Hello from QuickJS!
В целом, уже победа: у нас сервер на JS, но не Node.js. Круто!
В целом, всё можно так и оставить, и использовать QJS как рантайм. Но если мы хотим скомпилировать приложение, чтобы использовать всего лишь один бинарник, нам нужно сделать ещё кое-что.
Компилируем сервер
Сейчас QWS не часть QuickJS, а динамическая библиотека. Чтобы не таскать её за собой, нам нужно собрать программу со статической линковкой. Признаюсь честно, мне эта операция показалась не самым лёгким занятием, поэтому я решил это дело немного автоматизировать.
В составе пакета quickwebserver
распространяется скрипт create-build-makefile.sh
. Он, собственно, создаёт Makefile, в котором прописаны все необходимые шаги для сборки. Вызовем его так:
# ~/first-server ./node_modules/@lyohaplotinka/quickwebserver/create-build-makefile.sh ~/first-server/server.js
Как видите единственный аргумент - абсолютный путь к главному файлу нашего приложения. После выполнения команды рядом с файлом server.js
появится Makefile
.
Остался один шаг до сборки. Откроем server.js
и поправим импорт QWS так, чтобы подключалась версия для статической сборки:
import QuickWebServer from './node_modules/@lyohaplotinka/quickwebserver/src/QuickWebServer.build.js';
Время выполнить самый простой шаг - просто вызвать команду make
. Совсем скоро рядом появится исполняемый файл server
. Если его запустить и перейти на http://localhost:3000
, мы увидим всё ту же приятную фразу:
Hello from QuickJS!
Отдельно радует размер исполняемого файла - чуть больше мегабайта. Кстати, при компиляции из QuickJS можно “выпилить” какие-либо фичи языка, если они не используются. Так вы сократите размер файла.
Очень важно, что это никакой не “хак”, типо упаковки рантайма и кода в один пакет чисто “для виду”. При желании вы можете использовать QuickJS не для компиляции в бинарник, а для превращения вашего кода в C.
А зачем это вообще нужно?
Это извечный вопрос. Однако, у меня есть пара ответов.
Во-первых, затем, зачем я вообще что-либо делаю всегда: это просто прикольно. Ну, круто же иметь сервер в одном бинарнике!
Во-вторых, это быстро. Почти настолько же, как Node.js, но куда более транспортабельно и без необходимости в рантайме. И гораздо меньше по размеру.
В-третьих, некоторые люди действительно не хотят светить свой код. Компиляция позволяет усложнить возможность заглянуть в наши каракули.
В-четвертых, мне уже давно хотелось бы иметь какую-нибудь маленькую альтернативу Node.js, созданную для каких-нибудь простецких целей. Поэтому я лично рад, что у меня получилось это реализовать вот так.
Резюме
В итоге, конечно, всё это пока что несерьёзно. Нет HTTP-клиента, нет библиотек для работы с БД. Да и сама версия сервера ещё нестабильная, к примеру, нет возможности принимать от клиента файлы.
Во всём Интернете сейчас всего лишь один сайт работает на QWS - и это сайт его документации (https://qws.lyoha.info). Он, кстати, загружает доки из маркдауна и рендерит их на стороне JS. Уже что-то!
Тем не менее, эта работа меня сильно вдохновила. Поэтому я завёл на гитхабе организацию QuickJS Web Project, в которой хочу попытаться привнести в QJS всё то, что нужно для работы хорошего серверного движка. Если вы чувствуете силы и желание - я буду только рад работать вместе :)
Вот так вот можно написать свой сервер на JS, скомпилировать его и почувствовать себя немного серьёзнее, пусть даже всё это появилось из-за несерьёзного порыва любопытства.