Pull to refresh

Удачное портфолио 2: больше интерактивности с SIMILE Exhibit

Reading time 11 min
Views 2.7K
Решение, предложенное для показа портфолио в предыдущем посте, конечно, подкупает своей простотой, однако его можно сделать гораздо функциональнее, воспользовавшись технологией Exhibit из проекта SIMILE.

Проект SIMILE, разрабатываемый MIT, включает в себя набор приложений, предназначенных для обработки и отображения информации в стиле Semantic Web: несколько фреймворков для построения клиентских интерфейсов, средства анализа и отладки XML-документов и HTTP-запросов, набор конвертеров из различных форматов в RDF и многое другое.




Для решения нашей задачи нам хорошо подходит один из фреймворков, Exhibit, предназначенный для отображения набора данных, классифицированных по набору признаков, с возможностью построения пользователем выборок путем указания интересующих его значений классификаторов. Попробуем реализовать режим показа портфолио с использованием этого фреймворка. Наше решение носит чисто демонстрационный характер, не будем обращать внимания на вопрос производительности запросов к базе данных, тонкости верстки и таблицы стилей и т.д.

Набора примеров на главной странице сайта фреймворка достаточно, чтобы проникнуться его возможностями. Мы дадим краткий обзор структуры приложения, использующего фреймворк и обратим внимание на некоторые существенные детали.

База данных



База данных, наиболее часто формируемая веб-сервисом как JSON-данные, представляет собой набор элементов (items), каждый из которых содержит набор свойств (properties).

Каждый элемент принадлежит некоторому типу, который определяется значением служебного свойства type элемента. Вообще говоря, принадлежность элемента тому или иному типу не накладывает жестких требований на состав свойств элемента, однако логично ожидать, что все элементы одного типа имеют сходный набор атрибутов, хотя у некоторых элементов те или иные значения могут отсутствовать. Если тип элемента не указан явно, подразумевается тип по умолчанию, Item.

Система использует набор предопределенных служебных свойств, таких как:
  • id — уникальный идентификатор элемента;
  • label — текстовое описание элемента, не обязано быть уникальным, однако используется в качестве id, если id не указан;
  • type — собственно имя типа элемента;
  • uri — URI элемента.

Элемент может также включать произвольный набор пользовательских свойств различных типов.

Помимо собственное данных, база содержит некоторую метаинформацию, а именно описание типов и свойств. Для описания метаданных используется тот же формат, что и для элементов данных, то есть поддерживаются свойства id, uri и label.

Для типов помимо этих стандартных свойств можно указать свойство pluralLabel, содержащее имя типа во множественном числе (например «Проекты» для типа «Проект»).

В свою очередь, каждое свойство может характеризоваться дополнительным свойством valueType, показывающим, к какому типу относятся значения свойства, а также набором дополнительных свойств, определяющих тестовое отображение значения в пользовательском интерфейсе. Важно, что тип значений свойства может быть как примитивным (text, number, date и т.д.) так и объектным (Item или значение пользовательского типа).

Физически база данных представляется в виде JSON-объекта следующего вида:

{ 
  types: {
    'type1': { 
    // определение типа
    },
    // другие определения типов
  },
  properties: {
    'property1': {
      // определение свойства
    },
    // другие определения свойств
  },
  items: [
    { id: 'item 1',
      // другие свойства
    },
    // другие элементы данных
  ] 
}


Фреймворк по возможности пытается использовать разумные значения по умолчанию, так что в первом приближении можно вообще ограничиться только определениями данных. В целом, модель данных очень близка к RDF, вообще RDF лежит в основе многих продуктов проекта SIMILE.

Данные портфолио



Вернемся к нашей задаче. Исходная база данных проектов представляет собой типичную legacy-базу (сейчас мы работаем над новой, более простой и логичной структурой). Тем не менее, мы можем получить список проектов, классифицированный по следующим признакам:
  1. Дата публикации;
  2. Год публикации (как средство группировки);
  3. Клиент (тут много пустых значений, следствие недостаточной детализации базы до определенного этапа в развитии компании);
  4. Профиль (сфера деятельности клиента);
  5. Тип сайта (магазин, визитка, новостной сайт и т.д.)

Теперь напишем скрипт, который сформирует JSON-файл.

<?php
Core::load('DB', 'IO');

$db = DB::Connection('mysql://www:www@localhost/exhibit');

IO::stdout()->
  write("{\n\"types\": ")->
  write(json_encode(array(
      'проект'    => array('pluralLabel' => 'Проекты',   'label' => 'Проект' ),
      'профиль'   => array('pluralLabel' => 'Профиль',   'label' => 'Профиль' ),
      'категория' => array('pluralLabel' => 'Категории', 'label' => 'Категория' ),
      'клиент'    => array('pluralLabel' => 'Клиенты',   'label' => 'Клиент' ))))->
  write(",\n\"properties\": ")->
  write(json_encode(array(
      'профиль' => array('valueType' => 'профиль'),
      'категория' => array('valueType' => 'категория'),
      'клиент' => array('valueType' => 'клиент'))))->
  write(",\n\"items\": [");

foreach (array(<<<SQL
SELECT 'проект' `type`,
       CONCAT('wp-', wp.id) id,
       wp.name label,
       CONCAT('http://', wp.url) url,
       DATE_FORMAT(FROM_UNIXTIME(wp.publ_date), '%Y-%m-%d') `опубликовано`,
       YEAR(FROM_UNIXTIME(wp.publ_date)) `год`,
       CONCAT('http://www.techart.ru/', wp.screenshot) `скриншот`,
       IF(wp.profiles_id > 0, CONCAT('pr-', wp.profiles_id), NULL) `профиль`,
       IF(wp.type_web_id > 0, CONCAT('ct-', wp.type_web_id), NULL) `категория`,
       IF(pr.clients_id > 0, CONCAT('cl-', pr.clients_id), NULL) `клиент`
FROM web_projects wp, projects pr
WHERE wp.projects_id = pr.id AND wp.screenshot <> ''
SQL
, <<<SQL
SELECT 'профиль' `type`,
       CONCAT('pr-', id) id,
       name label
FROM   profiles
SQL
, <<<SQL
SELECT 'категория' `type`,
       CONCAT('ct-', type_web_id) id,
       name label
FROM   type_web;
SQL
, <<<SQL
SELECT DISTINCT 'клиент' `type`,
       CONCAT('cl-', c.id) id,
       c.name label
FROM   web_projects wp, projects p, clients c
WHERE  wp.projects_id = p.id AND p.clients_id = c.id
SQL
) as $i => $sql) {
	IO::stdout()->write($i > 0 ? ",\n" : '');
  foreach ($db->prepare($sql) as $j => $p)
    IO::stdout()->
      write($j > 0 ? ",\n" : '')->
      write(json_encode($p));
}
IO::stdout()->write("\n] }");
?>


Тут есть пара существенных моментов.

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

Во-вторых, мы генерируем искусственные идентификаторы записей, не несущие никакой смысловой нагрузки. Это правильно с точки зрения реляционной БД, но не очень правильно в смысле semantic-web. Будем считать это издержками работы с legacy-данными, если бы мы проектировали базу с нуля, подход мог бы быть другим.

Собственно, приложение



Exhibit-приложение реализовано в виде набора JavaScript-библиотек и целиком выполняется на стороне клиента. Пользователь создает HTML-страницу определенного формата, в которой декларативным образом описывает приложение.

Основные элементы приложения — это представления (view), шаблоны отображения данных (lens) и фильтры (facet).

Представления — элементы интерфейса, отвечающие за, собственно, отображение отобранных данных. Поддерживается несколько стандартных вариантов отображения:
  • Tile — шаблон по умолчанию, линейный список элементов;
  • Thumbnail — более компактное отображение в виде галереи;
  • Map — отображение элементов данных на карте Google Maps;
  • Timeline — отображение элементов данных в виде timeline, это еще одна великолепная технология SIMILE, достойная отдельного детального описания;
  • Tabular — табличное отображение;
  • Timeplot — отображение в виде графика, удобно для визуализации числовых данных, изменяющихся со временем.

Для отображения отдельного элемента данных внутри представления используются шаблоны отображения (lenses). Шаблон позволяет пользователю определить, каким образом должен выглядеть тот или иной элемент данных внутри представления. Если шаблон не указан, среда использует простой шаблон по умолчанию. Внутри шаблона можно ипользовать не только значения полей элемента данных, но и выполнять над ними различные вычисления.

Фильтры (facets) позволяют пользователю указывать различные критерии выборки. Фильтры тоже бывают разные:
  • List — пользователь выбирает интересующие значения;
  • Numeric Range — пользователь выбирает диапазон значений;
  • Text Search — ну тут все понятно;
  • Tag Cloud — название говорит само за себя;
  • Slider — значение можно выбрать с помощью слайдера, иногда удобно.

Вооружившись всем этим знанием и прочитав документацию, создадим минимальный каркас приложения:

<body>
  <div ex:role="exhibit-collection" ex:itemTypes="проект"></div>
  <table id="layout">
    <tr valign="top">
      <td ex:role="viewPanel" class="workplace">
        <div ex:role="view" ex:viewClass="Thumbnail" ex:abbreviatedCount="40">
          <div ex:role="lens" class="thumbnail-lens" style="display: none;"> 
            <div class="item">
              <div class="screenshot"><a ex:href-content=".url" target="_blank"><img ex:src-content=".скриншот" /></a></div>
              <div class="label"  ex:content=".label"></div>
            </div> 
          </div>       
        </div>
      </td>
      <td class="toolbar">
        <div ex:role="facet" ex:expression=".год" ex:facetLabel="Год" ex:height="150px" ex:sortDirection="reverse"></div>
        <div ex:role="facet" ex:expression=".категория.label" ex:facetLabel="Тип работы" ex:height="150px"></div>
        <div ex:role="facet" ex:expression=".профиль.label" ex:facetLabel="Сфера деятельности" ex:height="150px"></div>
        <div ex:role="facet" ex:expression=".клиент.label" ex:facetLabel="Клиент" ex:height="150px"></div>
      </td>
    </tr>
  </table>
</body>


Мы создали таблицу, большая левая колонка которой содержит представление данных, а правая — набор фильтров. Видно, что все параметры, имеющее отношение к exhibit, задается с помощью атрибутов с префиксом ex.

Обратим внимание на несколько важных моментов:
  1. Для того, чтобы в фильтрах показывались имена, а не идентификаторы, мы используем выражения вида ".категория.label";
  2. Блок с атрибутом ex:role="exhibit-collection" указывает тип данных по умолчанию с которым мы работаем, это позволяет фреймворку сразу же выводить правильный тип в информационной строке вверху, показывающей, сколько записей отобрано. Вообще, имеет смысл писать такую декларацию в случае, если база содержит данные различных типов.
  3. Определение шаблона представления помещено внутрь определения представления. Это указывает, что этот шаблон и это представление необходимо использовать совместно. Вообще представлений и шаблонов может быть несколько.


Осталось подгрузить библиотеку и данные. С данными вопрос однозначный:

<link href="/portfolio.js" type="application/json" rel="exhibit/data" />


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

С кодом все сложнее. Самое простое, что можно сделать — загрузить скрипты непосредственно с сервера проекта следующим образом:

<code>
<script src="http://static.simile.mit.edu/exhibit/api-2.0/exhibit-api.js" type="text/javascript"></script>
</code>


В этом способе хорошо все, за исключением одной проблемы: локализации. То есть локализация в продукте есть, испанская там, или немецкая, а вот русской как раз и нету.

Поэтому если мы хотим быстро получить русскоязычный интерфейс, необходимо скачать библиотеку и загружать ее со своего сервера.

Локальная версия библиотеки



Лучше всего руководствоваться документом Running Exhibit 2.0 yourself. При установке нужно учитывать два обстоятельства.

Во-первых, из-за большого размера библиотек при использовании в production имеет смысл после внесения каких-либо изменений в код пересобирать его в bundle. Для этого в исходниках есть ant-файл, соответственно нужен ant. Для прототипа это можно не делать.

Во-вторых, что более существенно, по умолчанию проект настроен на использование локального веб-сервера jetty, запущенного на порту 8888. Насколько я понимаю, это связано с тем, что некоторые возможности предполагают некую server-side обработку. Если такое поведение нежелательно, необходимо заменить в исходниках адреса, начинающиеся на 127.0.0.1:8888 на соответствующий путь в корневом каталоге сервера без указания имени хоста. Такие изменения необходимо внести в файл exhibit/webapp/api/exhibit-api.js.

Таким образом, в нашем случае, после установки библиотек в /exhibit код загрузки будет выглядеть следующим образом:

<script src="/exhibit/ajax/api/simile-ajax-api.js?bundle=false" type="text/javascript"></script>
<script src="/exhibit/webapp/api/exhibit-api.js?bundle=false&locale=ru" type="text/javascript"></script>


Отметим параметр bundle=false, запрещающий использование сборок, мы могли бы пересобрать все с помощью ant и избавиться от этого параметра (и разумеется, значительно ускорить загрузку).

Параметр locale задает используемую локаль, по умолчанию русской локали нет, но ее несложно изготовить самостоятельно.

Русская локализация



Для этого идем в webapp/api/locales и делаем копию каталога en с именем ru. Затем открываем в этом каталоге файл locale.js и правим в нем пути с en на ru. После этого можно править все остальное, главное помнить, что в русском есть падежи, поэтому необходимо как минимум поменять в некоторых местах порядок слов, а как максимум — реализовать на JavaScript написание с падежами.

В этом смысле особенный интерес представляет файл webapp/api/locales/ru/scripts/data/database-l10n.js, в особенности строка формирования сообщения о количестве выбранных записей, которое мы, недолго думая, переписали так:

span.innerHTML = label + ": <span class='" + countStyleClass + "'>" + count + "</span> "


Альтернативные представления



Поскольку всегда интересно посмотреть динамику работ во времени, реализуем альтернативное представление в виде представление Timeline. Это представление доступно в виде отдельного продукта и может быть использовано как само по себе, так и интегрировано в Exhibition. Собственно, его лучше один раз увидеть, чем долго о нем рассказывать, этот продукт достоен отдельного поста.

Итак, добавляем представление:

Для представления Timeline мы используем свой собственный шаблон отображения, определенный внутри элемента представления.

<div ex:role="view"  ex:viewClass="Timeline"  ex:timelineHeight="760" ex:start=".опубликовано" ex:topBandUnit="month" ex:bottomBandUnit="year" >
  <div ex:role="lens" class="timeline-lens" style="display: none;"> 
    <div class="item">
      <div class="screenshot"><a ex:href-content=".url" target="_blank"><img ex:src-content=".скриншот" /></a></div>
      <div class="label">
        <p><b ex:content=".label"></b></p>
        <p>Опубликовано: <span ex:content=".опубликовано"></span></p>
        <p>Клиент: <span ex:content="if(exists(.клиент),.клиент.label, 'информация отсутствует')"></span></p>
        <p>Профиль: <span ex:content="if(exists(.профиль),.профиль.label, 'информация отсутствует')"></span></p>
        <p>Категория: <span ex:content="if(exists(.категория),.категория.label, 'информация отсутствует')"></span></p>
      </div>
    </div> 
  </div>       
</div>


Теперь мы можем переключаться между двумя видами представлений.

Таким образом, подготовив данные, создав одну HTML-страницу и немного повозившись с локализацией, мы получили вот такую картину (может тормозить, для продакш можно оптимизировать). Высокая скорость разработки делает систему пригодной для быстрого прототипирования и анализа информации, например, при анализе данных, полученных в результате исследований. Для веб-разработки тоже существует масса применений: показ статистических данных, различных описаний, товаров в интернет-магазинах и т.д.

Ну и конечно, мы рассмотрели самый простой случай использования, в более сложных случаях можно расширять код фреймворка, реализовывать альтернативные представления и так далее.

Ограничения



Как и у любой технологии, у Exhibit есть свои ограничения.

Прежде всего, данные необходимо клиенту передать и на клиенте их обработать. Это накладывает определенные ограничения на количество записей, которые можно просматривать таким образом. С точки зрения скорости обработки я бы сказал, что использовать больше тысячи записей неразумно (хотя браузеры совершенствуются, и не в последнюю очередь в плане скорости выполнения JavaScript). С точки зрения передачи большого количества JSON-данных настоятельно рекомендуется использовать gzip-сжатие путем установки соответствующего модуля на сервере, результат заметен невооруженным глазом.

Решение работает в наиболее распространенных браузерах, тем не менее возможны проблемы с Оперой и IE6.
Tags:
Hubs:
+5
Comments 2
Comments Comments 2

Articles

Information

Website
www.techart.ru
Registered
Founded
Employees
101–200 employees
Location
Россия