Реализация модульной системы в JavaScript

No Comments

Продолжая написание игры столкнулся с тем, что JavaScript — довольно своеобразный язык по сравнению с C/C++, на которых я в основном пишу. Например, в нём нет такого понятия как классы, нет возможности подключать модули.

Поэтому я и говорил ранее, что он хоть и более «человечен», чем тот же ассемблер (хорошее сравнение:) ), часто удобнее всяких C, но сильно отличается от других ООП языков. Вообще, если копать глубже, то это один из самых запутанных языков, которые я видел :) Но это сказывается привычка к C++.

Реализация классов с наследованием.

Начнём с классов. Тут всё просто, на эту тему написано достаточно, поиск помогает найти кучу вариантов. Я взял за основу реализацию Simple JavaScript Inheritance написанную John Resig, разработчиком jQuery. Возможности этой реализации:

  • Создание конструктора просто (нужно всего лишь определить метод init()).
  • Для создания нового класса нужно (extend) существующий.
  • Все классы наследуются от единственного предка: Class.
  • И одна из основных возможностей, о котороя часто забывают: есть доступ к перекрытым (overridden) родительским медодам в правильном контексте. Реализовано через this._super().
  • Ну и instanceof работает как надо.

Код подробно прокомментирован и разобран в статье Simple JavaScript Inheritance, и без изменений перекочевал в файл core/class.js.

Кстати, нашёл небольшой видеокурс по jQuery на русском.

Модульная система.

Подключаемые модули значительно повышают удобство разработки. В финальном продукте конечно лучше пройтись javascript «уменьшалкой-сжималкой», чтоб получился один файл, который быстро загрузится браузером. Но при написании игры с модулями намного удобнее работать. Или может я чего-то не знаю о JavaScript :)

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

Хочется, чтоб:

  • учитывались зависимости модулей друг от друга;
  • в html достаточно было вставить один <script> и всё что надо (зависимые модули) загрузилось автоматически;
  • по имени модуля находился нужный файл;
  • один файл загружался один раз;
  • конструкция была простой.

Что-то похожее нашёл в статье Инклюд в яваскрипте, но там eval+ajax, можно ведь проще. Ну и ещё несколько вариантов видел, но всем критериям одновременно они не соответствовали. Уже после написания встретил Модульность в JavaScript, тоже не понравился, хотя близко.

В итоге из разных частей получилось такое решение:

zx = {};
zx.units = [];          // модули
zx._waitForOnload = 0;  // число скриптов, загружающихся, но ещё не загрузившихся
zx._jsQueue = [];       // очередь модулей

// Запускаем загрузку js: просто добавляем тэг <script>
zx._addJS = function(name) {
    zx._waitForOnload++;
    zx.units[name] = {name:name,requires:[],loaded:false,body:null};
    var path       = name.replace(/\./g, '/') + '.js';   // имя модуля превращается в путь
    var script     = document.createElement('script');
    script.type    = 'text/javascript';
    script.onload  = function(){ zx._waitForOnload--; zx._runUnits(); };  // при загрузке обновляем модули
    script.onerror = function(){ throw('Error while loading unit ' + name + ' from ' + path); };    // при ошибке - сообщаем
    script.src     = path;
    document.getElementsByTagName('head')[0].appendChild(script);
};

// обновляем модули. Ищем зависимости, выявляем полностью готовые модули (у которых готовы зависимости)
zx._runUnits = function() {
    var i,
    bLoaded = false,
    n = zx._jsQueue.length,
    q = [];  // новый массив, в котором хранится очередь
    for (i = 0; i < n; i++) {
        var unit = zx._jsQueue[i];
        var dependenciesLoaded = true;
        if (unit.requires) {
            for (var j = 0; j < unit.requires.length; j++) {
                var name = unit.requires[j];
                if (!zx.units[name]) {
                    dependenciesLoaded = false;
                    zx._addJS(name);
                } else if (!zx.units[name].loaded) {
                    dependenciesLoaded = false;
                }
            }
        }
        if (dependenciesLoaded && unit.body) {
            unit.loaded = true;   // отмечаем, что загружен
            unit.body();          // выполняем
            bLoaded = true;
        } else {
            q.push(unit);  // если модуль всё ещё зависим или не загружен, снова в очередь
        }
    }
    if (bLoaded) {
        zx._jsQueue = q; // очередь меняется только если что-то загрузилось
        zx._runUnits();  // на всякий случай надо запустить модули снова. TODO: проверить, нужно ли это.
    } else if (zx._waitForOnload == 0 && zx._jsQueue.length != 0) {
        var u = [];      // очередь не пуста, но всё загружено, значит что-то не нашлось. Сообщаем.
        for (i = 0; i < zx._jsQueue.length; i++) {
            u.push(zx._jsQueue[i].name);
        }
        throw('Unresolved: ' + u.join(', '));
    }
};

// Описание модуля
zx.unit = function(name, data) {
    if (!name || !data || !data.body)
        throw("Wrong unit declaration: name=" + name + ", data=", data);
    data.name = name;
    data.loaded = false;
    zx.units[name] = data;
    zx._jsQueue.push(data);
    zx._runUnits();
};

Теперь каждый модуль можно объявить примерно так:

zx.unit('minddef.game', {  // имя модуля. Этот будет в файле minddef/game.js
    requires:['core.gfx','game.some'],  // список модулей, от которых зависит
    body: function() {     // тело модуля
        // тело модуля
    }
};

И всё загрузится автоматически. Достаточно в html подключить описание объекта zx, а дальше описать какой-то общий модуль-загрузчик, который загрузит всё остальное и запустит. То есть в requires у него будет например ‘minddef.game’, а в body: MindDefGame.start(). При этом MindDefGame.start() реализован внутри minddef/game.js.

Leave a Reply

You must be logged in to post a comment.