GlassFish vs. Jboss

А кто-нибудь из коллег разработчиков работал с glassfish и jboss на продакшене?

Я почему интересуюсь - в последнее время у меня ощущение, что jboss выпущен в open source только с одной целью - чтобы пользователи переходили на коммерческую версию и платили бабки за поддержку. Очень, очень он кривой. И коммьюнити у него кривое. На многие вопросы в коммьюнити нет ответа, и никто его не знает, и бесплатно не скажет.

А вот glassfist с какой стороны не посмотришь - везде гладкий и красивый, и все у него здоровски сделано, и на тестовом серваке хорошо себя ведет.

Опять же, оба представляет собой EJB-контейнер, так что проблем с портированием приложений возникнуть не должно.

Но переводить работающие проекты с одного сервера на другой вот так, вдруг - боязно, потому я и спрашиваю - а стоит ли заморачиваться, и как оно вообще, по ощущениям, glassfist на production’е по сравнению с jboss’ом?

brainbench

Сегодня сел разбирать почту, обнаружил письмо от brainbench с уведомлением, что ряд тестов до 30 ноября можно пройти бесплатно. И ссылочку на этот самый ряд тестов.

Ну и вот результат:

Тесты ajax и javascript 1.5 проходить было весело, если никуда не подглядывать, то по ним вполне можно проверить уровень собственных знаний. А вот java 2 Fundamentals, OO Concepts и Web Development concepts разочаровали - совершенно ничего интересного, их можно пройти и вовсе не обладая знаниями и опытом.

Ну и на всякий случай, даю ссылку на мой профиль на brainbench: 7798148.

Автоматизируем клиентскую оптимизацию

Предыстория

Как известно, перед тем, как выложить сайт в нет, мы его разрабатываем. И делаем мы это, как ни странно, на машине разработчика. И давно замечено, что javascript, а в некоторых случаях и css удобнее при разработке держать в нескольких файлах.

Проблема в том, что, согласно принципам, описанным в статье Best Practices for Speeding Up Your Web Site (перевод доступен на сайте webo.in), для ускорения загрузки сайта нам нужно произвести следующие манипуляции над javascript и css файлами:

  1. Слить весь javascript в один файл, причем, желательно так, чтобы сохранился нужный порядок - т.е., скажем, библиотека jQuery - была ближе к началу, а функции и объекты, которые ее используют - после нее.
  2. Слить весь css в один файл
  3. Сжать эти большие файлы с помощью какой-нибудь утилиты вроде yui-compressor (за исключением css-файлов, название которых начинается, скажем, с префикса ie_, которые содержат data:URL, и поэтому критично относятся к переходам со строки на строку, так что их для собственного спокойствия лучше не сжимать)
  4. Расположить их в таком порядке - css-файл как можно ближе к открывающему тэгу head, а js-файл - как можно ближе к закрывающему тэгу body.
  5. Выставить HTTP-заголовок expires на подольше, чтобы браузер пользователя их закешировал. Ну а для того, чтобы при следующем билде у пользователя обновился js и css надо этим файлам дать какое-нибудь уникальное имя.
  6. Перед отдачей файлов клиенту сжимать их с помощью gzip

К чему это я?

Пункты 5 и 6 уже подробно расписаны в других местах.
Я же хочу рассмотреть в этой статье вопрос автоматизации пунктов 1,2,3,4. А точнее, я хочу предложить инструмент, с помощью которого одним (ну, максимум - двумя-тремя :) нажатием кнопки можно выполнить пункты 1, 2, 3, 4 настоящего списка и получить готовые к заливке на сервер javascript и css файлы.

Инструментарий

  • Apache Ant в качестве сборщика. Выбор пал на него за скорость работы, доступность, кроссплатформенность, а также то соображение, что получившийся скрипт легко включить в более общий скрипт выкладывания проекта на production, который будет выполнять какой-нибудь CI-tool, скажем, teamcity.
  • YUI Compressor в качестве утилиты сжатия js и css файлов. Взят за кроссплатформенность, адекватность, хорошую скорость работы
  • JSLint4Java (порт JSLint на java) в качестве валидатора javascript. Мы же не хотим выкладывать нерабочий код на продакшен, верно? Кстати, фраза, написанная на офф. сайте, “JSLint may hurt your feelings” очень даже справедлива :)

Алгоритм работы скрипта

  1. Скачиваем и распаковываем JSLint4Java и YUI Compressor, кладем их в папочку tools внутри проекта. Правильнее, конечно, ложить ее куда-нибудь в место, определенное системной переменной, что-нибудь вроде $TOOLS_LOCATION, но в демонстрационном скрипте и так сойдет, а уж вы для себя поправьте скрипт, как вам нужно.
  2. Натравливаем JSLint4Java на все js-файлы. Если JSLint находит какую-нибудь ошибку, выводим ее на экран и останавливаем выполнение скрипта.
  3. Сливаем все js-файлы, за исключением тех, в имени которых есть фраза test, в один файл с уникальным именем, при этом сохраняем порядок следования файлов, который мы где-нибудь в другом месте определим. В качестве уникального имени давайте возьмем такую конструкцию: main.hh.dd.MM.yy.js, где hh, dd, MM, yy, соответственно, текущие час, день, месяц, год.
  4. Сливаем все css-файлы, за исключением тех, имя которых начинается с ie_ в один файл с уникальным именем (имя такое же, как и в предыдущем пункте, только расширение сменится на `css`). Порядок следования в данном случае не важен.
  5. Натравливаем на получившиеся файлы YUI Compressor. Если при сжатии произошла ошибка, выводим на экран ошибку и останавливаем выполнение скрипта.
  6. В html-темплейте, который подключает все файлы стилей, удаляем все тэги link, кроме тех, в src которых прописаны файлы ie_ и тех, которые содержат правила стилей, а не подключают внешний css-файл при помощи атрибута src.
  7. В том же темплейте удаляем все тэги скрипт, кроме тех, которые содержат javascript-код (а не подключают внешний файл скрипта через атрибут src).
  8. В том же темплейте прописываем получившийся css-файл как можно ближе к открывающему тэгу head.
  9. В том же темплейте прописываем получившийся js-файл как можно ближе к закрывающему тэгу body или открывающему тэгу script получившийся js-файл.. Такое странное поведение нужно вот для чего: предположим, что мы в js-файле прописали какие-то библиотечные функции, а прямо в html-файле инициализируем прямо в server-side коде js-объекты какими-то данными. Вот для этого-то нам и нужно сохранить script-тэг, а также подключить получившийся js-файл до него.
  10. Выкладываем получившиеся js, css, html файлы в какую-нибудь директорию.

Пример реализации

<code>    
        <!-- место, куда будем складывать свежескачанные yui-compressor и jslint4java -->
 
        <!-- какую версию yui compressor'а и откуда качать, а также, какое имя будет у получившегося jar-файла -->
 
        <!-- какую версию jslint4java и откуда будем качать, а также, какое имя будет у получившегося jar-файла -->
 
        <!-- откуда мы будем брать js-файлы, css-файлы, html-темплейт -->
 
        <!-- и куда мы будем их все складывать -->
 
        <!-- порядок конкатенации js-файлов. Указанные файлы будут расположены в начале общего js-файла в указанном порядке -->
        <!-- все оставшиеся файлы будут присоединены в конец файла -->
 
        <!-- эта задача всегда выполнится первой -->
 
                <!-- запомним в качестве переменной текущее время в формате mm-hh-MM-dd-yyyy -->
 
            <!-- создаем директорию, содержащую yui compressor и jslint4java -->
 
            <!-- скачаем и распакуем jslint и yui compressor -->
 
        <!-- скачаем и подготовим к работе jslint -->
 
        <!-- удостоверимся, что jslint скачан и готов к работе - эта задача выполняется непосредственно перед проверкой js-файлов -->
 
        <!-- скачаем и подготовим к работе jslint -->
 
        <!-- удостоверимся, что jslint скачан и готов к работе - эта задача выполняется непосредственно перед сжатием js/css-файлов -->
 
        <!-- валидируем javascript -->
 
                    <!-- файлы библиотек тестировать не нужно, их и другие люди уже оттестировали -->
 
        <!-- сжимаем js/css-файлы -->
 
        <!-- удаляем все старые css и js файлы в темплейте, а затем вставляем ссылки на наши сжатые файлы -->
 
        <!-- конкатенация файлов -->
 
        <!-- вызываем цели в нужном порядке -->
 
</code>

На всякий случай, пример: ant1

TODO

  1. Не слишком красивым является указание порядка конкатенации js-файлов прямо в скрипте. Гораздо лучше было бы, если бы мы писали прямо в html что-нибудь вроде . При development build’е скрипт бы заменял эту строку на несколько script-tag’ов (удобно для debug-a), а на продакшене сливал бы файлы в один в указанном порядке.
  2. Скачивание файлов при помощи ant-а не сказать, чтобы очень удобное - а что, если выйдет новая версия или изменится ссылка на скачивание? Гораздо удобнее пользоваться maven‘ом для таких случаев.
  3. Дополняйте :)

Мэт Шнайдер

С удивлением заметил, что в рунете (да и вообще в нете) мало ссылок на Мэта Шнайдера (Matt Snider), человека, мнение которого лично мне кажется очень авторитетным в области веб-дева вообще и javascript’а в частности. В общем, рекомендую: http://mattsnider.com/

Back-button problem

При создании RIA возникает целый пласт различных проблем.
Самая, пожалуй, известная проблема - проблема “кнопки назад” (это я так перевел “back-button problem” в меру своих лингвистических способностей).

Хочу поделиться своим вариантом ее решения:

Сам принцип работы очень простой - есть объект, который занимается подгрузкой нужного контента в главный контейнер, назовем его PageFlow, у него есть общедоступный (если кто не понял, это я все еще упражняюсь в литературном переводе - как еще перевести public?!) метод loadPage, который выбирает, какой именно контент подгрузить, основываясь на параметре pageName. Есть также объект History, который отрабатывает первым на странице, и на основе части anchor URL-а вызывает loadPage метод, передавая ему location.hash. Также этот объект History запоминает переходы пользователя по anchor (я все о своем - якорь? нет?). Получается красивая схема - пользователь, кликнув на ссылку, скажем, с id’ом товара, вызывает объект History, который, в свою очередь, вызывает объект PageFlow, который уже подгружает нужный контент в страницу. При этом, если пользователь кликнет по кнопке “назад” или “вперед”, все работает, как и ожидает этот самый пользователь.

Прежде, чем я начную хуячить кодом, условимся: мы используем jQuery в качестве фреймворка и разрабатываем магазин, у которого есть список всех товаров (anchor ‘all’) и странички с подробным описанием товара (anchor ‘id’). Погнали:

/**
 * History managment, for ajax-based pages
 * @class History
 * @constructor
 */
History = function () {
    var
    /**
     * @property currentHash
     * @private
     */
    currentHash,
    /**
     * @property _callback
     * @private
     */
    _callback,
 
    historyBackStack,
 
    historyForwardStack,
 
    isFirst,
 
    dontCheck,
 
    check = function () {
        var i, hash;
        if($.browser.msie) {
            // On IE, check for location.hash of iframe
            var ihistory = $("#APHistory")[0];
            var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
            hash = iframe.location.hash;
            if(hash != currentHash) {
 
                location.hash = hash;
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));
 
            }
        } else if ($.browser.safari) {
            if (dontCheck) {
                var historyDelta = history.length - historyBackStack.length;
 
                if (historyDelta) { // back or forward button has been pushed
                    isFirst = false;
                    if (historyDelta < 0) { // back button has been pushed
                        // move items to forward stack
                        for (i = 0; i < Math.abs(historyDelta); i++) {
                            historyForwardStack.unshift(historyBackStack.pop());
                        }
                    } else { // forward button has been pushed
                        // move items to back stack
                        for (i = 0; i < historyDelta; i++) {
                            historyBackStack.push(historyForwardStack.shift());
                        }
                    }
                    var cachedHash = historyBackStack[historyBackStack.length - 1];
                    if (cachedHash != undefined) {
                        currentHash = location.hash;
                        _callback(cachedHash);
                    }
                } else if (historyBackStack[historyBackStack.length - 1] == undefined &#038;& !isFirst) {
                    // back button has been pushed to beginning and URL already pointed to hash (e.g. a bookmark)
                    // document.URL doesn't change in Safari
                    if (document.URL.indexOf('#') >= 0) {
                        _callback(document.URL.split('#')[1]);
                    } else {
                        _callback('');
                    }
                    isFirst = true;
                }
            }
        } else {
            // otherwise, check for location.hash
            hash = location.hash;
            if(hash != currentHash) {
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));
            }
        }
    };
 
    return {
        initialize : function (callback) {
            _callback = callback;
            currentHash = location.hash;
 
            if ($.browser.msie) {
                // To stop the callback firing twice during initilization if no hash present
                if (currentHash == '') {
                    currentHash = '#';
                }
 
                // add hidden iframe for IE
                $("body").prepend('<iframe id="APHistory" style="display: none;"></iframe>');
                var iframe = $("#APHistory")[0].contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = currentHash;
            } else if ($.browser.safari) {
                // etablish back/forward stacks
 
                historyBackStack = [];
                historyBackStack.length = history.length;
                historyForwardStack = [];
                isFirst = true;
                dontCheck = false;
            }
            _callback(currentHash.replace(/^#/, ''));
            setInterval(check, 100);
        },
 
        add : function (hash) {
            // This makes the looping function do something
            historyBackStack.push(hash);
 
            historyForwardStack.length = 0; // clear forwardStack (true click occured)
            isFirst = true;
        },
 
        /**
         *
         * @param hash {String} desiring hash without first #
         */
        load: function(hash) {
            var newhash;
 
            if ($.browser.safari) {
                newhash = hash;
            } else {
                newhash = '#' + hash;
                location.hash = newhash;
            }
            currentHash = newhash;
 
            if ($.browser.msie) {
                var ihistory = $("#APHistory")[0]; // TODO: need contentDocument?
                var iframe = ihistory.contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = newhash;
                _callback(hash);
            }
            else if ($.browser.safari) {
                dontCheck = true;
                // Manually keep track of the history values for Safari
                this.add(hash);
 
                // Wait a while before allowing checking so that Safari has time to update the "history" object
                // correctly (otherwise the check loop would detect a false change in hash).
                var fn = function() {AP.History.setCheck(false);};
 
                window.setTimeout(fn, 200);
 
                _callback(hash);
                // N.B. "location.hash=" must be the last line of code for Safari as execution stops afterwards.
                //      By explicitly using the "location.hash" command (instead of using a variable set to "location.hash") the
                //      URL in the browser and the "history" object are both updated correctly.
                location.hash = newhash;
            }
            else {
              _callback(hash);
            }
        },
 
        /**
         * Set need we check, or not.
         * @param check {Boolean}
         * @protected
         */
        setCheck : function (check) {
            dontCheck = check;
        },
 
        /**
         * @method getCurrentHash
         * @return {String}
         */
        getCurrentHash : function () {
            return currentHash;
        }
    };
}();

PageFlow:

/**
 * Page Flow controller - load hash-specific data, show appropriate container and all that
 * @Class PageFlow
 */
var PageFlow = function () {
    /**
     * show whole list of goods
     * @method loadListOfGoods
     * @private
     */
    loadListOfGoods = function () {
        $('#goodsItemDetails').css('display', 'none');
        $('#listOfGoodsWorkArea').css('display', 'block');
    },
    /**
     * show detailed goods item view
     * @method loadGoodsItemDetails
     * @private
     */
    loadGoodsItemDetails = function (id) {
        var item = M.ListOfGoods.getGoodsItemById(id);
        if (item.pluralizedProfit.length == 0) {
            item.pluralizedProfit = item.pluralizedPrice;
        }
        // fill container with appropriate data
        M.Renderer.renderGoodsItemDetails([item]);
        // show goodsItemDetails
        $('#goodsItemDetails').css('display', 'block');
        $('#listOfGoodsWorkArea').css('display', 'none');
    },
 
    return {
        /**
         * decide what page to load
         * @method loadPage
         * @param pageName {String|Number} location.hash with stripped `#` sign
         * @public
         */
        loadPage : function (pageName) {
            if (L.isUndefined(pageName)) {
                if (M.ClientURI.isMainPage()) {
                    loadListOfGoods();
                }
            }
            // if pageName is number
            if (L.isNumber(pageName) || pageName.replace(/\d+/, '').length == 0) {
                // need to load goods item details page with provided id
                loadGoodsItemDetails(pageName);
            } else {
                switch (pageName) {
                    case 'all':
                        loadListOfGoods();
                        break;
                }
            }
        }
    };
}();

Ну а дальше все просто:

При загрузке страницы инициализируем объект History, в качестве callback передаем ему метод loadPage объекта PageFlow, а на все ссылки с rel == history навешиваем обработчик, который вместо перехода на якорь будет вызывать метод load объекта History:

$(function () {
    $('a @rel=[history]').click(function () {
        History.load(this.href.replace(/^.*#/, ''));
        return false;
    });
 
    History.initialize(PageFlow.loadPage);
});

Все файлы в заархивированном виде: BackButton Archive

Занимательная задачка

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

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

Изначально я хотел писать сюда всякую хуйню вроде “создание и настройка snippet’ов в редакторе textmate”, которая нравилась бы новичкам и “продолжающим”, и сюда приходило бы много народу. Тщеславие и все такое, вы понимаете.

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

В последнее время я окончательно перекинулся на работу с javascript, и даже уже начал забывать как выглядит java или ruby (чему сильно рад :), так что готовьтесь, большая часть статей будет именно по этим темам: javascript, ria, клиентская оптимизация и прочее и прочее.

Ну и чтобы пост не прошел совершенно порожняком и нес что-нибудь полезное, вот небольшой вопрос-тренировка:

var Queue = function () {
    var me = this instanceof Queue ? this : new Queue();
    me.p = [];
    return me.add.apply(me, arguments);
};
 
Queue.prototype = {
    add : function () {
        var callbacks = A.Array(arguments, 0, true);
        this.p.splice.apply(this.p,[this.p.length,0].concat(callbacks));
        return this;
    }
};

Внимание, вопрос:
что именно делает вторая строка метода add (11 строка примера) и зачем нужна ее запись именно в таком виде.
Ну а также, было бы здорово услышать, зачем, по вашему мнению, нужна такая функция-конструктор и для чего предназначен этот класс.

Накипело #2. XML.

Ну, как я и обещал, пришло время рассказать еще об одной “состоявшейся технологии”, которую лично я терпеть не могу. Думаю, вы поняли из названия, что речь пойдет про XML.

Итак, что такое XML? Это язык разметки документов. Точка. Имею ли я против “расширяемого языка разметки”? Нет. Если он применяется именно для того, для чего он предназначен.

К сожалению, куча людей считает, что XML - это круто. Это, сука, крайне удобная и надежная технология. Так давайте же пихать его везде, куда можно и нельзя! Давайте пихать его в API сервисов, в конфигурационные файлы, сделаем его “стандартом” обмена данными между сервером и клиентом (в случае RIA).

Знаете, что я думаю? Я думаю, что XML повсеместно внедрили какие-то человеконенавистники. По прямому заказу какого-нибудь Люцифера. Ага.

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

XML предназначен для разметки документов. Документов, сука. Которые обрабатывает специальная программа, а потом выводит в красивом и понятном человеку виде. Нахуя использовать XML в передаче данных лично мне непонятно, лишние телодвижения ради лишних телодвижений: берем данные, превращаем их в документ, передаем на клиент, превращаем документ в данные и пользуемся ими. Охуеть. Не проще ли взять данные, передать их и использовать данные на клиенте? Наверное, проще. Наверное, именно поэтому JSON вытесняет XML.

Те, кто работал с java-фреймворками, поймут меня - XML это зло. И чем больше XML’a в проекте, тем больше зла. Особенно отличились на поприще распространения зла appfuse и tapestry. Ну, java вообще в этом смысле отличный язык - в смысле поебаться на пустом месте.

Я так понимаю, началось все с того, что какой-то идиот придумал, что XML легко читать человеку. И легко править. Ебанаврот! Это чисто машинно-ориентированный формат, и чтобы его прочитать, надо пять раз уебаться башкой об стену, чтобы голова напоминала по форме системный блок - вот тогда, наверное, можно будет читать этот “документ” свободно.

Или он подумал, что круто будет задавать property в виде XML, потому что так можно в сам формат зашить, какие именно свойства можно объявлять и какие они могут иметь значения. В данном случае, по моему мнению, нужно немножко больше верить в человека. Наверное, не совсем уебаны работают на должностях программистов и администраторов, и уж как-нибудь прочитают документацию, перед тем как что-то писать в конфиги. А не прочитают - ну так и XML им не поможет, это клиника. Лично я думаю, что гораздо проще и естественнее писать конфиги в формате .property:

названиеСвойства = значениеСвойства

Или посмотрите в сторону YAML, который используется в рельсах - руки от зависти можно сгрызть, как все просто и понятно.

Как бы то ни было, мы, java-программисты, имеем то, что имеем. Уебанские, нечитаемые и сложные в написании конфигурационные файлы.

К счастью, ситуация начинает меняться в лучшую сторону, и здравый смысл побеждает - XML постепенно уступает место более вменяемым форматам.

Резюмируя: я не против XML. Это отличная технология, которая хороша на своем месте. Но. Я считаю, что затыкать каждую дырку XML смешно и глупо. И поэтому использование XML надо ограничить.

Накипело. RegExp.

Я считаю, что регулярные выражения надо законодательно запретить. Наряду с xml. Хотя нет, xml не нужно запрещать, надо только ограничить область его использования. Но об этом я как-нибудь в другой раз напишу. А пока вернемся к регулярным выражениям.

Взгляните, к примеру, на это уебище:

/(^|[\n ])((www|ftp)\.[\w\#$%&amp;~/.\-;:=?@\[\]+]*)/is

Ни один, сука, человек в здравом уме не сможет сказать с первого взгляда, что делает эта поебень, и, главное, правильно ли она это свое дело делает.
Надо, блять, копаться, посимвольно разбирая, что ловит каждая группа, чтобы только понять, что это регулярное выражение находит в тексте url и разбирает его на части.

Но это еще хуйня. Самый секс с regexp’ами - отладка. Блядь, у меня в башке звенит, когда я пытаюсь понять, что в этой каше надо изменить, чтобы эта хуйня работала правильно. Кстати, приведенный пример бажный - он не учитывает, что в строке параметров может быть запятая.

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

Короче. Программирование - тяжелая штука. Лучшие умы бьются над тем, чтобы упростить написание программ, чтобы минимизировать затраты времени на получение работающего, безбажного, легкоподдерживаемого и расширяемого кода. В принципе, все, что происходит в мире программирования (и всякие там ООП, и паттерны проектирования, и всякие гибкие методики разработки, и новые языки, и фреймворки) нацелено на решение именно этой задачи - “Как бы нам так изъебнуться, чтобы поменьше ебаться?“. И я искренне надеюсь, что в какую-нибудь светлую голову (вроде моей:) стукнет идея, чем можно заменить, не потеряв в мощности и гибкости, регулярные выражения. А для того, чтобы эта идея поскорее катапультировалась с далекой звезды и ебнула кого-нибудь по темечку я предлагаю пока что законодательно ограничить использование regexp’ов - ну там, к примеру, не больше одной регулярки на приложение - если больше, то плати штраф. Если клиент настаивает - пускай закладывает в бюджет размер штрафа. Как-то так, рублем подгонять исследовательскую мысль.

Ускоряем wordpress

Привет.
Думаю, среди читателей моего блога немало тех, кто имеет stand-alone blog на движке wordpress.

Так вот, для вас, дорогие мои, у меня есть две новости, как водится, плохая и хорошая.
Плохая состоит в том, что wordpress - довольно-таки тормознутая штука.
Виноваты в этом в основном криворукие производители тем и, особенно, криворукие производители плагинов.

особенно кривой плагин, на мой вкус, wp-ajax-edit-comments, который является образцом быдлокодинга

Хорошая - в том, что это можно поправить. Сейчас дядя Саша научит вас, как.

## Теория ##

Сначала немного теории. Уже довольно давно умные люди из компании Yahoo провели исследования на тему “как же нам ускорить наши сайты”. И выяснили, что на скорость сайта _с точки зрения пользователя_ в основном влияет оптимизация front-end’a, а не server-side. Подробнее об этом можно почитать на сайте webo.in (кстати, отличный ресурс, посвященный оптимизации времени загрузки веб-сайтов) на русском и на сайте yahoo на английском, я же просто опишу несколько простых шагов, которые позволят существенно ускорить скорость работы своего блога.

Хотя я и старался максимально упростить текст, сведя его к набору инструкций, все же большинство шагов можно выполнить только имея опыт разработки веб-сайтов, так что, если вы не программист, то лучше попросите знакомого программиста выполнить эти шаги за вас. Да, и на всякий случай, не забудьте забэкапиться :)

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

## Практика ##

#### Оптимизируем тему ####

Да, открытость платформы Wordpress - это очевидное благо. Я серьезно.
Множество прекрасных дизайнеров, верстальщиков, программистов с горящими от энтузиазма глазами вдохновенно, вдумчиво создают темы и плагины для всех, для всего человечества, не требуя ничего взамен. Это действительно прекрасно.

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

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

Таким образом, нам надо исправить недостатки, которые (возможно) присущи вашей любимой теме от рождения и поправить ее код.

Итак, для того, чтобы тема стала работать быстрее, надо сделать следующее:

1. Если тема сверстана на таблицах, переверстать ее на дивы. Я не буду касаться давнего спора “как вестать - дивами и таблицами”, замечу только, что точки зрения поставленной перед нами цели (быстро работающий с точки зрения пользователя блог) таблицы проигрывают дивам - потому что таблица отрисовывается браузером только после того, как будет полностью загружена, тогда как дивы отрисовываются сразу, как только браузер получит их с сервера. А значит, если страница сверстана в дивах, пользователь быстрее увидит контент сайта, что нам и нужно, не так ли?
2. Вы будет смеяться, но нужно убрать все стилевые правила во внешние файлы. И JavaScript - тоже. Это настолько очевидно, что я даже не буду пояснять, зачем это нужно.
3. Внешний стилевой файл прописываем в блоке head, а JavaScript файл подключаем как можно ближе к закрывающему тэгу “body”. И все скрипты аналитиков тоже располагаем пониже.
4. Сжимаем js и css. Для этого мы будем использовать yui-comressor. Делается это примерно так: качаем последний стабильный релиз YUICompressor’a с официального сайта, устанавливаем, если еще не установлено JRE и выполняем на css/js файл команду следующего вида:

java -jar /path/to/yuicompressor-*.*.*.jar -o "output_filename" $file

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

Можете использовать тему моего блога как пример.

#### Уменьшаем количество файлов ####

Так как мы можем серьезно ускорить скорость загрузки файлов, уменьшив число этих файлов (удивительно, правда? :), то разумнее всего будет слить все css-файлы и js-файлы в один. Если эти файлы размером больше ~70 килобайт, то лучше разбить их на два куска.

Если нужно уменьшить количество картинок, используя технику css спрайтов и технику image map.

Тут надо отдельно заметить, что у многих (практически у всех) плагинов есть совершенно идиотская особенность - прописывать свои js и css файлы. Идиотизм заключается в том, что очень часто разные плагины используют одну и ту же, скажем библиотеку, и подключают ее по нескольку раз. И пользователь, просматривающий ваш блог, вынужден по два-три раза грузить, к примеру, prototype или jquery. Лишние ~30-160 KB. Неслабо, прадва?

Тех. заметка: при этом совершенно непонятно, что мешало создателям wordpress сделать контроль надо всеми ресурсами, что прописывают плагины. К примеру, если один плагин, которому нужен jQuery, прописывает тэг script с jQuery и рапортует - “Вот мол, подгрузил, все, кому надо, могут использовать”, а другой, которому тоже нужен jQuery уже знает об этом и не подгружает свой вариант

Лечится это так - все скрипты, что подгружают плагины слейте с теми, что написали сами и воткните в футер, а плагинам запретите их подгружать. Тоже самое сделайте с css, только воткните в header.

#### Оптимизируем графику ####

Картинки в png и jpg форматах довольно часто неоптимизированы и хранят много лишней информации. Было бы неплохо избавиться от этой лишней информации и таким образом сжать файлы. Делается это с помощью специальных утилит. Это позволит нам уменьшить их размер в отдельных случаях на 50 процентов. Неслабо, правда?

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

Информацию об этих утилитах и команду я позаимствовал у Дмитрия Ищенко. Надеюсь, он не в обиде :)

Итак, для оптимизации всего png картинок мы будем использовать pngcrush. Я затрудняюсь ответить, как ее инсталлировать на windows или linux, но я на своем mac’e без проблем установил эту утилиту из портов.

Чтобы сжать png-файлы без потери качества, используйте следующую команду:

pngcrush -rem alla -reduce -brute image.png result.png

Для сжатия jp(e)g-файлов используйте jpegtran, которая входит в пакет libjpg, который я также установил из портов. Команда для сжатия jp(e)g-файлов без потери качества:

jpegtran -copy none -optimize -perfect src.jpg dest.jpg

Я тут набросал shell-скрипт, который рекурсивно пройдет по директории и оптимизирует все png/jp(e)g картинки в ней:

for file in `find . -iname "*.jpg" -or -iname "*.png" -or -iname "*.jpeg"`;do
    ext=${file##*.}
	if [ -n "$ext" ]; then
		if [ "$ext" = "jpg" ]; then
            echo "optimizing ${file} as jpeg file with jpegtran"
            jpegtran -copy none -optimize -perfect -outfile temp_abracadabra_filename.jpg $file 
            mv -f temp_abracadabra_filename.jpg $file;
        fi
        if [ "$ext" = "jpeg" ]; then
            echo "optimizing ${file} as jpeg file with jpegtran"
            jpegtran -copy none -optimize -perfect -outfile temp_abracadabra_filename.jpeg $file
            mv -f temp_abracadabra_filename.jpeg $file;
        fi
        if [ "$ext" = "png" ]; then
            echo "optimizing ${file} as png file with pngcrush"
            pngcrush -rem alla -reduce -brute "$file" temp_abracadabra_filename.png;
            mv -f temp_abracadabra_filename.png $file;
        fi
	fi
done;

Просто выполните его в директории uploads что в папке wp-content и все будет хорошо. Можно даже повесить его на cron-job и больше не париться на этот счет - все вновь прибывшие файлы будут оптимизироваться.

Разумеется, надо оптимизировать графику, используемую в теме, так что вот еще один shell-скрипт, который рекурсивно пройдет по директории, сожмет css, js файлы и оптимизирует jp(e)g/png файлы. На всякий случай перед его использованием не забудьте забэкапиться:

for file in `find . -iname "*.jpg" -or -iname "*.jpeg" -or -iname "*.png" -or -iname "*.js" -or -iname "*.css" `;do
    ext=${file##*.}
    if [ -n "$ext" ]; then
        if [ "$ext" = "css" ]; then
            echo "compressing ${file} as css file with yui compressor"
            java -jar /opt/yuicompressor/yuicompressor-2.3.5.jar --type css -o "temp_abracadabra_filename.css" $file 
            mv -f temp_abracadabra_filename.css $file;
        fi
        if [ "$ext" = "js" ]; then
            echo "compressing ${file} as js file with yui compressor"
            java -jar /opt/yuicompressor/yuicompressor-2.3.5.jar --type js -o "temp_abracadabra_filename.js" $file 
            mv -f temp_abracadabra_filename.js $file;
        fi
        if [ "$ext" = "jpg" ]; then
            echo "optimizing ${file} as jpeg file with jpegtran"
            jpegtran -copy none -optimize -perfect -outfile temp_abracadabra_filename.jpg $file 
            mv -f temp_abracadabra_filename.jpg $file;
        fi
        if [ "$ext" = "jpeg" ]; then
            echo "optimizing ${file} as jpeg file with jpegtran"
            jpegtran -copy none -optimize -perfect -outfile temp_abracadabra_filename.jpeg $file
            mv -f temp_abracadabra_filename.jpeg $file;
        fi
        if [ "$ext" = "png" ]; then
            echo "optimizing ${file} as png file with pngcrush"
            pngcrush -rem alla -reduce -brute "$file" temp_abracadabra_filename.png;
            mv -f temp_abracadabra_filename.png $file;
        fi
    fi
done;

Используйте меньше dns-lookups. Не выкладывайте картинки, src которых указывает на другой ресурс, лучше загрузите их к себе и пропишите ссылку на автора. Работать будет быстрее.

#### Сжатие ####

Все современные браузеры поддерживают сжатие, так что можно существенно уменьшить размер отдаваемых файлов (а значит, и время их загрузки) при помощи mod_deflate (для Apache 2.2 для Apache 1.3 надо использовать mod_gzip). Так что включите mod_deflate. Если же вы используете Apache 1.3, ниже приведенный код вам не не нужен, вам поможет статья “mod_gzip — сжатие html страниц ‘на лету’” на сайте webo.in.

Найдите файл .htaccess в корневой директории установки wordpress и добавьте в конец следующее:

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml
    SetOutputFilter DEFLATE
    BrowserMatch ^Mozilla/4 gzip-only-text/html
    BrowserMatch ^Mozilla/4\.0[678] no-gzip
    BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
    SetEnvIfNoCase Request_URI \.(?:gif|png)$ no-gzip dont-vary
    Header append Vary User-Agent env=!dont-vary
</IfModule>

Таким способом можно добиться уменьшения js/css/html файлов на 70-80%, и примерно 10% уменьшения jpeg-файлов, что значительно ускоряет загрузку сайта. Следует, правда, помнить, что использования mod_deflate увеличивает нагрузку на сервер, так как ему нужно сжать файлы перед тем, как отдать их, так что стоит проконтролировать, что использование mod_deflate не создает чрезмерной нагрузки на сервер.

#### Кеширование ####

Ну и последнее - для того, чтобы ускорить серфинг по вашему сайта нужно врубить кеширование. Это не ускорит загрузку сайта у пользователя, который первый раз пришел на ваш сайт, но положит все внешние js, css файлы, картинки к нему в кеш, и в следующий раз, когда он будет бродить по вашему сайту, ресурсы будут грузиться не с сервера, а из кеша, что значительно ускорит скорость работы вашего сайта с _точки зрения пользователя_, а также значительно снизит нагрузку на ваш сервер. Помните, что закешированные у пользователя файлы берутся из кеша браузера, поэтому, если вы внесете изменения в файл, скажем, стилей, пользователь, закешировавший style.css, не увидит их. Так что лучше включать кеширование после того, как вы доработали тему.

Опять же, добавьте следующие строчки в конец файла .htaccess и не забудьте включить mod_headers и mod_expires (или хотя бы один из них):

# используем mod_expires
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresDefault A86400        
    ExpiresByType image/x-icon A2592000
    ExpiresByType application/x-javascript A2592000
    ExpiresByType text/css A2592000
    ExpiresByType image/gif A604800
    ExpiresByType image/png A604800
    ExpiresByType image/jpeg A604800
    ExpiresByType text/plain A604800
    ExpiresByType application/x-shockwave-flash A604800
    ExpiresByType video/x-flv A604800
    ExpiresByType application/pdf A604800
    ExpiresByType text/html A900
</IfModule>
# используем mod_header
<IfModule mod_header.c>
    # 3 Month
    <FilesMatch "\.(flv|gif|jpg|jpeg|png|ico|swf)$">
        Header set Cache-Control "max-age=7257600"
    </FilesMatch>
    # 1 Week
    <FilesMatch "\.(js|css|pdf|txt)$">
        Header set Cache-Control "max-age=604800"
    </FilesMatch>
    # 10 Minutes
    <FilesMatch "\.(html|htm)$">
        Header set Cache-Control "max-age=600"
    </FilesMatch>
    # NONE
    <FilesMatch "\.(pl|php|cgi|spl)$">
        Header unset Cache-Control
        Header unset Expires
        Header unset Last-Modified
        FileETag None
        Header unset Pragma
    </FilesMatch>
</IfModule>

#### Кеширование на стороне сервера ####

Есть несколько плагинов к wordpress’у, которые позволяют кешировать файлы на стороне сервера. Работают они по такому принципу: как только какая-то из ваших страниц запрашвается на сервере, она динамически строится и создается html-файл со всеми данными, который ложится в кеш. Как только приходит запрос на эту страницы, пользователю отдается html файл вместо того, чтобы динамически строить страничку, что значительно снижает нагрузку на сервер (теперь ведь не прогоняются p