
Здравствуйте дорогие хаброчитатели!
Думали ли Вы когда-нибудь о том, чтобы Ваш сайт одинаково хорошо работал с включенным 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-модулями, пишите мне, у меня уже есть кое-что для поддержания обмена :)
