Пишем single-page web application framework в 60 строках кода

    В качестве предисловия



    Веб-дизайнерский народ в последнее время распробовал single page web applications. Что оправдано во многих случаях.
    Но явно ошибочно считать что single page web application не сделать без чего-то типа AngularJS, Ember и прочих Knockouts.
    Во всяком случае если вам нужно сделать нечто простое типа To-do списка совершенно не обязательно тащить на клиент килобайты мега-фреймворка. На самом деле килобайты трафика это пол беды. Основная цена, скажем AngularJS, в том что он, как любой универсальный data binding механизм, создает значительную run-time нагрузку.

    Эта статья про то как в 60 строках кода + jQuery/Zepto сделать простой app framework котрый можно расширять под свои нужды и без лишних сущностей в нагрузку.

    Постановка задачи



    Наш framework должен ...:

    1. … поддерживать routing, т.е. должна быть возможность сказать в деклартивной модели: «этот вот url hash должен быть показан в этом view».
    2. Должна быть возможность динамической загрузки разных view. Какие-то части нашего приложения могут быть либо тяжелы для начальной загрузки, либо не нужны, например, для незалогиненного пользователя.
    3. Должна быть возможность динамической загрузки скриптов. По причинам изложенным в п. 2
    4. Наше приложение будет поддерживать browsing history — кнопка «назад» в браузере должна показывать предыдущую страницу и т.д.
    5. Ну и все это должно быть компактным и расширяемым как того будет требовать логика нашего приложения.


    Пример того что мы хотим получить



    Приложение Bootstrap'нутый список контактов — содержит сам список, карточку — детали контакта и некую панель управления (dashboard). Что будет на той панели нам не важно — знаем что что-то будет и ладно.

    Personas demo



    Поехали...



    Single page web application состоит из одного, как правило статического, html файла. Но по дизайну мы имеем явно выраженные суб-страницы или views. Договариваемся сами с собой что эти суб-страницы будут представлены <section> элементами в нашем markup:

    <body>
        ...
        <section id=dashboard class="container" src="pages/dashboard.htm"></section>
        <section id=about class="container" src="pages/about.htm"></section>
        <section id=contact class="container" src="pages/contact.htm"></section>
        <section id=contacts class="container" src="pages/contacts.htm"></section>
        ...
    </body>
    


    Здесь все понятно кроме нестандартного src атрибута (для section элемента).
    Договоримся что src атрибут будет указывать на html фрагмент требуемый для представления данного view. Такие фрагменты будем загружать по требовнию т.е. только тогда когда пользователь попросит показать этот view.

    Далее согласовываем с нашим веб-дизайнером что section элемент у которого установлен класс active будет текущим и соответственно видимым. Для этого нам нужно всего одно CSS правило:

    /* section visibility */
    body > section:not(.active) { display:none; }
    


    routing


    Договариваемся с командой что навигация внутри нашего приложения делается через гиперлинки вида
    • href="#имя-секции" и.или
    • href="#имя-секции:идентификатор-объекта"

    Т.е. активация такого гиперлинка должна показывать section элемент с id=«имя-секции».
    Использование гиперлинков для показа частей дает нам «из коробки» поддержку истории просмотра в браузере (кнопки «вперед» и «назад»).

    В данной имплементации я использую готовый hashchange() jQuery плагин но если целевые браузеры только те что поддерживаются jQuery2 то достаточно обычного event handler на соотвесвующем событии.

    Структура загружаемой «страницы»-фрагмента


    Загружаемый фрагмент в нашем случае будет состоять из разметки (собственно HTML) и script секции — обработчика нашей страницы. Вот пример
    pages/contact.htm — карточка для показа / редактирования одного контакта.

     <form class="form-horizontal" role="form" name="contactDetails">
     ...
     </form>
    
    <script>
      app.handler(function() {
    //|
    //| view initialization:
    //|
        var $page = $(this);
        var $firstName  = $("[name=firstName]");
        var $lastName   = $("[name=lastName]");
        ...
    //|
    //| view presentation:
    //|
        return function(param) {
          var contact = data.contacts[param];
          $firstName.val(contact.firstName);
          $lastName.val(contact.lastName);
          ...
        };
      }); 
    </script>
    
    


    Вызов app.handler(function() {...}) в коде выше инициализирует наш view и регистрирует функцию-загрузчик данных во view.

    Собственно вот и все. Осталось привести код нашего app framework — те самые 60 строк кода которые это все и связывают вместе.

    В принципе тут все должно быть понятно без каких-то особых комментариев. Но если что — свистим, не стесняемся.

    // Simple single page application framework
    // Author: andrew @ terrainformatica.com
    
    (function($,window){
    
      var pageHandlers = {};
      var currentPage;
      
      // show the "page" with optional parameter
      function show(pageName,param) { 
        // invoke page handler
        var ph = pageHandlers[pageName]; 
        if( ph ) { 
          var $page = $("section#" + pageName);
          ph.call( $page.length ? $page[0] : null,param ); // call "page" handler
        }
        // activate the page  
        $(".nav li.active").removeClass("active");
        $(".nav li a[href=#"+pageName+"]").closest("li").addClass("active");
        
        $(document.body).attr("page",pageName)
                        .find("section").removeClass("active")
                        .filter("section#" + pageName).addClass("active");
      }  
    
      // "page" loader
      function app(pageName,param) {
      
        var $page = $(document.body).find("section#" + pageName);  
        
        var src = $page.attr("src");
        if( src && $page.find(">:first-child").length == 0) { 
          $.get(src, "html") // it has src and is empty - load it
              .done(function(html){ currentPage = pageName; $page.html(html); show(pageName,param); })
              .fail(function(){ $page.html("failed to get:" + src); });
        } else
          show(pageName,param);
      }
    
      // register page handler  
      app.handler = function(handler) {
        var $page = $(document.body).find("section#" + currentPage);  
        pageHandlers[currentPage] = handler.call($page[0]);
      }
      
      function onhashchange() 
      {
        var hash = location.hash || "#dashboard";
        
        var re = /#([-0-9A-Za-z]+)(\:(.+))?/;
        var match = re.exec(hash);
        hash = match[1];
        var param = match[3];
        app(hash,param); // navigate to the page
      }
      
      $(window).hashchange( onhashchange ); // attach hashchange handler
      
      window.app = app; // setup the app as global object
      
      $(function(){ $(window).hashchange() }); // initial state setup
    
    })(jQuery,this);
    
    


    Всё вышеизложенное есть выжимка из реального framework используемого в нескольких mobile web applications.
    В mobile случае app расширен методами app.getData() и app.postData() — обёртки над $.ajax() поддерживающие caching в localStorage и анимацию переключения views. Эту функциональность я оставляю на воображение читателей.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 55
    • +10
      Переводчик от Бога
      • +1
        Это про что?
        • +49
          Тонкий hint на большой amount непереведенных words, которые вы посчитали нужным не переводить по тем или иным reasons.
          • +9
            Я живу и работаю в Канаде. Я просто не знаю как эти слова произносятся в современном русском. Например view в контексте MVC я просто не знаю как озвучить по русски. Буду признателен если кто-то предложит непротивречивый термин.
            • 0
              Официально — вид (модель-вид-контроллер). Неформально, среди разработчиков — вьюха.
              • +7
                Неформально, среди разработчиков — вьюха.
                Главное в переводах без вьюх.
                Читателей такие словечки ставят в тупик на ура. И никакой словарь не поможет. Уж лучше view.
                • +12
                  а я думал что «модель-контроллер-представление» входу сейчас…
                • +4
                  Чаще «представление», чем «вид» (если нет желания запутать читателя разными видами).
                  • 0
                    Не нужно придумывать. Официально — представление. А вид — это из окна.
                    • 0
                      Да, затупил немного. Соглашусь, что представление — более адекватный перевод.
                    • 0
                      представление, же, а вовсе не вид
                    • +1
                      Мультитран, на мой взгляд, один из лучших переводчиков английских терминов на русский.
                      Например view в контексте MVC
                      model-view-controller
                    • +1
                      Критикуя — предлагай.

                      url hash — ???
                      single page web application — ???
                      • +9
                        Я не берусь судить и мне, собственно, все понятно. Просто многим это не нравится, и они могут посчитать как неуважение. Это один из вечных срачей на хабре, и получить минусы за хорошую статью из-за такой ерунды — это тоже не хорошо.

                        url hash — веб-ссылка на гашиш
                        single page web application — одностраничное веб-приложение
                        • +11
                          Поаккуратнее с гашишем: нас того… и закрыть могут.

                          По вопросу перевода: уж лучше оставлять оригинальные написания на латинице, чем уродливо транслитерировать.
                          • 0
                            Если по поводу хэша еще можно усомниться, то термин «одностраничное веб-приложение» довольно звучен и не коряв (имхо). Хотя опять же, «хэш-ссылка» лично для меня звучит тоже нормально. Я понимаю, когда, например, пишут «линейка прокрутки», то не сразу можно понять, что имеется в виду. А тут, мне кажется, довольно понятно.
                            • 0
                              Если по поводу хэша еще можно усомниться, то термин «одностраничное веб-приложение» довольно звучен и не коряв (имхо). Хотя опять же, «хэш-ссылка» лично для меня звучит тоже нормально.
                              Если есть нормальный перевод или устоявшаяся транслитерация — конечно надо использовать их.

                              В остальных же случаях лучше оригинал, нежели «робастный скэлэйбл интерпрайз солюшн».
                              • 0
                                Хэш-ссылка — как человек, испорченный математикой, думал, что это хэш-функция, применённая к длинному урлу. Ну, типа, hel.lo/w0rLD123. И, только дочитав до href="#имя-секции", понял.

                                Не француз, конечно, с их калькой le mot diese в пику заимствованию hashtag, но хотелось бы что-то более благозвучное и, одновременно, понятное по-русски найти этой штуковине «hash url»
                        • –1
                          хэш урла
                          одностраничные web приложения (ваш К.О.)
                    • 0
                      Ну сами подумайте:
                      Но яыно ошибочно считать
                      распробовал single page web applications
                      этот вот url hash должен быть показан в этом view
                      Т.е. активация такого гиперлинка должна


                      Как минимум несогласованность текста, грамматика (скорее опечатки причем), куча терминов, которым можно найти достойный перевод (я понимаю что вы живете в Канаде, но все же это создает вполне нормальное впечатление о корявом переводе). Как-минимум перечитали бы разок перед публикацией.
                      • –7
                        Критикуя — предлагай.

                        url hash — ???
                        single page web application — ???
                  • 0
                    А как быть с индексированием? Я так понимаю в вашем случае шаблоны всеравно являются статичными, и просто подтягиваются с сервера. Мол индексироваться ваша страница не будет (либо нужно на сервере реализовывать механизм снапшотов). Не проще было бы каким grunt-ом скомпилировать все шаблоны в один html сразу. И по части js это реализуется довольно просто.
                    • 0
                      Если это приложение с логином (как правило) то зачем там индексирование?

                      Для поисковиков нужно делать правильный about текст который статически включается в index.htm

                      Или я не понял вопроса.
                      • 0
                        Например, так
                        help.yandex.ru/webmaster/robot-workings/ajax-indexing.xml

                        Применено, например, здесь. Весь сайт такой, «динамический»…
                        www.rozenbaum.ru/#!/songs/i/0472
                        • 0
                          я знаю про ajax-crawling, но в конкретно этой реализации это не предусмотрено. Если речь идет о такой небольшой странице, проще распихать все в одну страницу и просто показывать только частями. Если уже хоть сколько нибудь сложнее все, то тогда нужно дублировать рендринг снашотов на сервере и отдавать их ботам.
                      • 0
                        csmile, недавно на HN встретил пост, где автор тоже создал микрофреймворк для одностраничных приложений: moot.it/riotjs/ Может будет полезно подчерпнуть его идеи.
                        • 0
                          Ага, спасибо.

                          Замечательна MVCёвинка, и махонька така :)

                          А MVC… моделью можно считать данные на сервере, в базе данных например. Контроллер — частично в business logic layer (сервер), частично на клиенте. Т.е. то что работает на клиенте есть чистый view и его логика. Почему народ пытается MVCями обозвать скажем angularjs я не понимаю.

                          А вообще мне как прагматику лично более симпатичен Twitter Flight хотя это и не framework в общем смысле. И кстати Twitter Flight реально работает на twitter сайте. Скажем AngularJS в google продуктах не замечен.
                          • 0
                            AngularJS используется, как минимум, в одном сервисе Google — DoubleClick.
                            • 0
                              И это скорее контр-пример ибо UI там вообще никакой да и продукт какой-то уж совсем левый, половина ссылок здесь не работает толком.
                            • 0
                              Очень соблазнительно считать данные в базе моделью, но это заблуждение. Модель должна включать в себя логику обработки данных в единой точке, не размазывая и не дублирая ее по контроллерам. Хотя, конечно, данные в базе можно считать моделью, если доступ к ним идет через апи, «зашитое» в процедуры без прямого доступа к данным.

                              Как angularjs и прочие оказываются MVC? Тут наверно стоит упомянуть концепцию HMVC, которая, имхо, значительно лучше подходит для описания слоев логики приложения, чем попытка вместить их все в прокрустово ложе MVC.

                          • 0
                            Упс. Не туда комментарий.

                            • +5
                              Я так понял главная причина изобретение собственного велосипеда заключается в том что не хочется подгружать чего-то типа AngularJS, Ember и прочих Knockouts.
                              AngularJS 80k и для простых задач jquery вообще не нужен.
                              JQuery 1.7 = 92k и плюс еще Zepto.
                              На счет "создает значительную run-time нагрузку", не уверен что ваше решение создаст ее меньше. Такая нагрузка бывает когда не правильно инструмент используешь, а не сама по себе возникает при использовании чего-то типа AngularJS, Ember и прочих Knockouts.
                              • +1
                                jQuery 1.10 весит где-то 30-40Кб. Для правки. zepto весит меньше то толку от него не много. Насколько я помню работает он медленнее jQuery и там где он используется можно обойтись и vanilaJS. А есть еще jqMobi
                                бесполезный бенчмарк
                                • +1
                                  Про dirty checking в angularjs и проблемы со списками можно прочитать здесь: habrahabr.ru/post/200670/
                                  Data binding в angularjs требует создания отдельной функции-observer для *каждого* объекта в модели.
                                  Чем более «развесиста» и глубже модель — тем более медленно это всё работает ибо базовые алгоритмы имеют вычислительную сложность O(n*m). Где n — количество элементов данных в модели (объектов и их свойств) и m — количество обозревателей.

                                  При всем при том для базовых операций типа «отобразить список» в 99% случаях совершенно не нужен живой data binding.
                                  Т.е. в этих самых 99% мы платим за то что не используем вообще. В мобильных приложениях плата это расход батареи и пр.

                                  Мое решение оптимально в том числе в смысле расходов — ты платишь только за то что реально используешь.
                                  • 0
                                    Object.observer все еще является эксперементальной фичей и по умолчанию отключен в хромах. Как только это изменится, angular сразу перейдет на использование оной, что даст прирост производительности при работе со списками в 20-40 раз. Во всяком случае они упоминали это на одной из последних конференций. Пока же достаточно не писать конструкции вида $scope.watch('myFatModel', callback, true), и особых проблем с производительностью вы не заметите. Да, это сложнее, но не намного. Для мобильных приложений хватает и без angular, любые анимации или банальное выполнение чего-то в фоне во время анимаций тормозит сильнее, нежели периодически запускающиеся $digest циклы.

                                    В любом случае по другому нормально заимплементить датабиндинг не особо то и выйдет. Через геттеры/сеттеры тоже есть свои ограничения.
                                    • 0
                                      Даже при наличчи Object.observer в той форме что он предложен не поможет сделать AngularJS легче.
                                      Для того чтобы заменить $watch функциональность нужно пройтись по всем узлам модели (objects & arrays) и навесить на них по отдельной callback функции с помощью Object.observer.

                                      Для отображения списка с помощью ng-repeat нужно на каждую data item повесить свой scope что превращает задачу отображения списка в праздник для сборщика мусора.

                                      Data binding в принципе может быть полезной вещью в каких-то случаях, просто как он тотально навязан в Angular (либо data binding — либо никак) мне откровенно не нравится.

                                      Кстати я сделал свой неинтрузивный вариант data binding в Sciter в стиле AngularJS. Так как в Sciter есть namespaces, Object.add/removeObserver и декораторы функций то код действительно получается чистым.
                                      • +1
                                        На данные навешиваются обсерверы изменений только один раз. Если вам не нужно отслеживать изменения во всей модели (а обычно это не нужно) то все достаточно быстро работает. Во всяком случае у меня еще не было таких проблем с производительностью, что пришлось бы отказаться от ngRepeat, а если и будут, мне проще для конкретного случая написать свою директиву.
                                    • +1
                                      Как-тоу Вас все сформулировано, что сходу и не поймешь — Вы хорошо знаете как работает $watch, ngRepeat? Сам ngRepeat создаст только один watchCollection для самого списка/словаря. А далее — сколько в темплейте у Вас будет {{}} столько и будет весьма примитивных watch-ей. Не нужен живой датабиндинг — используйте неживой :-) (ссылку на статью сами и привели)
                                      • 0
                                        Имеем модель вида
                                        var list = [  
                                           { id:1 },
                                           { id:2 }
                                        ];
                                        

                                        отображаемую на некий repeatable в UI.
                                        Для того чтобы binding работал нужен примерно следующий код
                                        Object.observe( list, function() { ... } ); // list add/remove items observer
                                        Object.observe( list[0], function() { ... } ); // list[0] object property change observer
                                        Object.observe( list[1], function() { ... } ); // list[1] object observer
                                        

                                        итого три closures. Это на простой массив объектов-то…

                                        И вот сказ про то как человек бился с ng-repeat.

                                        Воистину, выбираем героический фреймворк чтобы потом героически его бороть.

                                        • +1
                                          Вы абсолютно уверены, что это будут реализовывать именно так? Отслеживать тотально все? А зачем?

                                          Воистину, выбираем героический фреймворк чтобы потом героически его бороть.

                                          Опять же зачем? Выбирайте инструмент под задачу. Если Вам постоянно надо его «бороть», то зачем Вы или кто-то другой его выбрали?
                                          • 0
                                            Вы абсолютно уверены, что это будут реализовывать именно так? Отслеживать тотально все?


                                            Да, уверен.

                                            Скажем вот список

                                            var list = [  { val:1 }, { val:2 }, { val:3 } ];
                                            


                                            Нужно представить live-bound cумму всех полей val в этом списке. Кроме как повесить по observer 'у на каждый элемент ничего не получится. В DOM дереве можно сгенерирвать bubbling событие ибо есть parent/child связь. И поймать это событие на root контейнере. В JS же объектах такого в принципе быть не может — только brute force — каждому объекту по observer.
                                            • +1
                                              Где не получится? Вы про AngularJS говорите или про что-то другое? Вот сейчас там, например, будет $watch на getTotal(). Почему Вы решили, что подход будет резко изменен?
                                          • +1
                                            Извините, сразу не было времени посмотреть статью. Правильно ли я понял, что в этой статье человеку все нравится, но он столкнулся с одной проблемой, которую успешно решил. Это называется «героически бороть»? Хм, у нас, видимо, разное представление о масштабах проектов и масштабах «борьбы».

                                            Кстати, ему первое решение не подошло, скорее всего потому, что watchCollection появилось только в 1.1.4.
                                    • +2
                                      Но явно ошибочно считать что single page web application не сделать без чего-то типа AngularJS, Ember и прочих Knockouts.

                                      А есть ведь вообще микроскопический (в килобайтах) Backbone, который сам по себе никакой нагрузки не создаёт (вся отрисовка на усмотрение разработчика и т.д.), зато приложение будет хорошо структурированным, а не набором спагетти-коллбэков.
                                      Так что можно-то оно конечно можно. Только зачем?
                                      • 0
                                        Про какие крнкретно «спагетти-коллбэки» идет речь?
                                        • 0
                                          Про те, которые появятся в процессе расширения функциональности.
                                          • 0
                                            Да не нужны там никакие callbacks.

                                            Достаточно вставить в код приведенный в моей статье что-то типа этого:

                                              currentPage.trigger("page:close", currentPageName);
                                              newPage.trigger("page:open", newPageName);
                                            


                                            и пусть кому надо подписываются на эти события. Делов то. Это называется loosely coupled (слабо связанные?) системы.
                                            Еще раз: смотрим на Twitter Flight, он прагматично правильный.
                                            • 0
                                              То есть надо добавить никак не описанную в статье систему подписок, так?
                                              А потом, например, окажется, что нужно передавать параметр в url. И? Переписывать половину приложения? Чем это прагматичнее варианта «использовать готовое решение на 6.5 килобайт», я никак не пойму? Кстати, тут вполне можно обойтись одним Backbone без jQuery (а подписки вы ведь именно из него хотели заюзать?) и сэкономить на разнице 75(!) килобайт.
                                              • 0
                                                Пардон, там нужен Underscore, так что разница будет 70kB
                                                • 0
                                                  Система подписок описана в jQuery и в каждой статье про pub/sub pattern

                                                  Система подписок также является основой Twitter Flight.
                                        • 0
                                          Важным вопросом для AJAX сайтов является поисковая доступность, чтобы не городить костыли и Graceful Degradation.
                                          • 0
                                            Вах! Спасибо! Обожаю короткие и элегантные решения. Пошел делать member-эрию на SPA.
                                            • 0
                                              Хорошо бы исходники демки — загрузить и поиграться, чем из статьи вырезать и компоновать…

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