В качестве предисловия
Веб-дизайнерский народ в последнее время распробовал single page web applications. Что оправдано во многих случаях.
Но яыно ошибочно считать что single page web application не сделать без чего-то типа AngularJS, Ember и прочих Knockouts.
Во всяком случае если вам нужно сделать нечто простое типа To-do списка совершенно не обязательно тащить на клиент килобайты мега-фреймворка. На самом деле килобайты трафика это пол беды. Основная цена, скажем AngularJS, в том что он, как любой универсальный data binding механизм, создает значительную run-time нагрузку.
Эта статья про то как в 60 строках кода + jQuery/Zepto сделать простой app framework котрый можно расширять под свои нужды и без лишних сущностей в нагрузку.
Постановка задачи
Наш framework должен ...:
- … поддерживать routing, т.е. должна быть возможность сказать в деклартивной модели: «этот вот url hash должен быть показан в этом view».
- Должна быть возможность динамической загрузки разных view. Какие-то части нашего прилжения могут быть либо тяжелы для начальной загрузки либо не нужны например для незалогиненного пользователя.
- Должна быть возможность динамической загрузки скриптов. По причинам изложенным в п. 2
- Наше приложение будет поддерживать browsing history — кнопка «назад» в браузере должна показывать предыдущую страницу и т.д.
- Ну и все это должно быть компактным и расширяемым как того будет требовать логика нашего приложения.
Пример того что мы хотим получить
Приложение Bootstrap'нутый список контактов — содержит сам список, карточку — детали контакта и некую панель управления (dashboard). Что будет на той панели нам не важно — знаем что что-то будет и ладно.
Поехали...
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. Эту функциональность я оставляю на воображение читателей.
Автор: csmile