На сегодняшний день существует великое множество javascript фреймворков, по многим из них написаны горы документации. Я хотел бы остановиться на фреймворке, который, по неизвестной мне причине, не пользуется особой популярностью у российских разработчиков.
Фреймворк называется qooxdoo. Произносится «куксду» (кому удобнее английская транстрипция: [‘kuksdu:]).
На Хабре было несколько попыток написать про этот фреймворк, но все они свелись к новостям о выходе новой версии или к парам абзацев в статьях типа «смотрите каких фреймворков понаписали». Я несколько лет работаю с qooxdoo и мне хотелось бы восполнить этот пробел.
Вкратце о том, что это за зверь и с чем его едят. Больше всего фреймворк «похож» на ExtJS. Слово «похож» не совсем корректное, в данном случае, но я затрудняюсь подобрать более подходящее. Разработка проекта началась в недрах компании 1&1 Internet AG. Первая публичная версия 0.1 вышла в 2005 году. Текущая актуальная версия 4.1, про нее и будем вести речь. Некоторые моменты позволяют мне сказать, что разработчики вдохновлялись Qt при создании своего детища. Основная изначальная задумка разработчиков дать возможность разрабатывать веб приложения людям без знания HTML, CSS и DOM модели. С помощью qooxdoo это возможно. Новичок, которому требуется написать, например, админку в виде single page application (далее SPA) и который не знает ни одного HTML тега, а про CSS вообще никогда не слышал, действительно, сможет это сделать. Это не означает, что знания HTML, CSS и DOM модели вдруг резко стали не нужны. Просто, поначалу, можно обойтись без них. Что будет особенно интересно, например, разработчикам десктопных приложений, которым потребовалось что-то сделать в вебе.
В конце статьи вы можете найти немного полезных ссылок. В частности, там есть ссылки на разнообразные демо и примеры реального использования фреймворка в продакшене.
Просто так рассказывать о фреймворке скушно и неинтересно. К тому же, разработчики это уже и так сделали. Поэтому я решил сделать какой-нибудь простенький пример для демонстрации возможностей фреймворка. Многие знают о проекте http://todomvc.com/. Вот и мы с вами сделаем что-то максимально похожее с использованием qooxdoo. Справедливости ради, разработчики уже сделали демо todo листа, но это не совсем то, что нам нужно.
Итак, приступим.
Следует оговориться, что рассматриваться будет именно SPA (Desktop в терминологии qooxdoo). Для начала необходимо загрузить qooxdoo sdk. Сделать это можно по этой ссылке. SDK содержит ряд утилит, которые позволяют сгенерировать шаблон приложения и собрать отладочную и релизную версию, собрать автоматическую докуентацию, туты и т.д. Ознакомиться с документацией по тулчейну можно тут.
Для создания шаблона приложения мы запустим:
create-application.py --name=todos
После этой операции мы получим следующий каркас приложения:
Приложение сгенерируется не пустым. Оно будет иметь кнопку, по нажатию на которую будет выводиться alert.
Основной файл Application.js будет содержать следующий код:
/** * This is the main application class of your custom application "todos" * * @asset(todos/*) */ qx.Class.define("todos.Application", { extend : qx.application.Standalone, members : { /** * This method contains the initial application code and gets called * during startup of the application * * @lint ignoreDeprecated(alert) */ main : function() { // Call super class this.base(arguments); // Enable logging in debug variant if (qx.core.Environment.get("qx.debug")) { // support native logging capabilities, e.g. Firebug for Firefox qx.log.appender.Native; // support additional cross-browser console. Press F7 to toggle visibility qx.log.appender.Console; } /* ------------------------------------------------------------------------- Below is your actual application code... ------------------------------------------------------------------------- */ // Create a button var button1 = new qx.ui.form.Button("First Button", "todos/test.png"); // Document is the application root var doc = this.getRoot(); // Add button to document at fixed coordinates doc.add(button1, {left: 100, top: 50}); // Add an event listener button1.addListener("execute", function(e) { alert("Hello World!"); }); } } });
Для того, чтобы увидеть задумку авторов, нам нужно будет собрать дебажную или продакшн версию приложения.
Первый вариант получится, если перейти в папку проекта и запустить:
./generate.py source
второй можно получить после запуска:
./generate.py build
После этого грузим в браузере соответствующий index.html файл и видим вот такую картинку:
На кнопку можно нажимать, а можно не нажимать. Можно грабить корованы. На этом возможности приложения заканчиваются. Чуда не случилось, дальше придется писать код, чем мы, собственно, и займемся.
Для нетерпеливых сразу даю ссылку на github с готовым вариантом, с которым можно играться. Для того, чтобы получилось, кроме исходников с гитхаба необходимо скачать SDK и прописать в файле config.json корректный путь «QOOXDOO_PATH». После чего необходимо собрать требуемую версию, как описано выше.
Ну а мы рассмотрим процесс создания приложения последовательно, в его естественном виде.
Для начала мы создадим заготовку для виджета окна для нашего todo листа и безжалостно удалим из Application.js все что там нам нагенерировал генератор. Получится у нас следущее.
Window.js
qx.Class.define("todos.Window", { extend : qx.ui.window.Window, construct: function(){ this.base(arguments); this.set({ caption: "todos", width: 480, height: 640, allowMinimize: false, allowMaximize: false, allowClose: false }); this.addListenerOnce("appear", function(){ this.center(); }, this); } });
Application.js
/** * @asset(todos/*) */ qx.Class.define("todos.Application", { extend : qx.application.Standalone, members : { main : function() { // Call super class this.base(arguments); var wnd = new todos.Window; wnd.show(); } } });
После сборки мы увидим вот такую красоту:
Пора наполнить ее смыслом. Нам будут необходимы следующие элементы: тулбар, запись todo листа и элемент добавления записи в лист. Запись todo листа является повторяющимся элементом, оформим его в виде отдельного виджета. Тулбар и элемент добавления записи в лист можно сделать как отдельными виджетами, что позволит их использовать повторно, так и частью Window. Тулбар сделаем отдельным виджетом, а элемент добавления записи оставим частью Window, чтобы показать, что можно и так и так. Сделаем все вышеописанное и наполним виджеты жизнью.
ToDo.js
qx.Class.define("todos.ToDo", { extend: qx.ui.core.Widget, events : { remove : "qx.event.type.Event" }, properties: { completed: { init: false, check: "Boolean", event: "completedChanged" }, appearance: { refine: true, init: "todo" } }, construct: function(text){ this.base(arguments); var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 20); grid.setColumnFlex(1, 1); grid.setColumnWidth(2, 20); grid.setColumnAlign(0, "center", "middle"); grid.setColumnAlign(1, "left", "middle"); grid.setColumnAlign(2, "center", "middle"); this._setLayout(grid); this._add(this.getChildControl("checkbox"), {row: 0, column: 0}); this._add(this.getChildControl("text-container"), {row: 0, column: 1}); this._add(this.getChildControl("icon"), {row: 0, column: 2}); this.getChildControl("label").setValue(text); this.addListener("mouseover", function(){this.getChildControl("icon").show();}, this); this.addListener("mouseout", function(){this.getChildControl("icon").hide();}, this); this.getChildControl("icon").hide(); this.getChildControl("text-container").addListener("dblclick", this.__editToDo, this); }, members : { // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "checkbox": control = new qx.ui.form.CheckBox; this.bind("completed", control, "value"); control.bind("value", this, "completed"); break; case "text-container": control = new qx.ui.container.Composite(new qx.ui.layout.HBox); control.add(this.getChildControl("label"), {flex: 1}); break; case "label": control = new qx.ui.basic.Label; control.bind("value", control, "toolTipText"); break; case "textfield": control = new qx.ui.form.TextField; control.addListener("keypress", function(event){ var key = event.getKeyIdentifier(); switch(key) { case "Enter": this.__editComplete(); break; case "Escape": this.__editCancel(); break; } }, this); control.addListener("blur", this.__editComplete, this); break; case "icon": control = new qx.ui.basic.Image("todos/icon-remove-circle.png"); control.addListener("click", function(){ this.fireEvent("remove"); }, this); break; } return control || this.base(arguments, id); }, __editToDo : function() { var tc = this.getChildControl("text-container"); var tf = this.getChildControl("textfield"); tc.removeAll(); tc.add(tf, {flex: 1}); tf.setValue(this.getChildControl("label").getValue()); tf.focus(); tf.activate(); }, __editComplete : function() { this.getChildControl("label").setValue(this.getChildControl("textfield").getValue()); this.__editCancel(); }, __editCancel : function() { var tc = this.getChildControl("text-container"); tc.removeAll(); tc.add(this.getChildControl("label"), {flex: 1}); } } });
StatusBar.js
qx.Class.define("todos.StatusBar", { extend: qx.ui.core.Widget, events: { removeCompleted: "qx.event.type.Event" }, properties: { todos: { init: [], check: "Array" }, filter: { init: "all", check: ["all", "active", "completed"], event: "filterChanged" } }, construct: function() { this.base(arguments); var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 100); grid.setColumnFlex(1, 1); grid.setColumnWidth(2, 130); grid.setColumnAlign(0, "left", "middle"); grid.setColumnAlign(1, "center", "middle"); grid.setColumnAlign(2, "right", "middle"); grid.setRowHeight(0, 26); this._setLayout(grid); this._add(this.getChildControl("info"), {row: 0, column: 0}); this._add(this.getChildControl("filter"), {row: 0, column: 1}); this._add(this.getChildControl("remove-completed-button"), {row: 0, column: 2}); this.update(); }, destruct: function() { this.__rgFilter.dispose(); }, members : { __rgFilter: null, update: function() { var todosCount = this.getTodos().length; var itemsLeft = this.getTodos().filter(function(item){return !item.getCompleted();}).length; this.getChildControl("info").setValue("<b>"+itemsLeft+"</b> items left"); if (itemsLeft === todosCount) { this.getChildControl("remove-completed-button").exclude(); } else { this.getChildControl("remove-completed-button").setLabel("Clear completed ("+(todosCount-itemsLeft)+")"); this.getChildControl("remove-completed-button").show(); } }, // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "info": control = new qx.ui.basic.Label; control.setRich(true); break; case "filter": control = new qx.ui.container.Composite(new qx.ui.layout.HBox); control.add(this.getChildControl("rb-filter-all")); control.add(this.getChildControl("rb-filter-active")); control.add(this.getChildControl("rb-filter-completed")); this.__rgFilter = new qx.ui.form.RadioGroup( this.getChildControl("rb-filter-all"), this.getChildControl("rb-filter-active"), this.getChildControl("rb-filter-completed") ); this.__rgFilter.addListener("changeSelection", this.__onFilterChanged, this); break; case "rb-filter-all": control = new qx.ui.form.RadioButton("All"); control.setUserData("value", "all"); break; case "rb-filter-active": control = new qx.ui.form.RadioButton("Active"); control.setUserData("value", "active"); break; case "rb-filter-completed": control = new qx.ui.form.RadioButton("Completed"); control.setUserData("value", "completed"); break; case "remove-completed-button": control = new qx.ui.form.Button; control.addListener("execute", function(){ this.fireEvent("removeCompleted"); }, this); break; } return control || this.base(arguments, id); }, __onFilterChanged : function(event) { this.setFilter(event.getData()[0].getUserData("value")); } } });
Window.js
qx.Class.define("todos.Window", { extend: qx.ui.window.Window, properties: { appearance: { refine: true, init: "todo-window" }, todos: { init: [], check: "Array", event: "todosChanged" }, filter: { init: "all", check: ["all", "active", "completed"], apply: "__applyFilter" } }, construct: function(){ this.base(arguments); this.set({ caption: "todos", width: 480, height: 640, allowMinimize: false, allowMaximize: false, allowClose: false }); this.setLayout(new qx.ui.layout.VBox(2)); this.add(this.getChildControl("todo-writer")); this.add(this.getChildControl("todos-scroll"), {flex: 1}); this.add(this.getChildControl("statusbar")); this.addListenerOnce("appear", function(){ this.center(); }, this); }, destruct : function() { var todoItems = this.getTodos(); for (var i= 0, l=todoItems.length; i<l; i++) { todoItems[i].dispose(); } }, members : { // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "todo-writer": var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 20); grid.setColumnFlex(1, 1); grid.setColumnAlign(0, "center", "middle"); grid.setColumnAlign(1, "left", "middle"); control = new qx.ui.container.Composite(grid); control.add(this.getChildControl("checkbox"), {row: 0, column: 0}); control.add(this.getChildControl("textfield"), {row: 0, column: 1}); break; case "checkbox": control = new qx.ui.form.CheckBox; control.addListener("changeValue", this.__onCheckAllChanged, this); break; case "textfield": control = new qx.ui.form.TextField; control.setPlaceholder("What needs to be done?"); control.addListener("keydown", this.__onWriterTextFieldKeydown, this); break; case "todos-scroll": control = new qx.ui.container.Scroll; control.add(this.getChildControl("todos-container")); break; case "todos-container": control = new qx.ui.container.Composite(new qx.ui.layout.VBox(1)); break; case "statusbar": control = new todos.StatusBar; control.bind("filter", this, "filter"); this.bind("todos", control, "todos"); control.addListener("removeCompleted", this.__onRemoveCompleted, this); break; } return control || this.base(arguments, id); }, __onWriterTextFieldKeydown : function(event) { var key = event.getKeyIdentifier(); switch(key) { case "Enter": var value = event.getTarget().getValue(); if (value) { event.getTarget().setValue(""); var todo = new todos.ToDo(value); this.getTodos().push(todo); todo.addListenerOnce("remove", this.__onTodoRemove, this); todo.addListener("completedChanged", this.__onTodoCompletedChanged, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); var cbAll = this.getChildControl("checkbox"); cbAll.removeListener("changeValue", this.__onCheckAllChanged, this); cbAll.setValue(false); cbAll.addListener("changeValue", this.__onCheckAllChanged, this); } break; case "Escape": event.getTarget().setValue(""); break; } }, __updateTodoList : function() { var toList; switch(this.getFilter()) { case "all": toList = this.getTodos(); break; case "active": toList = this.getTodos().filter(function(item){return !item.getCompleted();}); break; case "completed": toList = this.getTodos().filter(function(item){return item.getCompleted();}); break; } var container = this.getChildControl("todos-container"); container.removeAll(); toList.forEach(function(item){ container.add(item); }); }, __applyFilter : function() { this.__updateTodoList(); }, __onTodoRemove : function(event) { var todo = event.getTarget(); this.setTodos(this.getTodos().filter(function(item){return item !== todo;})); this.getChildControl("todos-container").remove(todo); todo.dispose(); this.getChildControl("statusbar").update(); }, __onTodoCompletedChanged : function() { var cbAll = this.getChildControl("checkbox"); cbAll.removeListener("changeValue", this.__onCheckAllChanged, this); cbAll.setValue(this.getTodos().length === this.getTodos().filter(function(item){return item.getCompleted();}).length); cbAll.addListener("changeValue", this.__onCheckAllChanged, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); }, __onCheckAllChanged : function(event) { var value = event.getData(); this.getTodos().forEach(function(todo){ todo.removeListener("completedChanged", this.__onTodoCompletedChanged, this); todo.setCompleted(value); todo.addListener("completedChanged", this.__onTodoCompletedChanged, this); }, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); }, __onRemoveCompleted : function() { var completed = this.getTodos().filter(function(item){return item.getCompleted();}); this.setTodos(this.getTodos().filter(function(item){return !item.getCompleted();})); completed.forEach(function(todo){ this.getChildControl("todos-container").remove(todo); todo.dispose(); }, this); this.getChildControl("statusbar").update(); this.getChildControl("checkbox").setValue(false); } } });
На этом этапе мы получили вполне себе функционально законченное приложение. Есть только один нюанс, оно страшно, как атомная война:
Попробуем привести его к пристойному виду. Оговорюсь сразу, дизайнер из меня, как из козла балерина, поэтому задача максимум для меня добиться, чтобы наш todo лист выглядел просто аккуратно, без изысков.
За внешний вид приложения в qooxdoo отвечают темы. Фреймворк поставляется с 4 темами. Темы можно расширять, переписывать и т.д. Тема в qooxdoo имеет 5 составляющих и определяется таким образом:
qx.Theme.define("todos.theme.Theme", { meta : { color : todos.theme.Color, decoration : todos.theme.Decoration, font : todos.theme.Font, icon : qx.theme.icon.Tango, appearance : todos.theme.Appearance } });
Подробнее про темы можно почитать тут.
Итак, сделаем следующие изменения:
Appearance.js
/** * * @asset(qx/icon/Tango/* */ qx.Theme.define("todos.theme.Appearance", { extend : qx.theme.simple.Appearance, appearances : { "todo-window" : { include : "window", alias : "window", style : function(){ return { contentPadding: 0 }; } }, "checkbox": { alias : "atom", style : function(states) { var icon; if (states.checked) { icon = "todos/checked.png"; } else if (states.undetermined) { icon = qx.theme.simple.Image.URLS["todos/undetermined.png"]; } else { icon = qx.theme.simple.Image.URLS["blank"]; } return { icon: icon, gap: 8, cursor: "pointer" } } }, "radiobutton": { style : function(states) { return { icon : null, font : states.checked ? "bold" : "default", textColor : states.checked ? "green" : "black", cursor: "pointer" } } }, "checkbox/icon" : { style : function(states) { return { decorator : "checkbox", width : 16, height : 16, backgroundColor : "white" } } }, "todo-window/checkbox" : "checkbox", "todo-window/textfield" : "textfield", "todo-window/todos-scroll" : "scrollarea", "todo-window/todo-writer" : { style : function() { return { padding : [2, 2, 0, 0] }; } }, "todo-window/statusbar" : { style : function() { return { padding : [ 2, 6], decorator : "statusbar", minHeight : 32, height : 32 }; } }, "todo-window/statusbar/info" : "label", "todo-window/statusbar/rb-filter-all" : "radiobutton", "todo-window/statusbar/rb-filter-active" : "radiobutton", "todo-window/statusbar/rb-filter-completed" : "radiobutton", "todo-window/statusbar/remove-completed-button" : { include : "button", alias : "button", style : function() { return { width : 150, allowGrowX : false }; } }, "todo/label" : { include : "label", alias : "label", style : function(states) { return { font : (states.completed ? "line-through" : "default"), textColor : (states.completed ? "light-gray" : "black"), cursor : "text" }; } }, "todo/icon" : { style : function() { return { cursor : "pointer" }; } }, "todo/text-container" : { style : function() { return { allowGrowY : false }; } }, "todo/checkbox" : "checkbox" } });
Color.js
qx.Theme.define("todos.theme.Color", { extend : qx.theme.simple.Color, colors : { "light-gray" : "#BBBBBB", "border-checkbox": "#B6B6B6" } });
Decoration.js
qx.Theme.define("todos.theme.Decoration", { extend : qx.theme.simple.Decoration, decorations : { "statusbar" : { style : { backgroundColor : "background", width: [2, 0, 0, 0], color : "window-border-inner" } }, "checkbox" : { decorator : [ qx.ui.decoration.MBorderRadius, qx.ui.decoration.MSingleBorder ], style : { radius : 3, width : 1, color : "border-checkbox" } } } });
Font.js
qx.Theme.define("todos.theme.Font", { extend : qx.theme.simple.Font, fonts : { "line-through" : { size : 13, family : ["arial", "sans-serif"], decoration : "line-through" } } });
После этого наш TODO лист будет выглядеть так:
На этом пока можно закончить. Я не затронул огромное количество вопросов, но это просто невозмозможно в рамках одной статьи. Хотелось познакомить с фреймворком на примере небольшой задачи, как можно меньше углубляясь в детали. Подробнее можно почитать по приведенным ссылкам. Обо всех ошибках и опечатках прошу писать в личку. Спасибо за внимание.
Полезные ссылки:
Домашняя страница qooxdoo: http://qooxdoo.org/
Страница загрузки SDK: http://qooxdoo.org/downloads
Разнообразные демо: http://qooxdoo.org/demos
Примеры использования: http://qooxdoo.org/community/real_life_examples
SPA туториал: http://manual.qooxdoo.org/current/pages/desktop/tutorials/tutorial-part-1.html
Код примера на гитхабе: https://github.com/VasisualyLokhankin/todolist_qooxdoo
ссылка на оригинал статьи http://habrahabr.ru/post/254767/
Добавить комментарий