Янв 11

Хороший доклад – Прокрустовы окна. Как вписаться в устройства с минимальными потерями (ссылка на видео – http://mediadl.microsoft.com/mediadl/www/r/rus/html5camp2011/4005_800_2.wmv, слайды на http://pepelsbey.net/pres/procrustes/).
Главное – это понять viewport и dpi.
Для примера:

<meta name="viewport"
content="width=device-width, target-densitydpi=device-dp, user-scalable=no, initial-scale=1.0, maximum-scale=1.0i" />

Советуют удобные сервисы для тестирования мобильных сайтов («firebug для мобильных устройств») – Opera Mini Simulator и Opera Mobile Emulator for desktop.

Теги:
Дек 26

Цель – стараться сделать код как можно более вертикально компактным, но читаемым.
Continue reading »

Теги:
Ноя 25

Для борьбы со спам-фильтрами gmail и яндекса, и чтобы доказать, что не верблюд, можно заморочаться и добавить дополнительный заголовок к письму и дополнительную запись в dns.
Называется это всё DomainKeys Identified Mail.
Очень понятно написано здесь: http://habrahabr.ru/blogs/spam/106589/, генерация ключей: http://www.port25.com/support/support_dkwz.php

Теги:
Ноя 25

Использовали мы на проекте одну лебедевскую библиотеку – include.js. Умеет она интересные вещи, вроде последовательной загрузки зависимых библиотек в нужном порядке. Нужно в яваскрипте написать js.include( ‘my/coolwiidget’ );. Работает она, если на пальцах, следующим образом: делается ajax-запрос, которым грузится текст нужного скрипта, потом ищутся в тексте все js.include, делается их подгрузка, после чего делается eval полученных текстов в обратном порядке. При этом ещё и отсекаются попытки по несколько раз грузить один и тот же яваскрипт.
Работал он очень неплохо, хотя и неудобно было в firebug-е искать скрипт по его имени.
Основная проблема встала, когда стало нужно вынести все яваскрипты на отдельный домен. Вот тут-то и проявились проблемы – ajax-ом нельзя грузить код с других доменов. Помогает заголовок Access-Control-Allow-Origin, но его понимают только современные браузеры (к примеру, firefox с версии 3.5), а на проекте критична максимально возможная совместимость (как обычно и бывает). Так что этот способ может быть актуален года через 3-5.
Посмотрели на другие способы, в итоге пришли к динамическому добавлению тега script в head, но особого смысла в этом нет, т.к. зависимости, для которых и городился изначально огород, при этом способе не отслеживаются (JSONP с колбэками не подошёл, т.к. в проекте много legacy-кода, который использует глобальные переменные, причём даже не как константы).
В итоге было принято гениальное в своей простоте решение – отказаться от всех этих изысков нафик и грузить скрипты как раньше – при загрузке страницы.

Ноя 16

Выбирал между backbone (habrahabr.ru/blogs/javascript/118782/) и knockout (http://habrahabr.ru/blogs/javascript/121926/).
Для выбора полезно почитать stackoverflow.com/questions/5112899/knockout-js-vs-backbone-js-vs.
В итоге выбрал knockout – менее монструозный и лично мне показался понятнее, к тому же не навязывает REST и не требует дополнительных библиотек.
Он реализует паттерн MVVM, который, думаю, больше подходит для клиентской части web-приложения, чем MVC.
На сайте есть подробные доки и очень удобный интерактивный туториал.
Пока в проектах не использовал, так что писать особо нечего(

Теги:
Ноя 16

Kyoto Tycoon в общем-то аналог MemcacheDB, но некоторые утверждают, что Kyoto Tycoon лучше.
Думаем использовать.
Там всё просто (через API Memcached):

	$kyoto = new Memcached();// для persistent: $kyoto = new Memcached( 'heyhey' );
	$kyoto->addServer( '127.0.0.1', '22122' );// kyoto
//	$kyoto->addServer( '127.0.0.1', '11211' );// memcached

	$kyoto->set( 'my_key', array( 'key1' => array( 1 ), 'key2' => null ) );
	$kyoto->get( 'my_key' );

Здесь про более старую версию (связка Tokyo cabinet и tokyo tyrant).
http://sameerparwani.com/posts/tokyo-tyrant-with-php, http://sameerparwani.com/posts/installing-tokyo-cabinet-and-tokyo-tyrant

Теги:
Ноя 11

Сегодня из-за очередных проблем с sol.adbureau.net было решено реализовать ленивую загрузку баннеров.
Начальное решение было использовать iframe, но идея была не очень удачной, т. к. могут быть проблемы с подсчётом кликов, да и модные картинки, увеличивающиеся при наведении, будут вести себя странно. А как этот iframe смотрится в разных браузерах – это вообще сказка)
В итоге родилась идея: при генерации страницы размещать в нужных местах, к примеру, пустые дивы с id с нужным префиксом (<div id=»js-advert-place-{num}»></div>), а сами баннеры загружать в футер вот в такую, к примеру, разметку:

<div id="js-all-advert-block" class="all-advert-block">
	<div class="js-advert-block-content" data-number="1">
		{баннер}
	</div>
	<div class="js-advert-block-content" data-number="5">
		{баннер}
	</div>
	<div class="js-advert-block-content" data-number="15">
		{баннер}
	</div>
</div>
{здесь грузится счётчик показов}

После загрузки страницы размещаем все баннеры на нужных местах:

	(function( $ ) {
		function moveAdvert() {
			$( '#js-all-advert-block' ).find( '.js-advert-block-content' ).each( function() {
				var $oldContent = $( this );

			// в ие перемещаем
				if ( $.browser.msie ) {
					$( "#js-advert-place-" + $oldContent.data( "number" ) ).append( $oldContent );
				}
			// в других браузерах убираем лишнее и вставляем заново
				else
				{
					var cleanedContent = $oldContent.html().replace( /document\.write/gi, 'function a__(){}' );

					$( "#js-advert-place-" + $oldContent.data( "number" ) ).html( cleanedContent );
					$oldContent.remove();
				}

			// этот вариант менее хороший - глюки в ие и проблемы с яндекс.директ

			//	var $newContent = $.browser.msie ? $( $oldContent.html() ) : $oldContent.clone( true );

			//// не надо заново создавать скрипты - они заново выполнятся, а там document.write
			//	$newContent.find( "script, link" ).each( function() {
			//		$( this ).remove();
			//	} );

			//// вставляем
			//	$( "#js-advert-place-" + $oldContent.data( "number" ) ).html( $newContent );

			//// на старом месте нужно убить всё, кроме скриптов
			//// (в скриптах могут быть необходимые для правильного подсчёта кликов переменные)
			//	$oldContent.find( '*' ).each( function() {
			//		var $el = $( this );
			//		if ( ! $( 'script, link', $el ).length && this.tagName.toLowerCase() != 'script' && this.tagName.toLowerCase() != 'link' ) {
			//			$el.remove();
			//		}
			//	} );
			} );
		}

		$( document ).ready( function() {
			moveAdvert();
		} );
	}( jQuery ));

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

UP:
Всё-таки проблем оказалась куча. К примеру, opera 10.10 (хотя у меня 11.52) некорректно понимает тег script при .html( cleanedContent ), поэтому приходится чистить все теги script, но тогда умирает yandex.direct. К сожалению, на проекте нет нормальной возможности отделить директ от остальной рекламы (нет возможности объяснить тем людям, как что нужно настраивать), поэтому этот способ подходит с большими оговорками.
UP2: Наткнулся на гениальное решение – переопределить document.write, потом немного доработал и вот что получилось:

(function ( $ )
{
	// для собирания тега script
	var previousValue = '',
	// для проверки, собрали ли уже полный тег script
		reTestIfScript = new RegExp( '<\/scr' + 'ipt>$', 'i' ),
	// чтобы найти id дива, куда вставлять код соли
		reGetSolId = new RegExp( "sol\\.adbureau\\.net[^>]+aamsz=(\\d+x\\d+)", 'i' ),
	// для проверки, что это относится к яндекс-директу
		reYandexDirect = new RegExp( "an\\.yandex\\.ru", 'i' );

// заменяем стандартные методы
	document.writeln = document.write = function(value)
	{
	// если последовательно пишут в документ для получения тега script (document.write( '<SCR' ); document.write( '<IPT src="...' );)
		try {
			$( value );
		}
		catch ( e )
		{
			var joinedValue = previousValue + value;
		// сли собрали полный тег скрипт, то вставляем его, куда надо
			if ( reTestIfScript.test( joinedValue ) )
			{
				value = joinedValue;
				previousValue = '';
			}
		// если начали писать тег скрипт, продолжаем до сбора всего тега
			else if ( joinedValue.toLowerCase().indexOf( '<sc' ) == 0 )
			{
				previousValue = joinedValue;
				return;
			}
		// непонятно что это - ничего не делаем
			else {
				return;
			}
		}

		var parentNodeId,
			matches;
	// если в строке есть упоминание яндекса - вставляем в яндекс
		if ( reYandexDirect.test( value ) ) {
			parentNodeId = 'adv_media';
		}
	// если в строке есть упоминание соли - ищем id и вставляем в нужный div
		else if ( matches = value.match( reGetSolId ) ) {
			parentNodeId = 'sol' + matches[1];
		}

	// скрипт вставляем только в нужный элемент
		if ( $( value )[0].tagName == "SCRIPT" && parentNodeId )
		{
			var js = document.createElement( 'SCRIPT' );
			var obj = document.getElementById( parentNodeId ).appendChild( js );

		// подгрузка внешнего скрипта
			if ( $( value ).attr( 'src' ) !== "undefined" ) {
				$( obj ).attr( {type: 'text/javascript', 'src': $( value ).attr( "src" )} );
			}
		// выполнение скрипта
			else
			{
				var reReplaceWrite = new RegExp( '(document\\.write[a-zA-Z]{0,2}\\([^)]+)(\\))', 'ig' );
				eval( $( value ).text().replace( reReplaceWrite, "$`$1, '" + parentNodeId + "'$2$'" ) );
			}
		}
	// стили ставим в head
		else if ( $( value )[0].tagName == "LINK" )
		{
			var js = document.createElement( 'LINK' );
			var obj = document.getElementsByTagName( 'head' )[0].appendChild( js );
			$( obj ).attr( {rel: 'stylesheet', 'href': $( value ).attr( "href" ) , type: 'text/css'} );
		}
		else if ( $( value )[0].tagName == "STYLE" )
		{
			if ( $.browser.msie ) {
				var css = document.createElement( 'STYLE' );
				document.documentElement.firstChild.appendChild( css );
				$( obj ).attr( {type: "text/css"} );
				css.styleSheet.cssText = $( value ).html();
			}
			else {
				$( value ).appendTo( $( "head" ) );
			}
		}
	// любой другой элемент вставляем в DOM, только если поняли, в какой элемент вставлять
		else if (  parentNodeId ) {
			$( value ).appendTo( $( "#" + parentNodeId ) );
		}
	};
}( jQuery ));

Всё бы ничего с этим решением, и даже знает, как собирать тег script из кусочков, но всё-таки мы не можем знать, в какое место DOM должна была производится запись, так что там есть костыли с определением места по контенту, и это решение не универсально.

Окт 27

Имеем флеш-плеер.
На некоторых страницах при нажатии на кнопку «плэй» в плеере происходит перезагрузка flash-плеера.
После долгих и мучительных разбирательств выяснилось следующее:
Flash-player при смене статуса (play/pause) меняет фавикон на сайте следующим кодом:

	$("link[rel='shortcut icon']").remove();
	$("head").append("<link rel='shortcut icon' href='" + link + "' />");

Меняет на всех страницах, а перезагрузка плеера происходит только на некоторых, и только в фф.
На этих страницах перед плеером стоял элемент <i class=»clearfix»></i> со следующим стилем:

i.clearfix:after {
    clear: both;
    content: ".";
    display: block;
    height: 0;
    visibility: hidden;
}

Важно здесь display: block;.
Причём стоит он не непосредственно перед плеером, а просто где-то выше по DOM-дереву.
При смене фавикона этот элемент, судя по всему, заставляет фаерфокс перерисовывать обкладывающий для плеера элемент, что приводит к рестарту флэш-плеера.
Firefox 7.0.1-7.0.2.
Будьте бдительны!)

Теги:
Окт 18

Работал раньше сайт с memcache.
Решили перейти на memcached.
И повалились ошибки SERVER HAS FAILED AND IS DISABLED UNTIL TIMED RETRY, причём сначала одна CLIENT ERROR, а уже потом куча SERVER HAS FAILED AND IS DISABLED UNTIL TIMED RETRY.
Гугление ничего не дало, а проблема оказалась в следующем: первая ошибка возникала из-за некорректного ключа (пробел в названии ключа), после чего сервер расстраивался, и больше ничего делать не давал.
Ключ был следующий: info__rating DESC__1, генерился автоматически и никаких проблем при использовании его в memcache не вызывал. Понятное дело, что ключ плохой, но это не мешало другому расширению успешно работать.
А решение применили банальное: $Key = str_replace( ‘ ‘, ‘_’, $Key );, теперь вроде работает).
Окончательно все проблемы решила опция

$this->setOption( Memcached::OPT_BINARY_PROTOCOL,   true );

После этого лог ошибок стал пустым.
Коллега, который этим вопросом плотно занимается, считает, что это оттого, что по умолчанию протокол используется plain text (а именно, ascii), и UTF-символы интерпретируются неправильно (у нас ферма из нескольких машин, и на каждой окружение несколько отличается, к сожалению). И как только включили бинарный протокол, разногласия исчезли.

	function __construct()
	{
		parent::__construct();

		$this->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
		$this->setOption( Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT );
		$this->setOption( Memcached::OPT_HASH, Memcached::HASH_CRC );
		$this->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, 3 );
		$this->setOption( Memcached::OPT_NO_BLOCK, true );									// асинхронный ввод-вывод
		$this->setOption( Memcached::OPT_TCP_NODELAY, true );									// при работе с сокетами надо потестить - может ускорить работу

		$this->connection = $this->addServers( array(	array( <хост>, <порт> ) ) );
		...
	}

При использовании ключа:

	...
	$key = str_replace( array( ' ', ',', ':' ), '_', $key );

	if ( strlen($key) > 220 ) {
		$key = md5( $key );
	}
	...
Теги:
Окт 12

Нужно было навешивать и снимать обработчик клика динамически, при этом не затронув чужие обработчики, а их на элементе несколько, каждый ставится своим виджетом.
Мой jQuery-ui-виджет навешивает клик только если его ещё нет. Для этого сделан отдельный метод:

_bindClick: function()
{
	var me = this;
	var $me = $( me.element );

// боремся с повторным навешиванием обработчика
	var events = $.data( $me.get(0), 'events' );
	var filteredEvents = [];
	if ( 'click' in events )
	{
		filteredEvents = $.grep( events.click, function( Event ) {
			return Event.type == 'click' && Event.namespace == me.widgetName;
		} );
	}
	if ( filteredEvents.length ) {
		return;
	}

	$me.bind( 'click.' + me.widgetName, function( Event ) {
	// логика клика, к примеру:
		if ( ! me.options.allowDoubleClick ) {
			$me.unbind( 'click.' + me.widgetName );
		}
	} );
},
//===========================================================================}}}
Теги:
preload preload preload