Как стать автором
Обновить

Page-View паттерн в Javascript

Время на прочтение8 мин
Количество просмотров1.8K
AJAX
Представьте себе, как построена незамысловатая онлайн-фотогалерея. По-простому говоря, это две отдельные страницы: список всех фотографий и просмотр отдельно взятой фотографии. При переходе от одной странице к другой пользователю приходится ожидать полной перезагрузки страницы. Интерактивность теряется.

Другой подход: использование AJAX. Вся логика навигации по страницам перемещается в JavaScript. При первом обращении к галерее страница загружается полностью, при последующих действиях пользователя обновляется только нужная часть страницы.

У такого подхода есть недостатки:
  • Сложная логика JavaScript.
  • Не работает навигация браузера back/forward.
  • У отдельных фотографий нет своего адреса URL для прямого перехода.
Последние два недостатка сводятся к первому путем ещё большего усложнения JavaScript-кода. В статье я покажу, как разработать приложение простой фотогалереи с применением паттерна Page-View. Основное преимущество подхода — хорошо масштабируемый объектно-ориентированный JavaScript-код.

Визуально галерея будет выглядеть так:

Список фотографий


Машинко


Паттерн Page-View


Итак, в чем заключается основная идея. Введем две абстракции. Page – целая страница, которая требует полной перезагрузки в случае обновления. В нашем приложении она одна — вся фотогалерея. View – отдельное представление страницы. В случае галереи их два: представления списка и отдельной фотографии. Таким образом, одной странице соответствует несколько представлений, однако в любой момент времени активно лишь одно из них. Ниже представлена диаграмма классов иерархий Page и View:

UML-диаграмма


Попробуем разобраться, что здесь изображено.

Начнем с того, как это использовать. Ниже приведен фрагмент html-кода единственной страницы в нашем приложении:

<!-- ListView -->
<div id="list_container" style="display:none;">
</div>

<!-- ImageView -->
<div id="image_container" style="display:none;">
  <p><a id="back_link" href="javascript:;"><< Back to list</a></p>
  <p id="img_place"></p>
</div>

<script language="javascript">
  $(document).ready(function()
  {
    var p = new Gallery.Page();
    p.Init();
  });
</script>


* This source code was highlighted with Source Code Highlighter.

В приведенном коде задается разметка для двух представлений: ListView и ImageView. Затем инициализируется экземпляр класса Gallery.Page, представляющий страницу галереи. $(document).ready — конструкция библиотеки jQuery, позволяющая выполнять заданную функцию после загрузки всего содержимого документа, другими словами, когда вся document object model (DOM) документа будет готова к использованию. Библиотека jQuery будет неоднократно использована в дальнейшем. Ее вызовы легко узнаются по символу ‘$’.

Рассмотрим теперь описание класса PageBase.

// * abstract class PageBase *
//
// Represents an entire page.
function PageBase()
{
}

// Array of page's views.
PageBase.prototype._views = {};

PageBase.prototype._AddView = function(view)
{
  this._views[view.GetTypeName()] = view;
}


* This source code was highlighted with Source Code Highlighter.

Поле _views – ассоциативный массив, ключом которого является название представления, значением – экземпляр конкретного класса ViewBase. Этот массив заполняется при инициализации подклассов PageBase c использованием функции _AddView.

// Performs page initialization.
PageBase.prototype.Init = function()
{
  var t = this;
  $.history.init(function(hash) { t._PageLoad(hash); });
}

PageBase.prototype._currentViewName = {};

// Handles page load event.
PageBase.prototype._PageLoad = function(hash)
{
  var params = PageBase._ParseHash(hash);

  if (!this._views[params.view])
  {
    // first view is the default one
    for (var viewName in this._views)
    {
      PageBase.Goto(viewName, params);
      return;
    }
  }

  // checks if the passed view has changed
  if (this._currentViewName && params.view !== this._currentViewName)
  {
    this._views[this._currentViewName].Hide();
  }

  this._currentViewName = params.view;
  this._views[params.view].Show(params);
}


* This source code was highlighted with Source Code Highlighter.

В методе Init происходит инициализация плагина History для jQuery. Этот плагин позволяет переписывать url-страницы в зависимости от текущего View и его параметров, а также корректно работать с навигацией back/forward.

Закрытый (private) метод _PageLoad выбирает соответствующее параметрам url отображение и показывает его, при необходимости скрывая предыдущий View.

PageBase.Goto = function(viewName, params)
{
  params.view = viewName;
  var hash = PageBase._MakeHash(params);
  $.history.load(hash);
}


* This source code was highlighted with Source Code Highlighter.

Goto – статическая функция, осуществляющая переход на заданный View. Обращаю внимание, что параметры с которыми работают все View – это ассоциативный массив. В то время как в url параметры (hash) записаны в виде строки. Для сериализации и десериализации параметров используются методы PageBase._MakeHash и PageBase._ParseHash, соответственно.

Перейдем к конкретной реализации класса PageBase, то есть к Gallery.Page:

// * namespace Gallery *
var Gallery = Gallery || {};

// * class Page *
//
// Represents an entire gallery page.
Gallery.Page = function() // extends PageBase
{
  PageBase.call(this);
}
OO.Extends(Gallery.Page, PageBase);

// * public methods *

Gallery.Page.prototype.Init = function()
{
  this._AddView(new Gallery.ListView());
  this._AddView(new Gallery.ImageView());
  
  Gallery.Page.superclass.Init.call(this);
}


* This source code was highlighted with Source Code Highlighter.

Функция OO.Extends – это паттерн наследования в JavaScript. Реализация позаимствована из книги Pro JavaScript Design Patterns (авторы: инженеры Yahoo и Google). Это лучшая книга по ООП на JavaScript из тех, что я знаю. Очень рекомендую.

Пришло время разобраться с классами иерархии View:

//* abstract class ViewBase *
//
// Represents a single view on a page.
function ViewBase()
{
}

// * private/protected fields *

ViewBase.prototype._params = null;
ViewBase.prototype._container = null;

// * public methods *

ViewBase.prototype.GetViewName = function()
{
  throw "ViewBase.GetViewName is not implemented.";
}
ViewBase.prototype.Show = function(params)
{
  if (!this._params) // lazy initialization
  {
    this._Init();
    this._params = {};
  }

  if (!ViewBase._CompareParams(params, this._params))
  {
    this._params = params;
    this._Refresh();
  }

  this._ShowImpl();
}

ViewBase.prototype.Hide = function()
{
  this._container.hide();
}

// * private/protected methods *

ViewBase.prototype._ShowImpl = function()
{
  this._container.show();
}

// View initialization. Container must be specified here (in subclasses).
ViewBase.prototype._Init = function()
{
  throw "ViewBase._Init is not implemented.";
}

ViewBase.prototype._Refresh = function()
{
  throw "ViewBase._Refresh is not implemented.";
}


* This source code was highlighted with Source Code Highlighter.

Поле _container – это jQuery объект, представляющий DOM-элемент контейнера, определенного в html-разметке страницы. Инициализация поля должна происходить в функции Init подклассов.

Благодаря проверке параметров на равенство с помощью функции ViewBase._CompareParams реализуется механизм кеширования: представление (View) не обновляется, если параметры не изменились с прошлого раза.

Функции, бросающие исключение, не что иное, как абстрактные методы класса. Таким образом, все что остаетcя сделать в подклассах ViewBase – определить реализации методов GetViewName, _Init, _Refresh. Вот как это делается в классе ListView:

// * namespace Gallery *
var Gallery = Gallery || {};

// * class ListView *
//
// Represents view on a page.
Gallery.ListView = function() // extends ViewBase
{
  ViewBase.call(this);
}
OO.Extends(Gallery.ListView, ViewBase);

// * public methods *

Gallery.ListView.prototype.GetTypeName = function()
{
  return "list";
}

// * private/protected methods *

Gallery.ListView.prototype._Init = function()
{
  this._container = $("#list_container");
}

Gallery.ListView.prototype._Refresh = function()
{
  var t = this;
  $.ajax(
  {
    url: "/Home/List",
    dataType: "json",
    success: function(data) { t._ListLoaded(data); }
  });
}

Gallery.ListView.prototype._ListLoaded = function(data)
{
  this._container.empty();
  for (var i = 0; i < data.Images.length; i++)
  {
    this._container.append("<p><a href='javascript:;'>" + data.Images[i] + "</a></p>");
  }
  $("a", this._container).click(function() { PageBase.Goto("image", { img: $(this).text() }); });
}


* This source code was highlighted with Source Code Highlighter.

Метод _Refresh для обновления данных страницы обращается к веб-сервису, расположенному по url /Home/List. Обратите внимание, что благодаря упомянутому выше механизму кеширования запрос выполняется только при открытии списка в первый раз.

Ниже приведен код реализации веб-сервиса на ASP.NET MVC:

[AcceptVerbs(HttpVerbs.Get)]
public JsonResult List()
{
    var images = new List<string>();
    foreach (var file in Directory.GetFiles(HostingEnvironment.ApplicationPhysicalPath
                                            + "/Content/Images/"))
    {
        images.Add(Path.GetFileName(file));
    }

    return Json(new {Images = images});
}


* This source code was highlighted with Source Code Highlighter.

Полный код приложения (VS 2008, ASP.NET MVC) можно скачать здесь.

JS-скрипты также можно скачать отдельно.


Подведение итогов


Благодаря паттерну Page-View удалось решить, указанные в начале статьи проблемы. Действительно, несмотря на то, что код получился достаточно громоздким, основная его часть сосредоточена в базовых абстракциях (PageBase и ViewBase). Таким образом, добавление новых страниц (Page) и представлений (View) становится тривиальной задачей.

Другой положительный результат применения паттерна. Обратите внимание, что серверная логика приложения сконцентрирована в методе List. Страница, содержащая разметку, статична. Это позволяет разрабатывать отлично масштабируемые веб-приложения. Динамическое поведение серверной части ограничивается обработкой ajax-запросов. Остальной контент, включая html-разметку и js-код, статичен, и, следовательно, может быть без проблем распределен по разным серверам кластера.

P.S. Огромное спасибо моему коллеге Дмитрию Егорову, который является автором большей части описанных здесь идей :)
Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+10
Комментарии14

Публикации

Истории

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн