В браузер!

Как запустить Node.js (приложения) в браузере?

Загрузка Node.js в браузере - просмотр не требуется

Есть несколько причин, по которым разработчик JavaScript может захотеть запустить код Node.js в браузере, в том числе:

  • разработал приложение Node.js - теперь хочу предложить его онлайн
  • нашел полезный пакет npm, который полагается на Node.js
  • хотите использовать API-интерфейсы Node.js (буфер, шифрование, форк, события, потоки и т. д.)
  • предпочитают писать модули в стиле CommonJS с require

Существующие варианты

Один будет ряд решений, выполняющих хотя бы некоторые из вышеперечисленных пожеланий, таких как webpack.js.org, browserify.org или nodular.js. Однако все эти подходы основаны на той или иной форме преобразования исходного кода - либо заранее (веб-пакет и просмотр), либо во время выполнения (узловой). В конце концов, эти решения могут быть абсолютно замечательными и достаточными для ваших целей, но на самом деле они просто пытаются имитировать или подражать определенному поведению. Довольно легко создавать программы на Node.js, которые не будут вести себя так же в браузере:

Nodular прерывает выполнение скрипта (выдает исключение), когда он попадает в require, загружает требуемый модуль асинхронно и пытается «возобновить» прерванный скрипт, выполняя магические действия над его исходным кодом. Это не работает, если require встречается в управляющих структурах или является частью сложных выражений.

Webpack, пожалуй, самый сложный продукт, он отлично справляется с обработкой модулей CommonJS и неплохо справляется с имитацией API-интерфейсов Node.js. Несмотря на преобразование ваших источников AOT, он позволяет отлаживать работающее приложение на основе ваших источников благодаря картам источников. Однако в конечном итоге тонкие различия в API (например, точное время process.nextTick vs setImmediate vs setTimeout) могут легко выявить фундаментальную разницу.

Мой подход

Как объяснено ниже, я загружаю JavaScript-часть Node.js внутри веб-воркера. Все подробности реализации будут предоставлены бесплатно! Я не написал ни одной строчки кода REPL, показанного на заглавном изображении этого поста. Не знаю, как отображаются объекты (макс. Уровень отображаемой вложенности, цвета и т. Д. Вроде [Circular]), не знаю, где код, который заставляетprocess.nextTick срабатывать до setImmediate. Я загружаю Node.js и подключаю его к элементу управления xtermjs.org.

Мотивация

API-интерфейсы Node.js находятся в движении, и, хотя есть полифилы практически для всего, мне нравится идея просто использовать исходные API напрямую. Свойства и тонкости Node.js должны быть доступны скриптам изначально. Кроме того, я хочу разрешить запуск сценариев Node.js напрямую и без изменений, без предварительной обработки. Это позволяет использовать REPL и онлайн-IDE, которые не полагаются на сервер для выполнения тяжелой работы.

Кроме того, что-то внутри меня просто хочет увидеть конвергенцию этих двух миров JavaScript (Node.js и браузеры). Для меня преобразование кода больше похоже на обходной путь, чем решение. Представьте, что 32-битные приложения требуют AOT-транспиляции для работы на 64-битной машине вместо реальной поддержки оборудования и ОС.

Задача: семантика CommonJS

Одна из самых больших проблем - это, конечно, правильная работа с модулями CommonJS и require. Поддержка синхронного характера require достигается с помощью webpack и browserift путем объединения (фактически предварительной загрузки) всех сценариев, которые могут быть required во время выполнения. Nodular пытается подделать семантику блокирующего вызова.

Node.js реализует require через операции синхронной файловой системы, поэтому давайте на секунду уменьшим масштаб и подумаем о вариантах:

  1. У браузера в любом случае нет прямого доступа к FS главного компьютера (и это нормально), так что это не проблема.
  2. Можно создать ФС в памяти (с использованием переменных или локального хранилища), которая, очевидно, будет поддерживать синхронный доступ. Что ж, по сути, это то, что делают существующие решения, объединяя файлы заранее.
  3. Можно рассматривать сетевые ресурсы как FS. Обычные URL-адреса похожи на файлы, доступные только для чтения, в то время как облачное хранилище (GitHub, OneDrive, Google Drive, iCloud,…) может действовать как файловая система с доступом на запись. Но веб-запросы всегда асинхронны, верно? Нет! Для ясности, я не предлагаю использовать (теперь устаревшие) функции в основном потоке, но для рабочих они разрешены и прекрасны - мы все равно должны загружать Node.js в рабочем потоке, но об этом позже. .
  4. Любая комбинация 2 и 3. Например, как и на реальных компьютерах, операции записи могут сначала попасть в некоторый кеш в памяти (поэтому они будут сверхбыстрыми), а затем сохраняться в фоновом режиме.

Поэтому при запуске в воркере кажется, что require может работать без связывания! 🎉 Для своего прототипа я просто направляю все записи в память и обрабатываю чтения, сначала просматривая путь в памяти и выполняя HTTP-запрос в случае сбоя (по URL-адресу, вычисленному из пути). Очень простой, но пока достаточный.

Теперь, как на самом деле загрузить Node.js?

Далее я дам обзор своего подхода и некоторых технических деталей.

Архитектура

В чрезмерно упрощенном смысле Node.js выглядит следующим образом:

Стандартная библиотека, написанная на JavaScript, предоставляет высокоуровневый API Node.js, к которому вы привыкли. Этот API реализован поверх собственных низкоуровневых привязок , которые обеспечивают доступ к функциям ОС, таким как файловая система или процессы. Привязки передаются в стандартную библиотеку через один объект: process. Другими словами, process - единственное, что в процессе Node.js происходит не со стороны JavaScript - подробнее об особенностях позже. V8 - это движок JavaScript, используемый для выполнения любого кода JavaScript, то есть стандартной библиотеки и всех скриптов.

Стратегия

Чтобы полностью работать в браузере, части C / C ++ должны быть заменены реализациями JavaScript:

В частности, часть V8 по сути бесплатна, поскольку браузеры выполняют JavaScript из коробки (в случае Chrome, используя V8 😏). Замена креплений означает настоящую работу с нашей стороны.

Я был рад увидеть, что огромное количество деталей реализации действительно является частью стандартной библиотеки, а не привязками - привязки по сути являются максимально низкоуровневыми, но при этом независимыми от платформы.

Имитация запуска

Как уже упоминалось, привязки предоставляются через объект process. В стандартной библиотеке есть функция начальной загрузки, которая принимает process в качестве аргумента и подготавливает большое количество API. В частности, есть функция process.binding, которая принимает имя типа 'constants', 'buffer', 'fs', 'os', 'tcp_wrap' или 'constants' и возвращает объект с собственными функциями - этот механизм сравним с require возвращающими модулями, но на более низком уровне.

Я заметил, что неправильный вызов собственных функций, предоставляемых process.binding, часто приводит к сбою процесса, например process.binding('os').getCPUs(). Я предполагаю, что проверка арности и типа не выполняется. Это, конечно, не проблема, поскольку стандартная библиотека вызывает эти функции правильно и не предоставляет их напрямую программисту.

Что касается моего порта, я фактически начал с передачи process = {} функции начальной загрузки, не имея ни малейшего представления о том, как все работает и чего ожидать. Я позволяю себе руководствоваться ошибками, выдаваемыми стандартной библиотекой. Одна из первых ошибок связана с отсутствием функции binding. Я реализовал binding как выдачу ошибки всякий раз, когда он вызывается с именем, которого я раньше не видел. В случае такой ошибки я бы просто добавил код для возврата пустого объекта для этого имени. Если что-то отсутствует в этом объекте, будет выдана ошибка и т. Д. Преимущество этого ленивого подхода заключается в том, что места ошибок обычно указывают именно на то, что вам нужно реализовать дальше. Никакого предварительного расследования или контекста не требуется. Если вы не уверены в реализации функции, реализуйте ее как debugger; и подождите, пока отладчик не остановится на ней. Анализ аргументов, вероятно, даст вам ключ к разгадке. Однако некоторые функции изменяют глобальное состояние (например, process.binding(‘fs’).stat), и в этом случае я проверил исходные коды C, чтобы понять, что должно произойти.

В какой-то момент вызов функции начальной загрузки не приведет к ошибке, ура!

Что звонить дальше?

Ничего, все готово! Загрузчик также позаботится обо всем, что должно произойти после начальной загрузки. Если process.argv содержит сценарий в качестве аргумента, этот сценарий будет вызван. Если этого не происходит, запускается REPL. Slick.

Следующие шаги

Есть еще чем заняться, но отсюда все катится под гору! Предложения:

  • Подключите process.binding('tty_wrap').TTY к чему-нибудь получше, чем какой-нибудь <textarea>. Я выбрал xtermjs.org.
  • Возможно, загрузка прошла успешно, но 99% привязок все еще отсутствуют ... как только вы попытаетесь загрузить сценарий или использовать REPL для вещей, выходящих за рамки "Hello World".toUpperCase(), снова начнут появляться ошибки. Но мы уже знаем, как их лечить. Заполнение пробелов.

Доброта веб-воркеров

Напомним, что мы запускаем все в воркере, чтобы получать синхронные HTTP-запросы. В любом случае это отличная концепция по нескольким причинам:

  1. Изоляция. В некотором смысле имеет смысл, что загруженный экземпляр Node.js не запускается в том же контексте / потоке, что и основной процесс (который может играть роль ОС). Взаимодействие с пользовательским интерфейсом можно обрабатывать через передачу сообщений, что в любом случае является основной концепцией Node.js.
  2. Изоляция: если предполагается, что несколько экземпляров Node.js могут запускаться одновременно, область global должна быть каким-то образом отделена, бесконечный цикл в одном экземпляре не должен влиять на другой и т. д. .
  3. Изоляция. Рабочие прекрасно работают с child_process API. Несмотря на то, что я до сих пор не касался этого, имеет смысл думать о рабочих как о процессах. Вы можете создавать их и удалять независимо от их внутреннего состояния. У экземпляров Node.js есть ресурсы, такие как очереди сообщений и таймеры, разрушение всего мира, в котором они живут, - самый простой способ убедиться, что ни один из этих ресурсов не вызывает утечки памяти.

Ограничения

Я столкнулся с рядом проблем, на которые пока не могу дать правильных ответов - предложения более чем приветствуются! Вот подборка:

  • Node.js предоставляет сетевые API низкого уровня, например сокеты TCP. В браузерах нет такого низкоуровневого API. Все, что вы получаете, - это API более высокого уровня, такие как HTTP или веб-сокеты. Поскольку http API Node.js полагается на низкоуровневые API, его тоже нельзя использовать. Думаю, в таких случаях уместно ввести, скажем, http polyfill браузера. С другой стороны, можно было бы реализовать TCP в стиле передачи памяти для связи между рабочими! Запросы, нацеленные на внешний мир, могут быть развернуты, проверены на предмет наличия HTTP (например, из брандмауэра) и воспроизведены с помощью собственных HTTP API браузера. По сути, имитировать NAT с брандмауэром только для HTTP?
  • vm API предоставляет методы для выполнения JavaScript в различных контекстах. Таким же образом выполняются сценарии required или работает REPL. В некотором смысле это то, что делает eval (и это то, что я сейчас использую для подделки vm). Однако на самом деле vm - это нечто большее, что становится очевидным при просмотре REPL: выполнение const x = 3 объявляет переменную x и делает ее доступной для последующих операторов. Это невозможно сделать с eval, поскольку const автоматически активирует строгий режим, а eval не вводит переменные в окружающую область видимости в строгом режиме. В частности, eval("var x = 3"); eval("x"); будет делать то, что вы ожидаете, а eval("const x = 3"); eval("x"); - нет. Облом. Может быть, возможен какой-то обман, используя importScripts в еще одном воркере, не уверен. Это также может решить следующую проблему, заключающуюся в том, что eval работает бесперебойно. Ctrl+C должен прерывать while(true); в REPL. К счастью, eval кажется достаточным для require, так что пока эта проблема носит скорее косметический характер.

Заключение

Мой порт очень прототипичен. Хотя он успешно использует немодифицированные пакеты npm, такие как cowsay или chalk (см. Изображение в заголовке), многие привязки отсутствуют. Тем не менее, я очень доволен результатами, поскольку они показывают, что загрузка Node.js в браузере работает! Что касается охвата, моя следующая цель - запустить npm в браузере - было бы здорово не только выполнять, но и загружать пакеты на лету.

Моя конечная цель - собрать вместе эти два, казалось бы, расходящихся мира JavaScript, без особых усилий. Мы привыкли к миру, в котором JavaScript преобразуется в другой JavaScript для выполнения. Это всегда один и тот же язык, но существует странное разнообразие вкусов, которые не могут быть полностью совместимы и взаимозаменяемы. Я надеюсь, что такие вещи, как оператор импорта ES6 в какой-то момент заставят миры снова сойтись.

Покажи мне код!

Код можно найти по адресу https://github.com/olydis/node-in-browser

Предупреждение: это взломанный, беспорядочный прототип, а не красивый образец программной инженерии.