Philippe Elsass. Demystifying Haxe to JS: Compilation, interop and bundling

В своем выступлении Филипп рассказал об основах работы с JavaScript в Haxe, о том как вызывать JavaScript-код из Haxe и наоборот и немного о том, как все это устроено изнутри.

Видео-версия доклада.

Его выступление состояло из нескольких блоков:

  • как устроены дела в чистом JavaScript
  • как работает CommonJS для создания модульного кода
  • как это ложится на Haxe
  • и, наконец, как настроить взаимодействие Haxe и JavaScript

Все эти вопросы рассматривались на простейшего простого модульного приложения, состоящего из:

  • точки входа приложения app.js, код которого имеет вид

    var div = document.getElementById('menu');
    Menu.render(div);    

  • компонента Menu с кодом:

var Menu = {
    render: function(dom) {
        dom.innerHTML = 'Menu';
    }
}    

  • и простой html-страницы:

    <div id="menu"></div>
    <!--JS order matters!-->
    <script src="menu.js"></script>
    <script src="menu.js"></script>    

В олдскульном JavaScript порядок подключения скриптов важен, все находится в глобальной области видимости (global scope) - как если бы вы писали не var Menu = ..., а window.Menu = ....

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

Чтобы избежать “загрязнения” глобальной области видимости, код помещают в приватную область видимости - в функции:

// a function scope is created for every function
function() {
    // private scope
}    

А функция, в свою очередь, может автоматически выполняться:

// an anonymous, auto-executed functions runs immediately
(function() {
        // private scope
})();

И весь код внутри этой функции будет полностью приватным.

Таким образом, можно реализовать инкапсуляцию, скрывая функции и переменные из глобальной области видимости, помещая код в анонимные автоматически исполняемые функции:

// an anonymous, auto-executed functions runs immediately
// a basic module
(function() {
    function secretFunction(msg) {
        return msg + "!";
    }
   
    var secretValue = secretFunction('my secret variable');
   
    // window needs to be explicit
    window.publicValue = 'my public variable';
   
})();    

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

Рабочая группа CommonJS создала спецификацию модулей CommonJS - API для инкапсуляции приватного кода и описания публичного API (используя объект exports).

В приведенном примере весь код является приватным за исключением явно указанного значения value и функции action:

При загрузке такого модуля (с помощью метода require()), его код исполняется один раз, и публичные поля из модуля (в модуле thing.js это поля value и action) сохраняются в объекте thing и доступны в коде запрашивающего модуля:

var thing = require('./thing');

То есть метод require() возвращает вам объект exports запрашиваемого модуля.

Такой подход используется в Node.js, где CommonJS является встроенной системой модулей.

Но он не работает в браузерах. Для того, чтобы такой код заработал в браузере, его необходимо специально подготовить. Процесс такого преобразования кода получил название “бандлинг” (bundling). В рассматриваемом примере результатом бандлинга модулей menu.js и app.js является файл bundle.js

Еще раз: процесс бандлинга начинается с входной точки приложения (в примере это app.js), проходим по всем запрашиваемым модулям и осуществляем их трансформацию. В результате получаем bundle.js

Для бандлинга существует множество инструментов:

(технически бандлинг не такое уж и сложное дело. Если вам интересно как устроена работа такого инструмента, то вы можете сами написать сбой бандлер и свою реализацию функции require())

Ну а как из множества Haxe-классов получается JavaScript?

В Haxe мы указываем компилятору точку входа (в примере это класс App), компилятор проходит по всем импортированным зависимостям, преобразует весь код и объединяется его в бандл (output.js):

И это практически тот же самый процесс бандлинга, что мы видели в JavaScript.

Пониманию того, как Haxe-код преобразуется в JavaScript, лучше всего способствует изучение кода, полученного на выходе из компилятора:

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

Все ваши классы помещаются в анонимную автоматически исполняемую функцию:

Весь код в итоге становится приватным (за исключением кода, для которого явно не указано, что он доступен извне).

Структура полученного бандла следующая:

  • утилитарный код, например, указывающий на то, как происходит наследование
  • объявления классов
  • инициализация статических полей
  • и в конце вызывается точка входа приложения (если таковая имеется)

И интересно то, что полученный бандл универсален - он может работать как в браузере, так и в Node.js (если, конечно, вы не используете какой-либо специфичный код).

Итак, имеем следующее:

  • писать в глобальную область видимость - плохо, лучше использовать модули
  • для того, чтобы модульный код работал в браузере, его нужно специально преобразовать с помощью бандлера
  • работа компилятора Haxe практически повторяет процесс бандлинга в JavaScript

И как же нам теперь настроить взаимодействие кода, написанного на JavaScript и на Haxe?

Давайте вернемся к самому первому примеру с модулем Menu и попробуем переписать его на Haxe.

У нас есть статическая функция render():

В Haxe этот код станет классом Menu со статическим публичным методом render():

Так, уже хорошо! Но для того, чтобы код из нашего класса Menu можно было вызывать из JavaScript, нужно использовать мету @:expose (иначе класс будет недоступен извне полученного на выходе из компилятора бандла).

На следующем слайде показан немного измененный (для краткости) код, который получается после компиляции класса Menu:

Здесь мы видим анонимную автоматически исполняемую функцию, но с дополнительным кодом для объявления публичного API:

  • если объект exports доступен (на Node.js), то публичный код будет доступен из него
  • если exports недоступен, то проверяется доступность глобального объекта window (доступного при работе в браузере), и если он есть, то публичное API будет доступно из него
  • затем идет проверка на доступность объекта self (он будет доступен, если код работает в веб-воркере)
  • и если никакой из перечисленных объектов недоступен, то используется объект this

После этого мы можем “прикрепить” публичное API к выбранному объекту:

var Menu = $hx_exports["Menu"] = …

Следующий вопрос: как использовать JavaScript-код из Haxe?

Здесь нужно рассмотреть два случая:

  • использование обычного JavaScript-кода
  • использование модулей CommonJS

Код из обоих случаев можно использовать!

Для классического JS-кода можно просто вызывать untyped код:

Можно также написать свою обертку с красивым API.

А еще можно написать экстерны (но сегодня мы не будем их касаться).

Использование npm-модулей - более интересный случай.

Для запроса npm-модуля можно использовать inline-синтаксис:

var jsLib = js.Lib.require('jsLib'); // на выходе преобразуется в `var jsLib = require('jsLib');`
jsLib.doSomething();    

Если же вы хотите использовать классы, то, скорее всего, вам понадобятся экстерны - метаданные, используемые для преобразования вашего Haxe-кода в соответствующий код платформы (в нашем случае - в CommonJS код). Если скомпилированный код будет использоваться в браузере, то его будет необходимо дополнительно преобразовать с помощью бандлера, т.к. компилятор Haxe подготовит только тот код, который он скомпилировал, с исходным JavaScript-кодом он ничего не сделает.

Стоить также отметить, что стандарт CommonJS уже устаревает, и сегодня все больше кода использует более новый синтаксис стандарта ES6, который может показаться запутанным даже для опытного JavaScript-программиста:

Филипп предложил просто запомнить правила преобразования ES6 синтаксиса в ES5:

1. import * as React from 'react';

преобразуется в экспорт объекта:

var React = require('react');

2. Использование фигурных скобок означает использование деконструкции из ES6 для получения значений из объекта:

import { connect } from 'react-redux';

преобразуется в

var connect = require('react-redux').connect;

3. и последний (непонятный даже Филиппу) случай, когда не используются ни *, ни фигурные скобки, означает, что из запрашиваемого модуля извлекается свойство default:

import Slider from 'rc-slider';

можно преобразовать в

var slider = require('rc-slider').default;

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

Хотя это и конец презентации, но это только начало для тех, кто собирается заниматься разработкой на Haxe под JavaScript, т.к. эта тема намного шире.

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

Также стоит обратить внимание на проекты Haxe Modular и hxgenjs, которые позволяют получать на выходе компилятора несколько js-файлов (а не один большой), таким образом обеспечивая возможность скачивать приложение по частям.