Как стать автором
Обновить
0

PHPLego: Ненавязчивый AJAX

Время на прочтение 9 мин
Количество просмотров 21K


Здравствуйте дорогие хаброчитатели!

Думали ли Вы когда-нибудь о том, чтобы Ваш сайт одинаково хорошо работал с включенным JavaScript-ом и без JavaScript-а? Чтобы, если JavaScript включен, блоки сайта перегружались AJAX-сом, а если JavaScript-а нет, то происходил просто переход на новую страницу?

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

Для себя я сформулировал задачу, по следующим критериям:
  • Переход по разделам сайта внутри и вне блоков должен осуществляться обычными ссылками, без каких бы то ни было onclick=”…”.
  • При включенном JavaScript-е блоки сайта перегружают только свою область страницы (свой div). При выключенном JavaScript-е должен происходить обычный переход по ссылке.
  • Должен существовать только один глобальный обработчик нажатия на ссылки $(“a”).click(…), который и делает всю работу по перегрузке нужных элементов страницы. Если же JavаScript отключен, то этот обработчик просто не срабатывает, и сайт продолжает работать в обычном режиме.
  • Постинг форм при включенном JavaScript-е также обновляет только ту область, в которой находится эта форма. При отключенном – все работает как обычно.
  • Должна быть возможность запретить AJAX-презагрузку некоторым областям страницы, например, поставив им какой-нибудь класс «noajax». Это если после перехода по ссылке меняется слишком много данных на странице, и они все в разных блоках. Тогда разумней перегрузить всю страницу целиком, чем обновлять каждый блочёк по отдельности. Оно и быстрее будет.
  • Должна быть возможность указать ссылке блок, который она должна перегрузить. Допустим, если нам нужно перегрузить не только текущий блок, но и блок родитель.
  • Если блок был загружен ранее, то он должен браться из кеша, дабы не гонять лишний трафик и не напрягать лишним запросом сервер.
  • В случае, если блок загружен из КЭШа, пользователь должен как-то понимать что эта не самая актуальная информация, и иметь возможность обновить блок.
  • Никакого JavaScript-a. Это конечно мое личное мнение, но я ненавижу писать на JavaScript-е. Поэтому я добавил еще один пункт. Смысл его в том, чтобы разрабатывая модули к сайту я не писал ни строчки JavaScript-а (ну максимум одну-две на модуль, и то для каких-нибудь чекбосов в форме). Не знаю, как ты, дорогой хаброчтец, но я, твою мать, лучше продам свою душу дьяволу, чем буду дебажить свой JavaScript во всех многообразиях браузеров!


Ну, вот собственно и все пожелания. Итак, приступим к реализации…

Для начала нам придется придумать систему модулей, и то, как они будут друг в друга вставляться.
Встает вопрос, где хранить данные о состоянии каждого блока? Где лучше это сделать, в куках, в переменной $_SESSION, в массиве $_GET или в базе данных? Я выбрал адресную строку, т.к. всегда можно послать ссылку другу и быть увереным, что он увидит то же самое, что и мы.
Итак, мы решили, что состояние каждого модуля должно быть отражено в GET параметрах. Т.е. по параметрам в GET-строке каждый модуль сможет определить какую информацию ему при данном запросе нужно отобразить.
Я назвал модули Lego-объектами, и решил оформить их в виде классов на языке PHP.

Запуск каждого Lego объекта осуществляется следующим образом:

$lego = new MyLego('my_name'); //Создем лего-объект и даем ему уникальное имя
//Запускаем его. В методе run() идет обработка GET-параметров и формируется вывод:
$lego->run();
echo $lego->getOutput(); // Выводим на экран результат работы Lego-объекта


Давайте задумаем так, чтобы каждый Lego-объект слушал свою переменную из адресной строки, которая будет говорить ему что делать, какой метод класса исполнять. И брать из переменной нужные ему данные, если это необходимо, ведь действия обычно делаются не просто так, а над какими-то данными – айдишниками фотографий, комментарий и т.п.

А также давайте договоримся, что адресная строка вида:
// http://example.ru/?my_name[act]=index&my_name[0]=1&my_name[1]=abc

вызовет метод index() Lego-объекта с именем my_name, передав в этот метод два аргумента – '1' и 'abc'.

Таким образом мы сделаем с вами такие объекты, методы которых можно будет вызывать прямо из адресной строки. Для безопасности, конечно, дадим таким методам в объекте какой-то префикс, например action_. Это чтобы злоумышленник не мог вызвать какой-нибудь системный метод и навредить нашему сайту.

Для того чтобы не писать один и тот же код для каждого класса Lego-объекта, давайте вынесем его в один базовый класс. Назовем его Lego. А все классы модулей будем наследовать от него.

abstract class Lego{
	private $name;
	private $output;
	public function __construct($name = false){
		//Если лень придумывать имя для лего, тогда оно будет совпадать с именем класса
		if(!$name) $name = get_class($this); 
		$this->name = $name;
	}
	
	public function getName(){
		return $this->name;
	}
	
	public function getOutput(){
		return $this->output;
	}
	
	public function run(){
		$action = $this->getAction();
		//в целях безопасности, сделаем, чтобы вызываемые извне методы начинались с action_
		$method_name = 'action_'.$act; 
		if(method_exists($this, $method_name)){
			// собственно сам вызов метода, в соответсвии с параметром в GET !!!
			// обратите внимание, то что возвращает метод, записывается в переменную $this->output
			$this->output = call_user_func_array(
					array(
						$this, 
						$method_name
					), 	
					$this->getActionParams($action));
		}
		else
			$this->output = "method {$action} does't exists";
		//А вот это уже для AJAX. Если передан в адресной строке параметр ajax={имя_лего}
		if($this->_get("ajax") == $this->getName()){
        		echo $this->output; //то мы выводим вывод только этого лего объекта
        		die; //и прекращаем выполнение скрипта. 
		}
	}
	
	// получить метод из адресной строки (тот самый my_name[act]=index);
	public function getAction(){
		$lego_params = $this->getLegoParams();
		if(!is_array($lego_params)) return;
		if(isset($lego_params["act"])) return $lego_params["act"];
		return "default"; // а это метод по умолчанию, если ничего в GET не передано
	}

	// получить все парамтры, относящиеся к текущему Lego из адресной строки
	public function getLegoParams($lego_name = false){
		if(!$lego_name) $lego_name = $this->getName();
		return $this->_get($lego_name, array());
	}

	// получить список параметров к методу (те самые &my_name[0]=1&my_name[1]=abc);
	public function getActionParams($action){
		$lego_params = $this->getLegoParams();
		if(!isset($lego_params[$action])) return array();
		if(!is_array($lego_params[$action])) return array();
		return $lego_params[$action];
	}

	// а это просто метод помошник, для ленивого получения параметров из GET
	static public function _get($key_name, $default_value = false){
		return self::__get_from_array($_GET, $key_name, $default_value);
	}

	// тоже самое, общий случай, для любого массива
	static private function __get_from_array($array, $key_name, $default_value = false){
		if(!isset($array[$key_name])) return $default_value;
		return $array[$key_name];
	}
}


Теперь любой Lego-модуль будет выглядить примерно так:

class FotoLego extends Lego{
	// этот метод исполнится по адресу http://example.ru/?{lego_name}[act]=allfotos
	public function action_allfotos(){
		$out = "тут скоро появится список всех фотографий";
		return $out;
	}
	
	// этот метод исполнится по адресу http://example.ru/?{lego_name}[act]=onefoto&{lego_name}[0]={id_фото}
	public function action_onefoto($foto_id){
		$out = "А тут типа будет отображаться одна фотка с id=$foto_id";
		return $out;
	}
	
	// этот метод исполнится по адресу http://example.ru/?{lego_name}[act]=bestfotos
	public function action_bestfotos(){
		$out = "А тут будут мои самые рейтинговые фотографии";
		return $out;
	}
}


А также, у нас получилось, что Lego-объект может содержать в себе сколь угодно много дочерних Lego-объектов:

class SomeLego extends Lego{
	....
	....
	
	public function action_someMethod(){
		// создаем дочерний Lego-контроллер
		$lego = new SomeSublego("sublego_name1");
		$lego->run(); //запускаем его
		return $lego->getOutput(); //возвращаем его вывод без изменеий
	}
	
	public function action_someMethodElse(){
		// или вот такой Lego-контроллер
		$lego = new SomeSublegoElse("sublegoelse_name1");
		$lego->run();
		// или, допустим, мы можем обернуть вывод одного Lego шаблоном:
		Smarty::assign("content", $lego->getOutput());
		return Smarty::fetch("some_template.tpl"); //и вернуть уже обернутый вывод
	}
	
	....
	....
}


А чтобы получить вывод только нужного Lego-объекта, нам всего лишь нужно дописать параметр ajax в адресную строку
// например:
// http://example.ru/?LegoSite=fotos&ajax={имя_лего_вывод_которого_нам_надо_получить}
// это благодаря тем самым последним строчкам метода Lego::run(), где мы прекращаем выполнение скрипта


Ну что же, можно приступить к созданию сайта, на этом этапе без всякого AJAX, но вполне работоспособного

Описываем класс самого корневого лего-контроллера:
class LegoSite extends Lego{
	
	//главная страница сайта
	public function action_default(){ //помните? Default вызовется, если в адресной строке не указан метод
		// На главной странице у нас будет блок авторизации:
		$lego = new LegoAuth();
		$lego->run(); //запускаем его
		Smarty::assign("auth_block", $lego->getOutput()); //передаем вывод LegoAuth в шаблонизатор
		
		// Блок горячих новостей:
		$lego = new LegoHotNews();
		$lego->run(); //запускаем его
		Smarty::assign("hotnews_block", $lego->getOutput()); //передаем вывод LegoHotNews в шаблонизатор

		// ну и, например основной блок, статьи:
		$lego = new LegoArticles();
		$lego->run(); //запускаем его
		Smarty::assign("articles_block", $lego->getOutput()); //передаем вывод LegoArticles в шаблонизатор
		
		// Рендерим самый внешний батя-шаблон:
		Smarty::fetch("body.tpl"); //В этом шаблоне определяется какой блок где находится
	}
	
	
	//а это страничка "О сайте". На ней нет никаких блоков
	public function action_about(){
		return Smarty::fetch("about_site.tpl"); //тут у нас просто текст
	}
	
}


Весь наш сайт, будет состоять из одного единственного файла — index.php:

include ".common/autoload.php"; // подключаем автоподгрузку классов

$lego = new LegoSite();	// В нем то мы и создадим этот самый корневой контроллер сайта
$lego->run();		// запускаем его
echo $lego->getOutput();// Вуаля! Выводим сайт нашему дорогому посетителю


Вот и все, сайт готов, осталось только разукрасить шаблоны (это вы уж сами) и, самое интересное, добавить немного живительного JavaScript-a. Я оформил его в виде плагина к JQuery.

Файл jquery.lego.js:
// функция, загружающая содержимое в нужное лего
jQuery.fn.lego.load = function(lego_name, url, data, nocache){
        jQuery.fn.lego.lastLoadedUrl = urldecode(url);
        jQuery.fn.lego.loadedUrls[lego_name] = urldecode(url);
        var lego = $("div.lego[name="+lego_name+"]");
        lego.addClass("loading");
        var pellicle = $("<div>"); // белая плева, закрывающая загружаемый блок
        pellicle.addClass('pellicle');
        $(".lego[name="+lego_name+"]").prepend(pellicle);
        
        var no_ajax_url = jQuery.fn.lego.getNoAjaxUrl(url);
        // если что-то пошло не так, то грузим страницу обычным способом
        if($(".lego[name="+lego_name+"]").length != 1){
            document.location = no_ajax_url;
            return;
        }
        
        location.hash = url; //сохраняем загруженный УРЛ в адресную строку
        
        //БЕРЕМ ИЗ КЭША
        var from_cache = LegoCache.get(lego_name, url);
        if(from_cache && data == null && !nocache){ //если есть в кэше
            $(".lego[name="+lego_name+"]").replaceWith(from_cache);
            return;
        } 
        
        $.ajax({
        	type: data == null ? "GET" : "POST",
        	url: url,
        	data: data, 
        	success: function(received){
                    	$().lego.log("ОК, загружено: "+received.length+" байт в лего "+lego_name+"...");
                    	if($(received).hasClass('lego')){
	            	    	$(".lego[name="+lego_name+"]").replaceWith(received);
                            	//Кладем в кэш
                            	LegoCache.put(lego_name, url, received);
                    	}
	           	 else{
	            	    	$().lego.log(lego_name+": Сервер не вернул требуемое Lego: "+url);
            		        document.location = no_ajax_url;
		    	}
	    	},
		error: function(x){
			$().lego.log("Не удалось загрузить url: "+url);
		}
        });
}

jQuery.fn.lego.ajaxEnable = function(selector){
    jQuery.fn.lego.startProcessHash();
    
    if(!selector) selector = "";
    // Обработчик на ссылки
    $(selector+":not(.noajax) a:not(.noajax)").live("click.myEvent", function(e){
        var href = $(e.currentTarget).attr("href");
        //Абсолютные ссылки не обрабатывем
        if(href.match(/^(http(s)?:\/\/)|(javascript)/i)) return true; 
        var name = $(e.currentTarget).lego().attr("name");
        var legotarget = $(e.currentTarget).attr("legotarget");
        if(typeof legotarget == "undefined") legotarget = name;
        var ajax_url = jQuery.fn.lego.getAjaxUrl(href, legotarget);
        jQuery.fn.lego.load(legotarget, ajax_url);
        return false;
    });
    
    // Обработчик на формы
    $("form:not(.noajax)").livequery("submit", function(e){
        var name = $(this).lego().attr("name");
        var legotarget = $(this).attr("legotarget");
        if(typeof legotarget == "undefined") legotarget = name;
        jQuery.fn.lego.load(legotarget, $().lego.getAjaxUrl($(this).attr("action"), legotarget), $(this).serialize());
        return false;
    });
}

// КЭШ
var LegoCache = {
	cache: {},
	put: function(lego_name, url, data){
		this.cache[lego_name+url] = data;
	},
	get: function(lego_name, url, data){
		if(typeof this.cache[lego_name+url] != 'undefined'){
			var ret = $(this.cache[lego_name+url]);
			var reload_block = $("<a href='javascript:void(0)' onclick='jQuery.fn.lego.reload(this)' />");
			reload_block.html("блок загружен из кэша, обновить");
			reload_block.addClass('reload_block');
			try{
				ret.prepend(reload_block);
			}catch(e){}
			return ret;
		}
	}
}



Конечно в полной версии еще есть пару второстепенных функций, которых я сюда не включил, боюсь статья станет плохо читаемой из-за этого. Но они всегда доступны в SVN, к вашим услугам.
Полную версию исходного кода PHPLego вы можете посмотреть в репозитории на Гугл-коде:
code.google.com/p/phplego
Там же вы можете получить инвайты на участие в разработке.

Потестить как это работает на деле можно тут.

Спасибо, надеюсь кому-то пригодится. Всегда ваш, Йожик.

P.S. Всем желающим обмениваться своими Lego-модулями, пишите мне, у меня уже есть кое-что для поддержания обмена :)
Теги:
Хабы:
+53
Комментарии 83
Комментарии Комментарии 83

Публикации

Информация

Сайт
xn--e1afggmlij4g.xn--p1ai
Дата регистрации
Дата основания
Численность
1 человек (только я)
Местоположение
Россия

Истории