30 мая 2014 в 19:10

Пишем свою IDE со встроенным дизайнером интерфейсов на PHP и ExtJS

В статье рассматриваются концепты создания IDE и дизайнера интерфейсов с использованием ExtJS и PHP. С одной стороны, создание подобных редакторов довольно редкая задача, с другой — концепты и приемы можно использовать для создания различных визуальных конфигураторов.


Как написать свою IDE со встроенным дизайнером интерфейсов, как сделать это быстро и с минимальными усилиями? Именно такой вопрос возник однажды в проекте, использующем связку ExtJS и PHP. Горящие сроки, растущая очередь задач. Список заданий ежедневно пополняется огромным количеством форм ввода, таблиц и отчетов, все это необходимо обрабатывать, фильтровать и отображать для пользователя.

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

Беглый поиск выдал Ext Designer (Sencha Architect) — интересный и полезный инструмент (Ext MVC еще не существовало). Ext Designer так и не помог решить поставленные задачи, но обо всем поподробнее.

В то же время был замечен проект PHP-Ext — интересная обертка для ExtJS, написанная на PHP. Ее можно было бы использовать для генерации этого нескончаемого потока интерфейсов, но хотелось большего: cмеси Ext Designer и PHP-обертки над ExtJS, чтобы это еще можно было научить “подглядывать” в базу данных и на основе таблиц строить формы. А еще лучше на основе структуры объектов ORM, ведь там есть все названия полей их типы и валидаторы.

Поиск подобного инструмента не увенчался успехом.



Из чего будет состоять IDE:
1 обертка для ExtJS, которая “умеет” генерировать код компонент;
2 файл проекта с простым API добавления элементов обертки ExtJS;
3 сборщик кода, который располагает элементы и кэширует результат;
4 дизайнер интерфейсов для упрощения настройки проекта, редактор кода;
5 “плюшки” в виде готовых компонент для автоматизации рутинных задач:
5.1 неочевидные особенности наследования в ExtJs для начинающих;
5.2 доопределение редакторов;
5.3 формирование url для Ajax-запросов за два клика;
5.4 импорт структуры из базы данных, автоматическое создание форм;
5.5 подключение внешних файлов и проектов;
5.6 локализация интерфейсов;
5.7 редакторы событий и методов;
6 бекенд дизайнера, который транслирует запросы фронтенда в команды API работы с проектом;
8 генератор проектов.

1 Обертка



Под эту задачу придется писать собственную обертку для ExtJS, адаптированную под нужды задумки. Поскольку объем библиотеки достаточной большой, сэкономим время отделив наборы свойств компонент библиотеки от описания их поведения. Не все объекты ExtJs будут зеркально отражены в коде PHP. Опишем классы ограниченного количества объектов таких, как Grid, Store, Window и т.п. Все остальные объекты могут физически не существовать в коде, а создаваться на основе набора свойств описанных в иерархии.

Таким образом, за пару дней была воссоздана иерархия свойств компонент ExtJS, в то время еще версии 3 (несколько десятков классов). Далее можно было по мере необходимости описывать поведение нужных компонент, основные генерировали на уровне json-конфига.

Получаем иерархию следующего вида (кусок диаграммы, времена Zend_Framework 1):



Ext_Object и его наследники отвечают за поведение компонента;
Ext_Config отвечает за хранение, валидацию и сериализацию свойств;
Ext_Property и его наследники отвечают за описание характеристик конкретного компонента ExtJS (классы с публичными свойствами, в какой-то мере повторяющие иерархию библиотеки ExtJs).
Ext_Virtual — класс. Объекты этого класса создаются фабрикой в том случае, если нет прямого наследника Ext_Object, описывающего поведение компонента. Создается на основе описания свойств Ext_Property_xxx. Отличается тем, что принимает имя класса для эмуляции, используя имя представляется компонентам библиотеки.

Ext_Object реализует метод __toString, возвращающий ExtJS код компонента. При необходимости этот метод можно переопределить в наследниках. Если объекты вложены друг в друга, в момент приведения к строке корневого объекта вся цепочка с легкостью самораспакуется (превратится в строку Js-кода).

Как показала практика это решение позволило избежать головной боли.

2 Файл проекта



Интерфейс на ExtJS состоит из компонент, выстроенных в цепочку и расположенных в Layout.

Необходимо как-то хранить настройки связей компонент проекта. В качестве конфигурационного файла можно использовать XML или какой-то другой похожий формат.
В таком случае каждый раз при загрузке проекта придется анализировать конфигурацию и инициализировать объекты, что может занять продолжительное время. Нужен простой, быстрый и легкий формат.
Что если объявить класс Designer_Project, который бы представлял сам проект и имел простенький API по добавлению элементов, а сами элементы хранил в древовидной структуре (внутри лежал бы объект работающий с древовидной структурой).
На тот момент был уже написан класс Tree, который достаточно шустро работал с древовидными структурами, без труда справлялся с иерархией до 25 000 — 30 000 вложенных элементов менее чем за секунду.

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

Основная проблема Ext Designer на то время представлялась в том, что при каждом изменении кода нужно пересобрать приложение и публиковать скрипты.

Структура Designer_Project (файл проекта):
— системное описание контейнеров (какие классы можно перемещать, какие могут содержать вложенные элементы и т.д);
— настройки текущего проекта (название, неймспейсы, подключенные файлы и прочее);
— API (набор методов для работы с проектом);
— Tree (дерево элементов, структура проекта).

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

Так появился файл проекта. Попутно написано несколько вспомогательных классов, например адаптер Designer_Storage (вдруг мы передумаем хранить проекты в файлах). Проведено несколько тестов на производительность, результаты были оптимистичными, задумка работала шустро. Важно отметить, что дерево проекта знает лишь о структуре вложенности элементов, но сами объекты фактически не находятся друг в друге. Designer/Project.php

3 Сборщик кода



Поскольку класс Designer_Project представляет собой контейнер с простым API и совершенно не знает, что делать со своим содержимым, потребуется вспомогательный механизм, который умеет правильно располагать код элементов в нужной последовательности — это класс Designer_Project_Code. Пожалуй самый сложный компонент по количеству различных условий и ветвлений. Получая на вход объект проекта должен вернуть JS-код для интерфейса. Рекурсивно проходя дерево проекта получает код компонент и располагает элементы в нужной последовательности. Важно отметить, что непосредственно код компонента выдает обертка для ExtJS, сам сборщик занимается расположением этого кода в нужной последовательности. Он должен определить, какие компоненты должны быть объявлены первыми, получить их код и вставить ссылки в зависимые компоненты.

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



Со временем он приобрел терпимый вид и структуру. Designer/Project/Code.php

Для объектов JS, являющихся расширениями базовых компонент ExtJS, был использован трюк для упрощения расположения вложенных элементов. Было создано свойство childObjects, представляющее собой список всех вложенных элементов, таким образом к ним очень легко обращаться и линковать в items.


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

Пол дела сделано. Создан упрощенный аналог обертки PHP-EXT, умеющий складываться в проект и работающий намного шустрее. Функционал ограничен, но никто не мешает его развивать.

4 Дизайнер



Пришло время для самого интересного — создания дизайнера интерфейсов. Концептуально он должен представлять собой:
— панель с тулбаром, в котором бы располагался список компонент, которые можно разместить в проекте (кнопки, формы, окна, панели);
— основную форму, которая бы отображала результаты рендеринга проекта;
— иерархию компонент (использован TreePanel);
— редактор свойств (Property Grid).

На текущий момент дизайнер имеет следующий вид (сильно отличается от первого):



1. панель настроек проекта (загрузка, сохранение, переключение режима дизайнер/редактор кода и прочее);
2. тулбар со списком компонент, которые можно добавить в проект;
3. панель, отображающая структуру проекта, поддерживает Drag & Drop перемещение элементов, при выборе элемента для него подгружается индивидуальная панель настройки свойств;
4. панель настройки свойств компонента (содержит дополнительные панели редактирования событий и методов).
5. центральная панель (отображает результат рендеринга проекта, на этом скриншоте — редактор локализаций). При загрузке проекта сервер сохраняет копию его объекта в сессию, все манипуляции производит с ней. Таким образом, если упал интерфейс можно перезагрузить окно, изменения не будут потеряны. Кнопка “Сохранить” сбрасывает проект на диск.

После внесения изменений интерфейс отправляет запрос на сервер, нужный контроллер принимает запрос, вносит изменения в объект Designer_Project. После успешного применения изменений дизайнер запрашивает перестроение JS.

Основная панель дизайнера, которая отвечает за расположение элементов в проекте представляет собой дерево с поддержкой drag & drop:



Дерево запрашивает список элементов у сервера, тот, в свою очередь, извлекает структуру из проекта при помощи API. Во время перетаскивания элемента отправляется запрос на сервер с инструкцией, какой компонент перемещаем. API Designer_Project содержит метод перемещения элементов по дереву. При клике по узлу дерева (выборе нужного объекта) инициируется событие itemSelected, отображается панель свойств этого компонента.

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

В качестве редактора свойств использован расширенный компонент Property Grid, дополненный методами общения с сервером (запрашивает список полей, отправляет изменение свойств, инициирует события).

В нашем случае этот компонент назывался designer.properties.Panel



Одной панелью свойств не обойтись, в некоторых случаях требуется возможность дополнительной настройки свойств, представляющих собой редакторы и окна. При необходимости расширения списка настроек для этого типа объектов назначается индивидуальный редактор, отнаследованный от designer.properties.Panel.

Первоначальными возможностями дизайнера не получилось решить все задачи, поэтому к проекту был привязан файл “actionJs” (файл с кодом JavaScript, используется для того, что нельзя сделать стандартными средствами, подключается после JS-проекта).

В качестве редактора кода используется codemirror.net.

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

Как отобразить результат? Можно рендерить проекты прямо в DOM открытой страницы, это очень быстро, лаги перестроения практически незаметны, после перекидывания элемента по дереву проходят доли секунды, прежде чем интерфейс перестроится. У этого решения есть одна серьезная проблема, если что-то идет не так в разрабатываемом проекте (неверно задано свойство или еще что), ошибки JS вызовут обрушение всего дизайнера. Разрабатываемый интерфейс лучше перенести в iframe это хоть и замедлит отклик, но обрушение кода проекта не приведет к глобальному краху. Сам iframe можно положить в центральную панель, при необходимости запрашивать обновление содержания.

Бы ло бы красиво, если элементы можно было бы кидать на форму и двигать/перемещать прямо внутри разрабатываемого проекта, как во всех “взрослых” дизайнерах, но по простому этот вопрос не решить. От этой затеи пришлось на время отказаться, сроки поджимали.
Позже появился механизм взаимодействия основного интерфейса дизайнера и самого создаваемого проекта. Можно двигать, растягивать колонки таблиц, менять размеры окон, все это сохраняется. Принцип работы достаточно прост — в разрабатываемый интерфейс во время режима разработки добавляются обработчики событий, которые формируют команду для дизайнера и помогают взаимодействовать с контроллерами дизайнера, внешний интерфейс дизайнера ожидает команды и при ее появлении реагирует. Например, при перемещении колонок таблицы отправляется запрос к serverside API и оповещается основной интерфейс дизайнера.


5 Плюшки в виде готовых компонент, автоматизации рутинных задач


5.1 Неочевидные особенности наследования в ExtJs для начинающих.



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

Ext.define('mypanel',{
    extend:'Ext.Panel',
    someProperty:{
        a:1,b:2,c:3
    },
    someProperty2:[1,2,3]
});

Ext.define('mypanel2',{
    extend:'mypanel'
});
var a = Ext.create('mypanel');
var b = Ext.create('mypanel2');

b.someProperty.a = 100;
b.someProperty2.push(100);

console.log(a.someProperty);
console.log(b.someProperty);
console.log(a.someProperty2);
console.log(b.someProperty2);


Object { a=100, b=2, c=3}
Object { a=100, b=2, c=3}
[1, 2, 3, 100]
[1, 2, 3, 100]

Эти особенности важно помнить, т.к. это спасет вам много нервов. Это не единственный подводный камень, будьте внимательны.

5.2 Редакторы



Переопределим редакторы базовых свойств, например, редактор store заменяем с текстового поля на выпадающий список всех созданных в проекте хранилищ (запрашиваются у сервера, отдаются Designer_Project API), так же и с другими подобными свойствами (layout, align и многими др.).


5.3 Формирование url для Ajax-запросов за два клика



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

Для упрощения назначения url-адресов (ajax-запросы, proxy url) написан компонент, который анализирует файловую и кодовую структуру. Reflection позволяет получать и анализировать список доступных действий для контроллеров, теперь их не нужно писать руками, просто ткнуть мышкой.

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


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



5.4 Импорт структуры из базы данных, автоматическое создание форм


Как еще облегчить жизнь программисту? Конечно же импортом полей в формы, хранилища, таблицы исходя из структуры БД или ORM.

Выбираем таблицу / объект, из которого мы хотим импортировать поля. Исходя из его структуры и типов данных система автоматически подставляет нам нужные значения в ячейки и заполняет иерархию форм.





Например, для int подставляется Ext.form.field.Number, для varchar — Ext.form.field.Text и т.д.
Попутно добавляется fieldLabel, если это поле формы или формат даты, если это поле хранилища и др. свойства.

Теперь нудная процедура описания элементов занимает пару кликов.

5.5 Подключение внешних файлов и проектов



Почему бы не дать возможность подключать в проект внешние JS-файлы и другие проекты. Мы можем создать проект, в котором опишем специфический компонент — редактор, позже подключим его там, где он нужен. Главное — разделить пространства имен проектов так, чтобы генератор кода располагал каждый проект в своем namespace, это не представляет особой сложности.



Все что потребовалось — добавить в Designer_Project api поддержку добавления списка файлов и проектов, назначение неймспейсов. Designer_Project_Code за пару часов был обучен раскладывать код внутрь неймспейсов и рекурсивно рендерить проекты.

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

5.6 Локализация интерфейсов



Долгое время оставалась не решенной задача локализации интерфейса. Не было ясно, как подступиться к ее решению. Весь проект строился на простых решениях, не хотелось городить огород, да и времени на это особо не было. В один момент оно нашлось.
Сама платформа использовала файлы локализации, которые перегенерировались при изменении в интерфейсе управления локализациями. В итоге на странице существовал некий JS-объект вида:

appLang = {
   yes:”Да”,
   no:”Нет”,
   cancel: ‘Отмена’,
     ...
};

Идея проста — разрешить разработчику вводить в качестве значений строковых свойств Js-код, для этого был выдуман токен “[js:]” (во время генерации кода свойства с таким токеном оформлялись как js-код).

Было:

Стало возможным:

С учетом того, что интерфейс управления локализациями был уже готов, работать с ним было одно удовольствие. Реализация решения заняла от силы 20 минут. Теперь можно локализовать проект не изменяя содержимое файла.

5.7 Редакторы событий и методов



Следующим прорывом стало добавление возможности расширения объектов, теперь можно добавлять методы и события. Реализация событий очень похожа на реализацию свойств. Похожим образом были реализованы и методы. В этот момент в системе появилось разделение на стандартные события, описанные в обертке Ext, и события, созданные пользователем (методы могли быть созданы только пользователем для “расширенных” (атрибут isExtended) объектов).







Дизайнер начал генерировать волне понятный, читаемый код.

После этого появляется очевидная проблема: события, методы и реакции разбросаны по элементам, сложно найти где их редактировать. Добавим в дизайнер отдельные вкладки со списками событий и методов, сгруппированные по объектам. Запрашиваем у Designer_Project список объектов, отображаем в виде Grid с группировкой.





6 Бэкенд



C точки зрения бэкенда все достаточно просто, можно использовать любой фреймворк. Нужен набор контроллеров и действий для манипуляций с проектом. Запускаем приложение, загружаем проект дизайнера, описываем список методов, которые обращаются к API Designer_Project и выполняют различные манипуляции с проектом. Из особенностей — понадобится контроллер, который может вывести собранный проект в интерфейс и подключить нужные JS-файлы.

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

7 Генератор проектов



Генератор представляет собой шаблон с набором действий над файлом проекта.

В упрощенном виде:

// создать новый проект
$project = new Designer_Project();
// создать объект обертки Ext_Panel реализующий обертку для Ext.panel.Panel
$panel = Ext_Factory::object('Panel',array(
           'width'=>100,
           'height'=>200,
           'title'=>'My Panel',
           'layout'=>'fit'
));
// назначить уникальный идентификатор
$panel->setName('myPanel');
// добавили панель в проект
$project->addObject(0, $panel);
// инициализировать хранилище
$designerStorage = Designer_Factory::getStorage($config);
// сохранить проект
$designerStorage->save($projectFile , $project);


Имея подобную структуру проекта написать генератор проектов на основе ORM не составило труда. За день было создано несколько шаблонов генерации стандартных интерфейсов, теперь на создание типового интерфейса уходило пара кликов, остальное время тратилось на нестандартное оформление и доработку.

На реализацию первой версии дизайнера ушло 3 недели отпуска — довольно короткий срок для такой глобальной цели.




Полученный профит:

— стандартные интерфейсы генерировались одним кликом, далее дорабатывались в дизайнере;
— значительно уменьшилось число ошибок в JS-коде;
— молодые разработчики легче вникали в ExtJS и разработку сложного проекта;
— дорабатывать интерфейс стало намного проще и интереснее, элементы передвигались одним движением мыши, без боязни забыть захватить кусок кода и упустить связанность с другим элементом;
— возросла скорость разработки продукта и прототипов;
— такие вещи, как поменять название кнопки или колонки, перестали вызывать батхерт во время поиска куска кода с инициализацией нужного элемента;
— получен интересный опыт в разработке собственной среды разработки “на коленке”.
— разработка на PHP приобрела совершенно новую удобную форму;
— удалось узнать много тонкостей ExtJS, c которыми не приходилось сталкиваться до этого момента.

Зачем это было все нужно?


За годы работы с ExtJS надоело писать одно и то же. Инициализировать компоненты, настраивать их, боясь пропустить запятую или что-то слинковать. Это задача, как оказалось, достаточно легко автоматизируется, в освободившееся время можно заняться более интересными вещами.

Поскольку инструмент оказался весьма полезным, появилось желание поделиться им с сообществом, внести свой вклад в OpenSource. Дизайнер и несколько других наработок были переработаны, собраны в одну платформу DVelum, разработка которой ведется уже несколько лет. С результатами можно ознакомиться на официальном сайте dvelum.net

Кирилл Егоров @coh
карма
21,0
рейтинг 0,0
Пользователь
Похожие публикации

Комментарии (15)

  • 0
    Я правильно понимаю, что все просто, и решается в конструкторе, пока дизайн от ExtJS?
    Тут написано, но пока не попробуешь не очень понятно. Если не сложно, в 2-х словах расскажите.
    • 0
      Это не совсем относится к теме статьи. В платформу интегрировано несколько модулей, реализующих функционал схожий с CMS. На той странице описано как работать со страницами блоками и меню, которые отображаются на frontend, дизайн публичной части не обязательно должен использовать ExtJS.
  • 0
    Знакомые интерфейсы! :)
  • +15
    дерево с поддержкой drug & drop

    панель, отображающая структуру проекта, поддерживает Drug & Drop

    Дерево и панель поддерживающие наркотики, это действительно круто! =)
    P.S. Поправьте в тексте (drag)
  • 0
    А потом вы поймете что надо было не код генерить, а хранить результаты дизайна как данные, и их рендерить. Но уже будет поздно.
    • 0
      Результаты дизайна лежат как данные в файле проекта. Код генерится/рендерится при изменении данных и первом отображении.
      • 0
        В файле проекта. А должны быть в базе системы — такие же данные как и все остальные. Доступные через тот же слой доступа к данным.
        Это добавило бы гибкости без особенных затрат — глядишь, заказчики сами бы себе формы добавляли…
        • 0
          Заказчики могут себе добавлять формы если им дать доступ. Стоит ли это делать, другой вопрос. Файл проекта чем не база системы? За сохранение данных отвечает абстрактный слой хранилища, при желании можно сохранять проект в СУБД или любом другом месте. Приведите пример проекта, в котором UI лежит в базе системы. Возможно я вас неверно понимаю, что вы подразумеваете под базой?
  • 0
    А в чем прелесть генерации js кода для построения интерфейса, если ext поддерживает постройку формы через json? Не облегчило бы это реализацию, путем простой сериализации обьектов в json?
    • 0
      Хранение объектов в формате JSON значительно усложнило бы реализацию. Кроме самой структуры расположения есть еще события, методы (+ исполняемый код), связи, наследование. Пришлось бы делать двойную работу генерировать этот JSON, потом распознавать для внесения изменений.
      • 0
        Ну при наследовании, можно же сделать компонент уже готовый, написать на js и подключить, и вызывать его через xtype, чем описывать тот же компонент через класы в php?
        • 0
          Да, конечно можно, так и делают. В нашем случае была задумка сделать визуальный редактор. Тут либо писать парсер ExtJS, либо генерировать код ExtJS, что значительно проще. Вы верно подметили, что логику создания интерфейса можно было бы полностью перенести на сторону клиента и это была бы совсем другая история со своими проблемами и решениями. Одна из задач, поставленная перед проектом — генерировать интерфейсы автоматически на стороне сервера на основе ORM.
          • 0
            Просто я решал такую же задачу, и автоматическая генерация форм, и по описанию в xml. Но реализовывалось генерацией json, правда тогда мы только знакомились с ExtJS, версия была помоему 3.5. Идея была немного в другом ключе, тоесть мы описываем формы одними обьектами (или автоматически или в xml), а в зависимости какая форма нам нужна ExtJS или обычная HTML выбирали специальный класс для рендеринга єтих обьектов. Вот сейчас собираюсь написать новую версию, учитывая все косяки которые были в предидущей реализации, и новые возможности. И возникла такая идея, есть круд контроллер, обычными 4 методами, и 5-й метод получение описания модели завязаной на этот контроллер, тоесть один метод всегда возвращает описание модели, а все другие гриды и формы используют эту модель, при чем загвоздка в том что отдавать js или json. Пока больше склоняюсь к json т.к. в реализации проще.
  • 0
    не совсем понял про локализацию. язык сменить можно только перед компиляцией?
    • 0
      В нашем случае за язык отвечает отдельный модуль, внутри дизайнера используется вызов конструкций JS, например appLang.someWord. Язык переключается в настройках системы. К странице подключается нужный файл локализации, тексты автоматически заменяются. Эти действия не требуют перекомпиляции.

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