Реализация модульной системы в JavaScript
Янв 09
2011
ИгроДел framework, JavaScript, jQuery, модульная система, ООП 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.