Основные проблемы разработки ajax-приложений -------------------------------------------- Ajax-приложение отличается от обычных веб-приложний. Пользователь видит это отличие - он замечает, что страница его приложения обновляется не целиком, а частями. Также, пользователь видит большую отзывчивость таких приложений, и ему это нравится.
С точки зрения разработчика отличий гораздо больше. Взаимодействие с сервером происходит не так, как раньше. И, если разработчик хочет не просто "добавить в страницы немного динамики", а построить вокруг принципов ajax все свое приложение, то довольно быстро он понимает, что происходит не так, как раньше, не только взаимодействие с сервером, а буквально все.
(А на самом деле - я уверен, что нет никаких серьезных причин, чтобы ajax программирование было сложнее обычного. Я уверен, что все должно быть наоборот. Оно проще. Почему?) - Если раньше состояние сессии было на сервере, то теперь оно переползает в браузер. - Серверная часть, соответственно, сильно упрощается. Это хорошая новость. Но - приходится отдельно проектировать клиент-серверный прикладной протокол. С этим, впрочем, сложностей нет, если разработчик вовремя узнал про RESTful интерфейс. - Серверные шаблоны использовать нельзя - приходится генерировать HTML динамически в браузере. Это тоже достаточно быстро решается - сейчас нет недостатка в клиентских шаблонных движках. - Традиционные средства разбиения приложения на компоненты, где приложение нарезано на HTML-страницы с модулями скриптов, перестали работать. Все приложение живет в одной странице.
Последнее и представляет собой наибольшую проблему. HTML-файл стремительно раздувается. То же самое происходит с джаваскриптом. Разработчик быстро начинает бить джаваскрипт на модули, и загружать их тегом script.
Начиная с некоторого момента - программист путается в зависимостях своих .js файлов, и задает на форумах вопрос: "how can I include .js in other js" (погуглите по include js)? И - одновременно его мучает вопрос, что же делать с HTML-шаблонами и раздувшимся HTML-файлом, в котором также черт ногу сломит.
Знакомо? Значит, читайте дальше.
Разработчик дальше находит, что решаются эти проблемы следующими способами: 1. Google Closure Compiler. Это компилятор из JS в JS, который умеет модули, и умеет минифицировать код. 2. В Dojo Toolkit, оказывается есть система модулей. Но давайте на минуточку предположим, что разработчик не хотел бы ради модулей менять свой веб-фреймворк. 3. Require.js - дает ровно то, что нужно. Нейтральная к фреймворку, позволяет делать клиентский include (точнее, require) для скриптов, и - умеет делать статическую сборку при помощи Closure Compiler. Его написал автор системы модулей Dojo Toolkit. И - он скоро переведет Dojo а эту штуку. Таки да - это оно. Наш выбор. (для пытливых умов. Вы обзязательно найдете еще и инициативу CommonJS. И удивитесь, что именно они там курят - ибо их основной API синхронный, и для браузера не годится. Правда, там не так давно появился и асинхронный вариант API. Думаю, вы не удивитесь, что require.js с ним совместим)
Итак, взяв require.js - мы можем делать клиентский include одного js в другой, независимо от фреймворка. Однако, поделав немного этот include, мы быстро выясняем, что одной этой возможности мало. require.js позволяет устранить бардак в логике, но не решает проблемы гуя. Проблемы остается как минимум две: - До сих пор непонятно, что делать с шаблонами, живущими плотной кучкой внутри HTML. Можно их разнести по файлам при помощи SSI (Server-Side-Include), и это, в принципе, поможет. Но не до конца. Ибо для динамического гуя одного HTML-шаблона мало. К нему надо добавить логику обработки событий и управления отрисовкой на JavaScript, которую в шаблоне изображать крайне неудобно. - Второе. API require.js выглядит реально страшно. Определения модуля и инклуды меньше всего похожи на что? Но определения модулей, и инклуды. - Ну, и третье. Не смотря на появившуюся техническую возможность разнести все по модулям-файлам, остается непонятным _принцип_ этого разнесения. То есть, главный вопрос - каким образом структурировать ajax-приложение, до сих пор не отвечен.
Include.js - что и зачем? ========================= (Если еще не сделали - идем на сайт requirejs.org, читаем, что это за хрень, потом - на https://github.com/gaperton/include.js, и читаем там README. После этого - продолжаем)
Первое, что мы делаем - оборачиваемся вокруг require.js, заставляя его API выглядеть по человечески. И у нас появляется нормальный include.
Второе, что мы делаем - это замечаем, что проблема с тем, что помимо HTML-шаблона нужна логика обработки событий и управления отрисовкой, решается паттерном Model-View-Controller. Как он выглядит в нашем случае?
1. Мы пишем HTML-шаблон, который умеет разворачиваться в текст. В паре с CSS - это самый настоящий View. 2. Мы берем этот текст, превращаем его в DOM, навешиваем обработку событий, и вставляем в документ, заменяя новым поддеревом уже существующее. Этот код - это у нас типичнейший контроллер. Обратите внимание - в данном случае контроллеру получается удобно разворачивать view-шаблон. Значит, что? Значит, контроллер будет у нас завернут в модуль. И этот модуль-контроллер должен уметь включать в себя шаблон, который будет тоже модулем - view. 3. И, при разворачивании шаблона, мы передаем ему некий контекст. Этот контекст, и место, откуда он берется - это Model. Этот код без проблем нарезается на модули, которые будут включаться include-ом в контроллер.
Таким образом, все приложение будет состоять из иерархии модулей-контроллеров, которые будут в себя включать: - модуль со своими HTML-шаблонами (View). - модули модели. - другие модули-контроллеры.
Идея понятна? Теперь, мы делаем второе, и самое важное. Придаем этой идее человечный вид, используя в качестве базиса нашу систему модулей. С этого момента предполагается, что для навеса событий и работы с DOM используется jQuery. Не мазохисты же мы, делать это врукопашную? А jQuery как раз в этом силен.
Начнем с простого. Чего именно в системе модулей для этого не хватает? Ну конечно же поддержки модулей-шаблонов. Модуль-шаблон - это отдельный файл с HTML и парой тегов для внедрения в него вставок на JS. При включении такого модуля, он должен скомпилироваться в функцию на JS, вызвав которую с параметром-контекстом, мы получим чистый, как слеза младенца, HTML.
Теперь, самое главное. Как оно, включение и вызов шаблона, выглядеть-то будет? Не слишком-ли страшно? Смотрите, вот так - устроит?
$.include({ html: "template.html" model : "model" }) .define( function( _ ){ ... var plain_html = _.html( _.model.query_data() ); ... });
Неплохо, не так ли? Обратите внимание - сейчас, для простоты, я считаю, что данные модели доступны синхронно. И это, на самом деле, именно мой случай - я их вынимаю из SQL-базы Google Gears. А там API синхронный.
Но это еще не все. Это мы развернули шаблон. Но - мы ничего не сделали с результатом, и, таким образом, наш модуль - никакой пока не контроллер. Наш модуль даже пока ничего не экспортирует. Теперь у нас стоит второй вопрос - а что он должен экспортировать, или - как именно выглядит снаружи контроллер? Что это за штука такая, как его вызывать?
Здесь возможны самые разные варианты, но я рекомендую вам следующий. Контроллер - это _функция_, следующего вида: function( $this : DOM-node, data_or_paramenets : any, callbacks : { function } ) : controller_obj
Первый аргумент - это узел DOM-дерева, куда контроллер должен вставится. По выходу из вызова - контроллер обязан произвести все необходимые манипуляции с DOM.
Второй аргумент - объект произвольного вида, в котором контроллеру передаются его параметры. Он, контроллер, знает, как их обрабатывать.
Третий - объект с набором коллбэков. Часть событий контроллер знает как обрабатывать, и обработает сам. А часть - не знает, и просигнализирует о них наверх.
И возвращаемый объект - это интерфейс экземпляра контроллера. Он может содержать методы для управления поведением контроллера, после того, как он создан. Эта штука не всегда требуется, но вообще - наличие такой возможности важно. Итак, что мы получили:
template.html:
{{ message }}
view.js:
$.include({ html: "template.html" model : "model" }) .define( function( _ ){ _.exports = function( $this, a_data, a_events ){ function update(){ // готовим данные из модели... var context = _.model.query_data( a_data );
// перегенерируем View, вставляя результат в DOM... _.html.renderTo( $this, context );
return { force_update : update; // и - даем вызывающему "хвост", чтобы заставить нас обновиться. }; } });
Вот и все. Ничего лишнего. Каждая строчка - фукнциональна.
Кроме одного момента. На самом деле, здесь показана не лучшая практика - я выставил наружу полное обновление как управление. Дело в том, что для обновления такого View "сверху" мне достаточно просто его еще раз вызвать, пересоздав. Но так как простого и содержательного примера управления привести нелегко (это скорее редкость, когда оно требуется), и так как в ответ на вызов "управления" придется что-то обновлять, я сделал в примере так. Но, если бы я задавал его без управления, то это выглядело бы проще, и красивее. Имейте это в виду.
Короче говоря, мы определили view. Для того, чтобы использовать данный view, нам достаточно включить один файл view.js. И вызвать его. Примерно так:
Это были базовые возможности библиотеки, и обзор проблем, ей решаемых. Продолжение следует.
ЗЫ: Обновлено описание проекта на github. Со слабой надеждой ищутся добровольцы, которые помогут мне в описании. Например, Маммут. :) PPS: Необходимо в срочном порядке инициировать merge данной либы либо в require.js, либо - в jQuery. Для этого и нужны нормальные описания на английском. Я сам, боюсь, не осилю. Времени это требует не меньше, а больше, чем программирование. К сожалению, у меня времени нет. Мне надо делать тот проект, ради которого эта либа создана.