Pull to refresh

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

Reading time 5 min
Views 76K

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



Веб-дизайнерский народ в последнее время распробовал 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. Эту функциональность я оставляю на воображение читателей.
Tags:
Hubs:
+39
Comments 55
Comments Comments 55

Articles