Monday, February 20, 2012

Верочка

Вот у нее слова ложатся так гладко, складно, хочется читать и читать.

Короткий рассказ это как окно первого этажа вечером, проходишь мимо, видишь лишь мгновение, небольшую сценку... но в целом можешь представить ход сюжета. Но в сюжете главное интрига, даже когда ее нет, ищешь кнопку "show more". ...президент встречался с Уваровым - скука, нет интриги, как долго он с ним встречался, знает ли жена... Так много деталей упущено, хочется пролистать до места где "Уваров доставлен в следственные органы до выяснения...". Все в жизни завязано на интриге, даже сама жизнь. Вот идешь на свидание с девушкой, ты не знаешь чем все закончится. Что будет после того как ты возьмешь ее за руку, а если поцелуешь. Новости... Новости нужно рассматривать как интригу мира. Вот есть общество... Сирия например, и бац - у них переворот. Переворот тут как раз сценка за грязным окном квартиры. Прошли мимо, заметили движение за окном, и забыли. Окна притягивают. Один дом - десяток окон, в каждом своя сюжетная линия.

Подъездная дверь скрипнула, силуэт небольшого человека вышел из подъезда.
На улице душно, мужчина посмотрел на часы и повернул за угол дома, вечер медленно зажигает огни "сцен".
Зажегся свет на малой сцене, занавес поднят.
На сцене полумрак... Глазам не нужно привыкать к смене света, как в классическом театре - картинка появляется резко, и четко. Старые засаленные обои, силуэты обитателей сцены, по центру стены часы с кукушкой.

Кухня. Это определенно кухня. Сервиз рядом с настенными часами и небольшой столик с кучей хлама. Женщина, сгорбившись над жестяным тазом, что-то моет в грязно-мыльной воде. Мужчина проходя мимо нее останавливает свой взгляд на часах. Кукушка давно сломалась, но "интрига" не дает починить этот замысловатый механизм. Пусть ей и не суждено "выстрелить", но пусть весит. Душ совмещен с кухней, горелка силится нагреть огромный бак, разделяющий раковину кухни и ванную комнату. Ванная комната выполнена в классических тонах - темно синяя плитка, потрескавшаяся под глянцевым покрытием, и слоем не то плесени, не то ржавчины по периметру. Противоположная стена уходит в полумрак и слабо представляется что там.

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

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

Метро.

Wednesday, November 23, 2011

Организация кода на PHP

What-is-PHP

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

  • Каждый класс, контроллер, вьювер в отдельном файле. (на практике люди умудряются в один файл все приложение запихать на 5000 строк)
  • На один файл – одна точка входа. (люди же обычно обработку запросов строют через условия if-else, либо switch-case)
  • Хорошая система вывода сгенерированных на сервере данных. (передал классу данные, готовые к выводу и указал формат, вот идеальный вариант)

Использовать готовый фреймворк для решения небольшой задачи, это равносильно подключение jQuery для выборки по id. Что есть небольшая задача для меня: промо-сайт, api сервис, сайт с логикой на клиенте (js RIA), где PHP как прослойка между базой и приложением на JS.

Типичный файл index.php которые я встречал при работе с чужим кодом. Максимум логику одного кейса вынесут либо в отдельный файл через include, либо оформят в статический класс.

$action = @$_REQUEST['action'];
switch($action){
	case "list":
		$filter = @$_REQUEST['filter'];
		//...
		echo json_encode($result);
		break;
	case "get":
		$id = @$_REQUEST['id'];
		//...
		echo $result;
		break;
	case "add":
		$name = @$_REQUEST['name'];
		//...
		echo "ok";
		break;
	default:
		//404
		break;
}

Предлагаю элегантное решение проблемы в виде роутера запросов.

$route = @$_REQUEST['r'];
$format = @$_REQUEST['f'];

unset ($_REQUEST['r']);
unset ($_REQUEST['f']);

$view = new View();
$api = new Api($view);
$api->call($route, $_REQUEST, $format);

Класс View занимается только форматом вывода, т.е. представлением. Одни и те же данные мы можем вывести как JSON, HTML, Шаблон Smarty или еще как-то.

Вот примерная реализация на все случаи жизни.

class View implements IView
{
	public $modes = array(
		'html' => 'html',
		'text' => 'html',
		'tpl' => 'tpl',
		'js' => 'js',
		'json' => 'json',
		'result' => 'result',
		'debug' => 'debug'
	);

	public function html($result)
	{
		Header("content-type: text/html; charset=utf-8");
		echo $result;
	}

	public function tpl($result)
	{
		require('Smarty.class.php');
		$smarty = new Smarty();

		$smarty->setTemplateDir('smarty/templates');
		$smarty->setCompileDir('smarty/templates_c');
		$smarty->setCacheDir('smarty/cache');
		$smarty->setConfigDir('smarty/configs');

		$smarty->assign('item', $result['data']);
		Header("content-type: text/html; charset=utf-8");
		echo $smarty->fetch($result['tpl']);
	}

	public function debug($result)
	{
		Header("content-type: text/html; charset=utf-8");
		print_r($result);
	}

	public function json($result)
	{
		Header("content-type: application/json; charset=utf-8");
		return json_encode($result);
	}

	public function js($result)
	{
		Header("content-type: application/javascript; charset=utf-8");
		echo $result;
	}

	public function result($result)
	{
		$json = $result === true ? array('result' => true) : array('result' => false);
		return $this->json($json);
	}

	public function output($result, $mode, $callback = null)
	{
		if (isset($callback) && ($mode != 'json' || $mode != 'result')) {
			$mode = 'json';
		}
		$raw = '';
		if (array_key_exists($mode, $this->modes)) {
			$methodName = $this->modes[$mode];
			$raw = $this->$methodName($result, null);
		}
		if (isset($callback)){
			echo $callback . '(' . $raw . ')';
		}else if (isset($raw)){
			echo $raw;
		}
	}
}

Класс Api наследует App класс, в котором описана все логика. Сам же Api описывает только кейсы.

class Api extends App
{
	public $export = array(
		'list' => 'getList',
		'add' => 'addItem',
		'get' => 'getItem'
	);

	public function getList()
	{
		$filter = $this->arg('filter', '')
		$items = array();
		return $items;
	}

	public function addItem()
	{
		$name = $this->arg('name', 'default name')
		return true;
	}

	public function getItem()
	{
		$id = $this->arg('id', null)
		return array(
			'name' => 'Name',
			'url' => 'URL'
		);
	}
}

Класс App реализует всю внутреннюю логику работы с таблицей доступа

abstract class App
{
	public $view;
	public $args;
	public $export = array();

	public function __construct($view = null)
	{
		if (isset($view)) {
			$this->view = $view;
			if (!($this->view instanceof IView)) {
				throw new Exception('View must implement IView interface');
			}
		}
	}

	public function arg($name, $default)
	{
		return (isset($this->args[$name]) && !empty($this->args[$name]) ? urldecode($this->args[$name]) : $default);
	}

	public function call($route, $args, $format = null)
	{
		if (!isset($route)) throw new Exception('Route is not exists');

		$this->args = $args;
		$callback = $this->arg('jsoncallback', null);

		if (array_key_exists($route, $this->export)) {
			$methodName = $this->export[$route];
			$result = $this->$methodName();
			if (!isset($this->view)) {
				return $result;
			} else {
				if (!isset($format)) throw new Exception('Format is not exists');

				$this->view->output($result, $format, $callback);
			}
		}
	}
}

Api можно использовать и без представления, в случае, если у нас api используется не только как сервис.

$api = new Api();
$api->call('list', array('filter' = > '100'));

Примеры и сама реализация лежат на github

Thursday, September 29, 2011

Javascript State Machine v2

Совершенно случайно набрел на библиотеку под названием State Machine.

Это библиотека, реализующая событийную модель основанную на состояниях.

При реализации сложной логики событий эта библиотека сильно упрощает жизнь разработчика.

Для чего она и что умеет. Я думаю начну с примера, который хорошо показывает ее возможности.

var fsm = StateMachine.create({

    events: [
      { name: 'start', from: 'none',   to: 'green'  },
      { name: 'warn',  from: 'green',  to: 'yellow' },
      { name: 'panic', from: 'green',  to: 'red'    },
      { name: 'panic', from: 'yellow', to: 'red'    },
      { name: 'calm',  from: 'red',    to: 'yellow' },
      { name: 'clear', from: 'red',    to: 'green'  },
      { name: 'clear', from: 'yellow', to: 'green'  },
    ],

    callbacks: {
      onbeforestart: function(event, from, to) { log("STARTING UP"); },
      onstart:       function(event, from, to) { log("READY");       },

      onbeforewarn:  function(event, from, to) { log("START   EVENT: warn!",  true);  },
      onbeforepanic: function(event, from, to) { log("START   EVENT: panic!", true);  },
      onbeforecalm:  function(event, from, to) { log("START   EVENT: calm!",  true);  },
      onbeforeclear: function(event, from, to) { log("START   EVENT: clear!", true);  },

      onwarn:        function(event, from, to) { log("FINISH  EVENT: warn!");         },
      onpanic:       function(event, from, to) { log("FINISH  EVENT: panic!");        },
      oncalm:        function(event, from, to) { log("FINISH  EVENT: calm!");         },
      onclear:       function(event, from, to) { log("FINISH  EVENT: clear!");        },

      onleavegreen:  function(event, from, to) { log("LEAVE   STATE: green");  },
      onleaveyellow: function(event, from, to) { log("LEAVE   STATE: yellow"); },
      onleavered:    function(event, from, to) { log("LEAVE   STATE: red");    async(to); return false; },

      ongreen:       function(event, from, to) { log("ENTER   STATE: green");  },
      onyellow:      function(event, from, to) { log("ENTER   STATE: yellow"); },
      onred:         function(event, from, to) { log("ENTER   STATE: red");    },

      onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); }
    }
});

var async = function(to) {
    pending(to, 3);
    setTimeout(function() {
      pending(to, 2);
      setTimeout(function() {
        pending(to, 1);
        setTimeout(function() {
          fsm.transition(); // trigger deferred state transition
        }, 1000);
      }, 1000);
    }, 1000);
};

var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); };

fsm.start();

У нас есть состояние какого-то процесса, например ядерного синтеза, и датчики, сигнализирующие о текущем состоянии системы. Зеленый – все ОК. Желтый – предупреждение. Красный – тревога.

При переходе из одного состояния в другое система должна реагировать определенным образом на это. Например смена состояния с зеленого на желтый сопровождается генерацией событий onleavegreen, onyellow, onbeforewarn, onwarn, onchangestate, где мы соответствующим образом можем отреагировать на смену состояний процесса.

Также, в качестве примера возможностей библиотеки, функция async симулирует этапный переход из одного состояния процесса в другой. Метод transition является аналогом метода resolve у объекта $.Deferred() из jQuery. Он сообщяет StateMachine о готовности перехода из одного состояния в другое.

Как это все выглядит в динамике можно посмотреть тут.

Saturday, September 24, 2011

Cоздание прототипа социальной сети на ExtJS. Первые и не последние проблемы с ExtJS 4

Постоянно меняющиеся требования и сжатые сроки подтолкнули нас к использованию ExtJS 4 для создания прототипа.

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

В интернете много статей про ExtJS 4 уровня basic. Например очень порадовала эта статья http://css.dzone.com/articles/how-use-extjs-4-jquery. А вот серьезных “не кассовых” статей по решению (или созданию) каких-то проблем с помощью ExtJS не много.

Предлагаю вашему внимаю первую статью в рамках ExtJS patch lab… Ну и немного о самой соц. сети… совсем немного.

Для ленты постов решено было использовать Ext.panel.Grid с Ext.data.Store. Можно сказать это те фундаментальные вещи, ради которых и выбрали использование ExtJS.

Почему именно grid, а не dataView (в последствии конечное пришлось отказаться от грида в пользу вьюшки). У грида понравился визуальный режим сортировки, фильтрации данных, “бесконечный” скролл. Радость от предоставленных возможностей просто переполняла.

Ext.data.Store

Данные приходят от сервера с помощью API вызова, далее в сыром виде передаются в метод loadData класса Store. Использовать встроеный в Store прокси не стали из-за наличия у нас API как у Foursqure.

var myStore = Ext.create('Ext.data.Store', {
    model: 'Post',
filterOnLoad: true,
idProperty: 'id',
filters: new Ext.util.Filter({
filterFn: function (item) {
return item.get("status") > 0;
}
})
});

API.Events.get().done(function(posts){
myStore.loadData(posts);
});

Модель Post из себя представляет наследника класса Ext.data.Model.

Ext.define('Post', {
    extend: 'Ext.data.Model',
    idProperty: 'id',
constructor: function (raw) { return this.callParent([this.prepare(raw), raw[this.idProperty]]); },
prepare: function (raw) {


} });

В модели Post происходит подготовка сырых данных через метод prepare. В конструктор родительского класса данные приходят уже в обработанном виде.

Что происходит в методе prepare:
Например приведение даты поста из строки к объекту Date

this.data = new Date(this.data);
…получение строкового значения наименования поста из typeId через хэш таблицу соответствий
this.typeName = Dict.post.types[this.typeId];
И так далее...

Store хранит внутри себя данные в виде инстанцированных моделей, в методе loadData происходит проверка, если данные еще сырые, то передаем их в конструктор модели, и записываем ее у себя, и так с каждым элементом массива.

Приятная полезность #1

Таким образом loadData принимает как массив сырых объектов, так и уже инстанцированных моделей.

Особенность #1

При передачи в loadData единичного объекта произойдет ошибка, метод ожидает только массива, пусть даже из одного элемента.

Особенность #2

Объекты в javascript передаются по ссылке, поэтому переданные сырые данные в loadData станут массивом моделей.

API.Events.get().done(function(posts){
//posts is array of objects
myStore.loadData(posts);
//posts is array of Models
});

Если вам нужны исходные данные после загрузки в неизменном виде, используйте метод Ext.Array.clone(), но этот метод не поддерживает глубокого клонирования, как jQuery.extend.

Проблема #1

У метода loadData есть второй параметр append (append True to add the records to the existing records in the store, false to remove the old ones first). Т.е. от сервера приходят новые посты, мы их должны добавить к уже имеющимся. Для этого выставляем второй параметр true.

В нашем случае хранилище использует фильтрацию по загрузке (filterOnLoad = true)

Как работает фильтрация:

Store хранит все записи в двух коллекциях, основной – data, и в скрытой – snapshot. Ключ “filterOnLoad: true” в Store заставляет хранилище при загрузке данных любым способом проверять их на фильтрах и отбрасывать отфильтрованные в свое внутреннее хранилище snapshot.

Использование фильтров в Store и метод loadData несовместимы.

Листинг из файла ext-all-debug.js (Метод loadRecords используется loadData… и не только)

loadRecords: function(records, options) {

if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records);

}
Вот здесь и кроется проблема, options.addRecords в нашем случае true, удаление snapshot не происходит, данные попадают только в основную коллекцию.
API.Posts.get().done(function(posts){    
myStore.loadData(posts); //posts.length 8, filtered 2 entries
});

API.Posts.get().done(function(posts){
myStore.loadData(posts, true); //posts.length 10, filtered all entries
});

В этоге при первой загрузке прошли только 2 записи, а во втором все.

me.data содержит 2 + 10 записей, а me.snapshot только 8, т.к. данные из второго вызова не попали в скрытое хранилище.

Для того, чтобы пересортировать нашу коллекцию, нам нужно очистить старые фильтры и применить новые, нельзя просто “перечитать” фильтры (что очень и очень странно).

Метод mystore.clearFilter() убивает 10 последних записей, в итоге в хранилище у нас 8 записей.

Решение

loadRecords: function(records, options) {

if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records);
//add records in snapshot too.
if (options.addRecords && me.snapshot) {
me.snapshot.addAll(records)
}


}

Проблема #2

Ключ idProperty не используется по назначению.

Поле idProperty это аналог первичного ключа в БД. В нашем случае данные имеют уникальный ключ ID, по которому свежие посты должны добавляться в хранилище. В случае изменения какого-то поста, например добавления к нему комментария мы должны обновить эту запись в хранилище. Метод loadData в режиме append с выставленным ключом idProperty = "id" должен решить эту проблему. Но на самом деле этот ключ не используется для этого, и мы не можем сделать объединение данных автоматом.

Покопавшись в исходникам я обнаружил что такой функционал присутствует, но попросту не используется. Метод me.data.addAll(records) может принимать не только массивы, но и объекты ключ-значение. Нужно лишь слегка модифицировать тот же метод loadRecords чтобы использовать эту возможность.

Решение

loadRecords: function(records, options) {
… if (!options.addRecords) { delete me.snapshot; me.clearData(); } me.data.addAll(records); if (this.idProperty) { var indexRecord = {}; for (i = 0; i < length; i++) { indexRecord[records[i].data[this.idProperty]] = records[i]; } me.data.addAll(indexRecord); if (options.addRecords && me.snapshot) { me.snapshot.addAll(indexRecord); } records = []; for (key in indexRecord) { records.push(indexRecord[key]); } length = records.length; } else { me.data.addAll(records); if (options.addRecords && me.snapshot) { me.snapshot.addAll(records); } }
… }

После этим изменений в качестве ключа мы имеем значения id, и можем добавлять новые данные, они не будут дублироваться…

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

После этим изменений в качестве ключа мы имеем значения id, и можем добавлять новые данные, они не будут дублироваться…

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

На горизонте рассмотрение проблемы синхронизации Store с его представлением, и что делать если представлений несколько.

Wednesday, September 21, 2011

Новая версия плагина Observable 1.4

sticker

Скачать плагин можно здесь https://github.com/freeart/observable

Что нового/интересного?

Для начала краткая историческая справка, плагин создавался для упрощения привязки функций-обработчиков к представлению.

Первая версия плагина работа весьма просто.

Есть такая dom-структура

<div role="document" class="container">

<span class="run" data-fn="run" data-event="click">Run it!</span>
<span class="stop" data-fn="stop" data-event="click">Stop!</span>

</div>
… и бизнес-объект
var testWorker = { 
run: function(e){
//..run code
},
stop: function(e){
//..stop code
}
}

Плагин Observable ищет в ветке [role="document"] все декларации событий в виде data-параметров. data-fn = “имя функции-обработчика в указанном объекте testWorker” и data-event = “имя события – в данном примере click” – обязательные параметры. Также существуют необязательные параметры data-pref, data-scope, data-selector, data-type, но о них позже.

Это примерно эквивалентно

$('[role="document"]').delegate(".run", "click", function(e){
    //..run code
});
$('[role="document"]').delegate(".stop", "click", function(e){
//..stop code
});

Зачем же нужен этот плагин, если и так все работает замечательно!?

  • Заставляет использовать бизнес-объекты с публичными методами, которые хорошо организуют код.
    Например методы run/stop объекта testWorker создают более понятный код, чем в случае с анонимными обработчиками.
  • Отделяет логику от представления: дизайнер рисует, верстальщик верстает, программист привязывает обработчики к UI и описывает логику приложения.
    Посмотрите в сторону Objective-C, Flash, WPF.
  • Для привязки функций-обработчиков не используются атрибуты id или class – дизайн никак не пересекается с логикой приложения.
    Например имя класса .run вестальщик может изменить на что-то более ему понятное/удобное = .button, а программист получает нерабочий код. А декларация в виде data-атрибутов никому не мешает.
  • Упрощает код, на небольшом количестве обработчиков этого конечное не понять, а вот когда количество обработчиков превышает 20-30 шт. - код превращается в неуправляемую массу.
  • Не анонимные методы в качестве обработчиков заставляют писать заведомо правильный и масштабируемый код. Бизнес-объект testWorker имеет метод run, и может быть вызван не только по нажатие на кнопку “run it!”, но и по другому условию, например при загрузке страницы.
    $.ready(function(){
        testWorker.run()
    });
  • Функции-обработчики приходится “группировать” – получая в итоге привязку одной бизнес-единицы к представлению.
    $('[role="document"]').observable(testWorker);
    С точки зрения семантики эта строчка означает привязку бизнес-объекта testWorker к представлению document.

Начиная с версии Observable 1.3 стали поддерживаться конструкции маппинга функций-обработчиков вторым параметром. Он используется в случае когда у вас dom-структура еще не загружена.

$('[role="document"]').observable(testWorker, {
"data-pref": "run,stop",
"data-fn-run": "run",
"data-event-run": "click",
"data-fn-stop": "run",
"data-event-stop": "click"


});

Вот и дошли до этого атрибута data-pref. Т.к. data-параметры не поддерживают массивы, то для объединения data-fn с data-event используется такой синтаксис:

"data-pref": "run", 
"data-fn-run": "run",
"data-event-run": "click"

Синтаксис полностью совместимый с dom версией, но несколько неуклюж и громоздок.

Поэтому версия Observable 1.4 получила поддержку сокращенного синтаксиса:

$('[role="document"]').observable(testWorker, [
{
fn: "run",
event: "click"
},
{
fn: "stop",
event: "click"
}
]);

В случае если вы по каким-то соображениям не хотите в верстке использовать data-параметры, вы можете использовать поле selector, и по старинке привязать обработчик по селектору.

$('[role="document"]').observable(testWorker, [
{
fn: "run", event: "click",
selector: ".run" //эквивалентен $("[role="document"]").delegate(".run", "click", testWorker.run) }, {
fn: "stop", event: "click",
selector: ".stop" //эквивалентен $("[role="document"]").delegate(".stop", "click", testWorker.stop) } ]);

Остались два параметра data-scope и data-type. По умолчанию привязка обработчиков происходит через delegate, при желании вы можете указать bind. Но нужно понимать что bind можно привязать только если этот элемент присутствует в dom-дереве на момент.

Все функции-обработчики выполняются как обычные обработчики dom элементов в контексте dom-элемента, т.е. this == элемент, который сгенерировал событие, в наших примерах это span dom элементы .run и .stop . Если вы хотите использовать свой контекст, например того бизнес объекта, который вы передаете в метод observable, т.е. testWorker, в этом случае указываем data-scope = "this", или любой другой в рамках testWorker, т.к. ссылку на имя контекста scope ищется от нашего ROOT объекта testWorker.

$('[role="document"]').observable(testWorker, [
{    
fn: "run", event: "click",
scope: "this"
}, {
fn: "stop", event: "click",
scope: "this" } ]);

Методы run и stop в этом случае вызывается таким образом:

fn.call(testWorker, event, thisElement, testWorker);

Что почти эквивалентно такому приему:

$('[role="document"]').gelegate("click", ".run", function(e){
    //..run code
}.bind(testWorker));
var testWorker = {
run: function(e, element, obj){
//..run code
// для справки: this === element, obj === testWorker
console.log(this);
},
stop: function(e, element, obj){
//..stop code
console.log(this);
}
}
$('[role="document"]').observable(testWorker);

Sunday, July 17, 2011

Ленивая инициализация событий на jQuery

Rich приложения, подобные google+, gmail, facebook имеют множество динамически создаваемых элементов управления. Следить за привязкой событий при каждом переходе между интерфейсами может превратиться в ад. Выделять менеджер собитий в отдельный модуль тоже не целесообразно – придется поддерживать список событий в актуальности и со временем этот менеджер стан просто помойкой событий.

<div data-fn="test">Test click</div>

Предлагаю простой вариант ленивой призявки событий к шаблону/интерфейсу одной строчкой:

$.observable({
    test: function(){
        alert($(this).text()); //Test click
    }
});

data аттрибут используется для описания привязываемого события. data-fn – имя метода в объекте events, data-event – тип события, data-type – вариант привязки события, через bind или delegate.

var MainInterface = function() {
    this.dom = $('<div></div>');

    this.dom.append(
        '<div data-fn="say" data-event="click" data-type="delegate" data-word="hello">say hello</div>'
    );
    this.dom.append(
        '<div data-fn="say" data-event="click" data-type="delegate" data-word="world">say world</div>
    ');

    this.dom.observable(this);

    this.say = function () {
        alert($(this).data('word'));
    }
}

var mainInterface = new MainInterface();
mainInterface.dom.appendTo($(document.body));

Какие плюсы мы получаем: Программист пишет чистый код отдельно от верстки, не заботясь об именах классов элементов. Верстка и логика не пересекаются, меняя как угодно дизайн, названия классов, название id элементов мы даем волю дизайнерам и не выясняем почему очередная кнопка отвалилась после изменения дизайна. В качестве шаблонизаторов можно использовать любой движок, любите haml? пожалуйста - добавте data аттрибуты и все события привяжутся автоматом после рендера.

Скачать плагин можно здесь https://github.com/freeart/observable

Friday, June 17, 2011

Генерация 100% уникальных кодов

В наш век рекламы и продвижения товаров я думаю каждая рекламная компания хоть раз сталкивалась с задачей генерации уникальных кодов в огромных размерах… ну например 50 миллионов уникальных кодов состоящих из цифр и латинского алфавита без учета регистра. Решений в интернете, так скажем, не много… программы платные, причем сомнительного качества – судя по отзывам или по внешнему виду сайта, и ручаться за 100% уникальность не видя исходников я бы не стал. А проверить уникальность постфактум не представляется возможным. А уж какой может получиться скандал, если во время проведения акции будут дубли… никому это не надо.

Решения которые предлагаются на различных форумах тоже оставляют желать лучшего. Как например совет использовать базу данных для проверки на уникальность с помощью индекса на тестовом поле с флажком – уникальный. Вот попробывали бы они использовать такой метод при генерации 50 милионов записей, я бы посмотрел сколько лет они будут ждать результата. Также присутствовали “обнадеживающие” ответы типа “вот тебе функция генерации GUID, он длинный, так что очень малый шанс что будут дубли” не радуют. Нам не нужно длинный, и не нужен GUID, и уж точно нужен уникальный.

Хотел бы предложить мой вариант генерации кодов, который может гарантировать хорошую скорость генерации (500.000 уникальных кодов за меньше чем 2 секунды), которая не будет зависить от количества уже сгенерированных элементов с одной стороны, а с другой неоспоримую уникальность сгенерированных кодов. Огромную скорость мы получаем из-за отказа от поиска на предмет дублей. Искать дубли среди десятков миллионов кодов не простая штука я вам скажу. Тут без бинарного дерева не обойтись. Казалось бы – вот оно решение – поднимаем в памяти бинарное дерево, и генерируем себе коды – 100% уникальность. Правда есть одна проблема... оперативная память, которая не резиновая. При организации бинарного дерева придется хранить ссылку на вышестоящий символ, плюс к этому добавляет уровень вложенности – в нашем случае это номер символа в строке. А это все избытки, которые на больших объемах данных вырастают в гигабайты.

Ну я думаю хватит информации для затравки, приступим к делу:

Версия на Perl

#!/usr/bin/perl -w
$dict = '1234567890qwertyuipasdfghjklzxcvbnm';
$totalLength = 500000;
$codeLength = 12;
$dictLength = length($dict) - 1;

%codes = ();
while (keys(%codes) < $totalLength){
  $res = '';
  for ($i = 0; $i < $codeLength; $i++){
    $res .= substr($dict,rand($dictLength), 1);
  }
  $codes{$res} = undef;
}

Версия на PHP

$dict = '1234567890qwertyuipasdfghjklzxcvbnm';
$totalLength = 500000;
$codeLength = 12;
$dictLength = strlen($dict) - 1;

$codes   = array();
while (count($codes) <$totalLength){
  $res = '';
  for ($n = 0; $n < $codeLength; $n++){
    $res .= $dict[mt_rand(0, $dictLength)];
  }
  $codes[$res] = null;
}

Ну вот собственно и все, хэш codes наполнен 500000 элементами, код выполняется ~ 1 секунды на core i5.

Ключевые моменты: $dict – это словарь доступных символов, $totalLength – это кол-во генерируемых кодов, $codeLength – это длина кода.
While используется вместо for потому что при генерации могут возникать дубли, а нам же важно не кол-во проходов, а количество конечных элементов.

По желанию можно добавить различные условия и проверки, например вхождение одного символа не более 3-х раз в коде, или запретить подряд идущие символы и тд. При генерации более 20 милионов кодов рекомендую запускать на perl x64 версии, и с 4 гигабайтами на борту, т.к. из-за свопа генерация может растянуться на сутки.

Sunday, June 12, 2011

Javascript. Модульное проектирование RIA проектов.

“Работая над проблемой,
я никогда не думаю о красоте.
Я думаю только о решении проблемы.
Но если полученное решение некрасиво,
я знаю, что оно неверно.”
Richard Buckminster Fuller

Проблемы при разработке больших проектов

ninja2

Отделение котлет данных от представления, проектирование, велосипеды… главное без фанатизма. Какая проблема часто встречается при разработке RIA приложений. Команда сильных программистов «влюбляется» в новый проект, и с мыслями «сейчас мы сделаем нечто — главное все сделать самим, чтобы ни один чужой баг не закрался» начинают отказываться от готовых фреймворков, библиотек, решений.
Правда существует одно логичное объяснение этой тенденции, боязнь завязаться на чужой продукт, бывает такое, что в ходе разработки выясняется — такой интерфейс будет очень сложно разработать на доступном инструментарии, и рождаются костыли, правки чужих фреймворков и тд. Хотел бы предложить решение этой проблемы. Создание менеджера интерфейсов (модулей). Один модуль может использовать ExtJS, другой dhtmlx, а может и вообще чистый Javascript

Модульность как лекарство от многих болезней

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

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

Не чужое – а открытое

Какие плюсы использования готовых решений? Проще найти нового человека на проект, когда вы используете известный сторонний продукт. В ситуации когда вы остались единственный из создателей фреймворка и решаетесь уйти, то скорее всего новая команда перепишет все с нуля. Плюс правка багов и добавление новых фич пока вы работаете над бизнес логикой. Хотите мощное оружие в разделении данных от представления – используйте ExtJS например, зачем писать свой фреймворк!? Нужно динамически подгружать зависимости – посмотрите в сторону YepNope. Работу с dom предоставьте jQuery или любому другому известному фреймворку. Сконцентрируйтесь на результате. Я понимаю что большинство программистов любит писать все с нуля – но это не от того что он хочет учится, развиваться… это от лени… да-да, именно от лени. Изучение чужого кода, чтение документации куда сложнее чем разработка всего “с нуля”. Когда пишешь все “с нуля” многому не научишься, люди учатся в основном от других, исследуя чужую работу перенимаешь опыт (это как съесть печень своего противника), а в совокупности со своим опытом придумываешь что-то новое… более совершенное. Создавая стабильные приложения за короткие сроки вы получите больше признания чем закапываясь в правках очередного своего мега-фреймворка после выхода нового браузера IE6, IE7, IE8, IE9.
Если у вас цель написать свой фреймворк, вместо разработки ERP на которую вам дали срок 2 месяца – то начните с ОС… или с архитектуры процессора… а почему нет!?

Ядро – всему голова

Ядро приложения – это менеджер частей системы. Топ менеджер комнании, который выполняет функции супервизора. Он контролирует доступ к ресурсам и общение между объектами компании. Но никак не контроль загрузки товара на склад, выдача провианта и тд. Этим пусть занимаются утилиты и библиотеки, которым будут делегироваться задания.

В обязанности ядра входят:

  • Делегация загрузки модулей и необходимых зависимостей отделу библиотеке (в нашем случае я выбрал YepNope).
  • Делегация создания канвы для интерфейса модуля.
  • Передача информации между модулями.
  • Оповещение о каких либо событиях системы (закрытие приложения, активация нового модуля, изменения размера окна, потеря коннекта с сервером и тд.).
  • Выполнение запросов модулей (например загрузка другого модуля)

Полный код проекта смотрите на GitHub

Упрощенный вариант ядра:

/*=== core.js ===*/
!function () {

    var listOfLibraries = {};
    
    $(window).resize(function() {
        $.each(listOfLibraries, function(){
            this.body.onResize($(window).width(), $(window).height());
        });
    });

    $("script[id=core]").bind('request', function (data) {
        switch (data.header) {
            case "attach":
                var library = {
                    name: data.body.name,
                    body: new data.body.module()
                };
                listOfLibraries[library.name] = library;
                $('<div id="'+library.name+'"></div>').appendTo('body');
                var moduleContext = $('#'+library.name);
                library.body.main(moduleContext);
                break;
            }
    });
}();

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

Даешь независимость модулям!

Пример модуля:

/*=== HelloWorld.js ===*/
!function () { 

    var Lib = function () {
        var context;
        this.main = function (moduleContext) { 
            context = moduleContext;
            context.html('Hello World');
        }
        this.onResize = function(width, height){
            context.width(width);
            context.height(height);
        }
    }

    $("script[id=core]").trigger({  
        type: "request",                
        header: "attach",
        body: {
            module: Lib, 
            name: "HelloWorld"
        }
    });
} ();

Как видно из примера ядро общается с внешними компонентами по средствам событий, вернее одного события “request” – пула сообщений.
Тело сообщения состоит из заголовка (header) и тела (body). В данном случае модуль сообщает ядру что он хочет зарегистрироваться в системе под именен “HelloWorld” и сообщает ссылку на тело модуля.

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

Зачем же нужно тогда ядро, если мы могли легко в модуле создать контекст и работать с ним!?

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

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

Все изменения касаются лишь добавления новых событий. Для унификации интерфейса для общения ядра с модулями лучше выделить абстрактный класс с базовой реализацией рутины и все Lib классы модулей наследовать от него.

window.Module = function () { 
    this.context;  
}
window.Module.prototype = {
    main: function (context) {
        this.context = context;
    },
    onResize: function (width, height) {

    }
}

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

Полный код проекта смотрите на GitHub

Friday, November 5, 2010

Рецепт быстрого приготовления расширений под популярные браузеры.

0147 (1)

Возникла задача написание простенького расширения под все (по возможности) популярные браузеры. Деятельность расширения заключается во внедрении javascript`а в тело документа.

Доктор прописал инъекции javascript`а

Казалось бы все просто, выполняем в строке браузера код:

javascript:var s = document.createElement('script');
s.type='text/javascript';
document.body.appendChild(s);
s.src='http://our_site/script.js';
void(0);
Но что делать если ваш скрипт должен обрабатывать все страницы чужого сайта. Не заставлять же пользователя постоянно click`ать по закладке с кодом инъекции после каждого перехода на новую страницу. Наивные поиски простого решения не увенчались успехом:
  • Это был перехват всех ссылок на страничке и добавление своего кода “javascript:”, разумеется код выполнялся в рамках открытой страницы и потом переходил на новую “чистую” страницу через (location.href).
  • Вариант с setTimeout был вообще в порядке бреда… выполение отваливалось после начала загрузки новой страницы.
Написание расширений под множество браузеров меня пугала больше всего, но что делать, начал вспоминать наиболее популярные:
  • IE 7+ (на 6-ку решил просто забить, много затрат ради небольшой аудитории пользователей – уж простите меня)
  • Firefox 1.5+
  • Chrome 4+
  • Opera 9+ (в итоге оказалось extension`ы можно писать только под Opera 11)
  • Safari 3+

“С Firefox и Chrome проблем не должно возникнуть” – подумал я.

Google Chrome


За час был написал extension для Chrome – масса документации на официальном сайте, масса примеров. В общем рай для разработчика.

Примеры и Руководство

фрагмент из background.html

function onRequest(tabId, changeInfo, tab) {
if (changeInfo.status == 'complete'){
chrome.tabs.executeScript(tabId, { code:"код без javascript:" });
};
};
chrome.tabs.onUpdated.addListener(onRequest);

Все получилось просто и со вкусом. Напрямую вставить в тело документа <script> не получилось, гугл ругался на чужой домен в адресе скрипта, разбираться было не когда, и executeScript сильно упростил реализацию.

Расширение собирается средствами Google Chrome:

image

”Как все здорово пошло” – подумал я. “Такими темпами я завтра для всех браузеров напишу”…

Mozilla Firefox


Хорошо обстоят дела с документацией, с примерами правда похуже чем у Chrome, но есть даже online builder addon`ов (делает пустышку zip, который необходимо “собрать” в xpi добавлением своих исходников).

Правда вначале смутил very basic extension на главной который не работал. Который через пару дней убрали со страницы слава богу.

Документация, Addon Builder

Синтаксис тоже вполне логичный и стандартный, фрагмент из chrome/content/js/main.js

var test = { 
onload: function(aEvent){
var doc = aEvent.originalTarget;
if (doc instanceof HTMLDocument && doc.location.href != "about:blank" && !doc.defaultView.frameElement) {
var s = doc.createElement('script');
s.type = 'text/javascript';
doc.body.appendChild(s);
s.src = 'http://our_site/script.js';
}
}
}
window.addEventListener("load", function(){
var appcontent = document.getElementById("appcontent"); if (appcontent) {
appcontent.addEventListener("DOMContentLoaded", test.onload, true);
}
}, false);

Safari


Upd: Спасибо Tails за информацию.

Документация и примеры
Подробная пошаговая инструкция

Главное нужно получить Сертефикат разработчика Safari, для этого нужно получить AppleID и вступить в группу разработчиков на apple.com

фрагмент из inject.js

var s = document.createElement('script'); 
s.type = 'text/javascript'; 
document.body.appendChild(s); 
s.src = 'script.js';

Microsoft Internet Explorer


Вот тут конечное я дня 2 убил на изучение спецификации по COM объектам и поиска рабочих примеров addon`ов. В итоге нашел на одном китайском сайте рабочий пример с привязкой к событиям браузера.

Привязываемся к событию DISPID_DOCUMENTCOMPLETE, и в случае возникновения выполняем код:

VARIANT vFlags = { 0 }; 
VariantInit(&vFlags);
vFlags.vt = VT_I4;
vFlags.intVal |= navNoReadFromCache;
BSTR testUrl = SysAllocString(L"javascript: код инъекции");
mWebBrowser2->Navigate(testUrl, &vFlags, NULL, NULL, NULL);

Но радость была бы не полной без такого замечательного бага. У IE проблема с обновлением по F5 и событием DocumentComplete.

Оно просто не вызывается при обновлении странички через F5

Для решения проблемы с F5, вешаем hook и по нажатию выполняем код:

CComBSTR url; 
mWebBrowser2->get_LocationURL(&url);
VARIANT v;
v.vt=VT_I4;
v.lVal= (navNoHistory);
mWebBrowser2->Navigate(url, &v, NULL, NULL, NULL);

Который вызовет DISPID_DOCUMENTCOMPLETE “искусственным” путем.

Opera


Тут оказалось все очень просто и красиво, правда есть баги в работе addon`ов, но Opera 11 все таки еще alpha.

Статьи с примерами

фрагмент из includes/base.js

window.addEventListener('load', function(event) { 
var currentDocument = (window.document) ? window.document : false;
opera.extension.onmessage = function(event) {
if (currentDocument) {
var s = currentDocument.createElement('script');
s.type='text/javascript';
currentDocument.body.appendChild(s);
s.src='http://our_site/script.js';
}
};
}, false);

фрагмент из background.js

window.addEventListener("load", function() {
function myEvent() {
var tab = opera.extension.tabs.getFocused();
if (tab) {
tab.postMessage('go');
}
}
opera.extension.onconnect = myEvent;
}, false);

Из обнаруженных мною проблем:

  • Из javascript`а делаем back history и событие onconnect не срабатывает.
  • Из функции в onmessage нельзя обратиться к window.document, может оно конечное так и задумано, но не логично.

На мой взгляд ребята из opera хорошо постарались над api.

Под Opera младше 11 версии возможна установка UserJS через свой installer (админских прав не нужно), установка происходит через файл opera.ini который расположен в профиле пользователя, раздел [User Prefs] > «User Javascript File»

Примеры скриптов И еще

Фрагмент из userjs.js

addEventListener('load', function(e){ 
	if (!document.body) { 
		return; 
	} 
	var s = document.createElement('script'); 
	s.type = 'text/javascript'; 
	document.body.appendChild(s); 
	s.src = 'userjs.js'; 
}, false);

Wednesday, September 1, 2010

jQuery плагин визуального стека сообщений

Стояла задача динамического визуального отображения реакции на действия пользователя со стороны системы.
Другими словами: изменилось какое-то состояние объекта - вывести сообщения об удачном завершении сохранения изменения в базе/системе.

Untitled-1

Пример использования, который показывает функционал плагина:

var msgObj = $.notify.add("Начало сохранения", 3); 
//Выводим сообщение о начале действий (продолжительностью 3 сек) и сохраняем ссылку на сообщение в msgObj
$.get('/test.php', function (result) {
if (result.ok) { //если все выполнилось удачно
$.notify.remove(msgObj, 0.5);
//удаляем предыдущее сообщение через 0,5 сек, вдруг все сохранилось быстрее чем мы думали
$.notify.add("Сохранение выполнено успешно", 1);
//и добавляем новое сообщение в стек
} else { //если произошла ошибка
$.notify.remove(msgObj, 0.5);
//удаляем предыдущее сообщение через 0,5 сек, вдруг все сломалось быстрее чем мы ожидали
$.notify.add("Ошибка сохранения", "error", 3);
//выводим сообщение "необычного" оформления на 3 сек
}
});

Скачать jquery.notify.js