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