9 нояб. 2008 г.

Пакетирование js- и сss-ресурсов. Управление подключением ресурсов с помощью XML/XSL.

В этой статье я затрону тему подключения ресурсов, а именно, как можно управлять подключением нужных JavaScript и CSS-файлов в больших проектах, с большим количеством функционала, чтобы это быстро не превратилось в хаос.

Я расскажу о способе, который использую в своем фрэймворке «Конфигуратор», однако, если вашим проектам не чужды понятия XML и XSL, вы легко можете адаптировать предлагаемый способ к своим нуждам.

Если вкратце, то я предлагаю разбивать все js- и css-ресурсы проекта на пакеты, исходя из их функциональности. Внутри пакета собирать входящие в него файлы и минимизировать их. В режиме разработки отдавать исходники как они есть, а в «боевом» — собранные по пакетам и минимизированные (для уменьшения количества http-запросов и размера файлов). Кроме этого, за каждую функциональность проекта должен отвечать отдельный xsl (который непосредственно и знает о необходимых этой функциональности ресурсах). Причем все подключения, сборки пакетов, и их минимизации должны происходить при как можно меньшем количестве телодвижений со стороны разработчика.

Рассмотрим на примере двух пакетов — global (содержащего базовую функциональность проекта) и zforms (содержащий функциональность для форм). Пакет global должен подключаться везде, а пакет zforms — только на страницах форм.

Xml-описание пакетов (должно быть доступно везде внутри проекта):

<package name="global" version="1">
  <css media="screen">
    <file name="common-screen.css" dev-mode="development" />
    <file name="main-screen.css" dev-mode="development" />
    <file name="main-colors-screen.css" dev-mode="development" />
    <file name="main-screen-package.css" dev-mode="production" />
  </css>
  <css media="screen" cc="if IE">
    <file name="common-screen-ie.css" dev-mode="development" />
    <file name="main-screen-ie.css" dev-mode="development" />
    <file name="main-screen-ie-package.css" dev-mode="production" />
  </css>
  <js>
    <file name="Common.js" dev-mode="development" />
    <file name="Common_Ajax.js" dev-mode="development" />
    <file name="Common-package.js" dev-mode="production" />
  </js>
</package>

<package name="zforms" version="2.43">
  <css media="screen">
    <file name="ZForms-screen.css" dev-mode="development" />
    <file name="ZForms-screen-package.css" dev-mode="production" />
  </css>
  <css media="screen" cc="if IE">
    <file name="ZForms-screen-ie.css" dev-mode="development" />
    <file name="ZForms-screen-ie-package.css" dev-mode="production" />
  </css>
  <js>
    <file name="ZForms.js" />
  </js>
</package>

Рассмотрим подробнее из чего состоит пакет. Во-первых, у пакета есть имя (@name), и версия (@version). Во-вторых, пакет делится на блоки по типу ресурсов: css и js. Каждый такой блок может содержать несколько файлов с @dev-mode = ’development’ (которые будут подключаться в режиме разработки) и один файл с @dev-mode = ’production’ (подключаемый в «боевом» режиме). Если же используемые файлы уже собраны (например вы используете какую-то библиотеку), то @dev-mode указывать не нужно. Для примера, именно так подлючается ZForms.js.

Кроме того, блоки пакета могут содержать необязательные атрибуты:

  • для css-блоков:
    • @media (screen/print/handheld)
    • @cc (для указания conditional comments, если стили блока нужно подключать только для IE)
    • @version (версия блока)
  • для js-блоков
    • @cc (аналогично css-блоку)
    • @version (версия блока)
    • @pack, true/false (использовать ли алгоритм base-62 для сжатия production-версии)

Суммарная версия файла будет складываться из версии пакета и версии блока. Версия будет добавлена к имени файла в качестве параметра — ?v={номер_версии}. Добавление этого параметра позволяет избежать кэширования браузерами предыдущей версии файла.

Я не буду останавливаться на подробном описании xsl-шаблонов для подключения ресурсов, приведу лишь общие моменты.

В общем шаблоне для всех страниц:

<xsl:template match="&document;" mode="cdocument:head">
  <head>
    ...
    <xsl:apply-templates select="." mode="cdocument:include-packages">
    ...
  </head>
</xsl:template>

<xsl:template match="&document;" mode="cdocument:include-packages">
  <xsl:call-template name="cinclude:package">
    <xsl:with-param name="name" select="'global'" />
  </xsl:call-template>
</xsl:template>

В шаблоне для форм перекрываем:

<xsl:template match="&document;" mode="cdocument:include-packages">
  <xsl:apply-imports /> <!-- подключение остальных пакетов -->
  <xsl:call-template name="cinclude:package">
    <xsl:with-param name="name" select="'zforms'" />
  </xsl:call-template>
</xsl:template>

В итоге, получаем, что за каждый функционал отвечает отдельный xsl, который вызывает шаблон для подключения своих ресурсов и, что самое важное, вызывает через apply-imports шаблоны для подключения всех остальных ресурсов текущей страницы, причем не только из пакета global, но и из других возможных независимых пакетов (при этом ничего не зная о них).

Все файлы с @dev-mode = ’production’ можно и нужно создавать автоматически при выкладке в «боевой» режим, распарсив xml-описание пакетов.

Текущее состояние dev-mode определяется глобальной переменной $cglobal:dev-mode, которая, в свою очередь, берет его из конфига проекта.

1 нояб. 2008 г.

Победа в WebHiTech

На закончившемся первом технологическом конкурсе веб-сайтов в Рунете, ZForms.ru и «Еврогараж» заняли первое и второе места в номинации «Лучшие потребительские качества»!

15 окт. 2008 г.

Нестандартное использование xsl:copy-of. Разделение содержания и представления

Обычно конструкцию вида xsl:copy-of используют для копирования фрагмента исходного дерева в результирующее дерево. При этом забывая, что xsl:copy-of можно использовать для копирования фрагмента результирующего дерева. Вроде бы, польза от этого неочевидна, однако, я покажу, как это можно применить.

Допустим, у нас есть верстка какого-то сложного html-блока, причем этот блок может повторяться во многих местах, но с разным контентом. В качестве примера рассмотрим блок с закругленными углами, так как обычно это достаточно сложная html-конструкция, состоящая из нескольких html-элементов:

<div class="rounded"> <div class="corner l-t"></div> <div class="corner r-t"></div> <div class="corner r-b"></div> <div class="corner l-b"></div> <div class="content"> <!-- контент --> </div> </div>

И мы хотим обрамлять такой конструкцией свои блоки, допустим — статьи, формы, меню. Решение в лоб — в шаблоне вывода каждого такого блока добавлять такую обрамляющую конструкцию. Т.е. «копипаст» со всеми вытекающими из него последствиями.

Примерно, это выглядело бы так (возьмем блок статей):

<xsl:template match="articles"> <div class="rounded"> <div class="corner l-t"></div> <div class="corner r-t"></div> <div class="corner r-b"></div> <div class="corner l-b"></div> <div class="content"> <xsl:apply-templates select="article"/> </div> </div> </xsl:template>

Мой вариант — создать для обрамляющей html-конструкции отдельный именованный шаблон, а контент передавать ему параметром, который внутри шаблона копировать с помошью xsl:copy-of:

<xsl:template name="block"> <xsl:param name="content"/> <div class="rounded"> <div class="corner l-t"></div> <div class="corner r-t"></div> <div class="corner r-b"></div> <div class="corner l-b"></div> <div class="content"> <xsl:copy-of select="$content"/> </div> </div> </xsl:template>

Шаблоны соответствующих блоков (статей, форм, меню) будут выглядеть следующим образом:

<xsl:template match="articles"> <xsl:call-template name="block"> <xsl:with-param name="content"> <xsl:apply-templates select="article"/> </xsl:with-param> </xsl:call-template> </xsl:template> <xsl:template match="navigation"> <xsl:call-template name="block"> <xsl:with-param name="content"> <ul class="navigation"> <xsl:apply-templates select="navigation-item"/> </ul> </xsl:with-param> </xsl:call-template> </xsl:template> <xsl:template match="form"> <xsl:call-template name="block"> <xsl:with-param name="content"> <xsl:apply-templates select="." mode="zforms"/> </xsl:with-param> </xsl:call-template> </xsl:template>

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

P.S. Впервые похожую идею я когда-то услышал от Владимира Токмакова из Студии.

5 сент. 2008 г.

Еще раз о наследовании в JavaScript

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

Допустим, что у нас есть иерархия классов вида A → B → C и в методе наследников нам нужно вызывать тот же метод базового класса вверх по иерархии. Обычно это выглядит примерно так (вариантов может быть несколько, но смысл не меняется):

// описание методов класса A ... method : function(param1, param2) { // действия класса A } ... // описание методов класса B ... method : function(param1, param2) { // действия базового класса A A.prototype.method.call(this, param1, param2); // действия класса B } ... // описание методов класса С ... method : function(param1, param2) { // действия базового класса B B.prototype.method.call(this, param1, param2); // действия класса C } ...

Таким образом, для вызова метода базового класса приходится сооружать такую неуклюжую конструкцию, где каждый раз упоминается имя базового класса и имя метода, где приходится вызывать call, а в параметрах передавать this. Вообщем, мягко говоря, не очень красиво. Ведь было гораздо красивее написать так:

// описание методов класса B ... method : function(param1, param2) { // действия базового класса A this.__base(param1, param2); // действия класса B } ... // описание методов класса С ... method : function(param1, param2) { // действия базового класса B this.__base(param1, param2); // действия класса C } ...

Возможно ли такое в JavaScript? Оказывается, возможно.

Читая блог Dean Edwards’а, я наткнулся на интересную идею реализации подобного функционала — при наследовании не просто записывать в прототип нового класса перекрываемый метод, а заворачивать его в метод-обертку, которая будет перед вызовом метода заменять this.__base на необходимый метод базового класса, а после вызова восстанавливать (потому что возможно ситуация когда перекрываемый метод вызывает другие методы, которые также могут вызывать одноименные методы базовых классов). Тоже самое можно сделать и для конструктора.

В итоге, наследование у меня происходит сейчас так:

var SubClass = BaseClass.inheritTo( { // если нужно, перекрываем конструктор __constructor : function(param) { // если нужно, вызываем конструктор базового класса this.__base(param); // и делаем что-нибудь еще }, // перекрываемый метод method : function(param) { // если нужно, вызываем метод method базового класса this.__base(param); // и делаем что-нибудь еще } }, { // если необходимо, тут перекрываем статические свойства и методы } );

Для единообразия базовый класс создается так (наследуется от абстрактного класса Abstract):

var Class = Abstract.inheritTo( { // конструктор __constructor : function(param) { }, // обычный метод method : function(param) { } }, { // если необходимо, тут создаем статические свойства и методы } );

Работоспособность этого способа была проверена на иерархии классов ZForms (около 30 классов). После рефакторинга код стал намного стройнее, короче и понятнее.

27 авг. 2008 г.

CSS-селекторы для Common.js

Начал писать свою реализацию (плагин для Common.js) css-селекторов, реализованных в большинстве фреймфорков. Пока результаты весьма радуют — на большинстве из тех селекторов, которые уже успел реализовать (на данный момент это практически все css2-селекторы), скорость гораздо выше, чем у всех остальных фреймворков. Для тестов использую SlickSpeed Selectors Test.

13 авг. 2008 г.

ZForms.ru

Наконец-то хватило сил доделать ZForms.ru.

14 июл. 2008 г.

Выравнивание чекбоксов и радиобаттонов с их лэйблами

Всегда, даже работая в Студии, как-то обходил эту проблему стороной.

Но тут перфекфионизм взял верх и решил, наконец, сделать «круто». Вкратце, в чем состоит проблема — у одних браузеров (ie, opera) есть некоторый отступ у чекбоксов и радиобаттонов (который нельзя ничем сбросить), у других нет.

Кусок исходного html-кода:

... <dl> <dt> <label>Пол</label> </dt> <dd> <div class="option"> <input type="radio" name="sex" id="sex-male" value="male" /> <label for="sex-male">мужской</label> </div> <div class="option"> <input type="radio" name="sex" id="sex-female" value="female" /> <label for="sex-female">мужской</label> </div> </dd> </dl> ...

Решение для «нормальных» браузеров:

.option { position: relative; } .option input { position: absolute; left: 0; top: 0.69em; // Величина сдвига подобрана для line-height: 1.2 margin: -4px 0 0; } .option label { margin-left: 18px; }

Теперь нужно «разобраться» с ie и opera, имеющих пресловутые отступы. Начнем c ie:

.option input { // margin: -7px 0 0 -3px; }

Кто использует conditional comments — выносит вышестоящую конструкцию в файл стилей для ie.

Осталась opera. Долго искал какой-нибудь хак для нее, самым красивым, с моей точки зрения, оказался следующий (такую конструкцию не понимает ни один браузер, кроме opera):

html:first-child .option input { margin: -7px 0 0 -3px; }

И вуаля, везде красота! Даже при масштабировании шрифта.

P.S. Никогда не пишите в css конструкцию вида: * { padding: 0 }. Если вам, конечно не все равно, как выглядят ваши формы в Firefox. Firefox сбрасывает внутренний отступ у input, а у select не сбрасывает. Потом будет проблематично их выровнять по высоте.

10 июл. 2008 г.

jQuery-адаптер для ZForms

Написал адаптер для ZForms, позволяющий работать через jQuery.

Напомню, что изначально ZForms работал только через Сommon, но было бы неправильно заставлять использовать разработчиков несколько фреймворков на проекте, если им нужны возможности ZForms.

Вкратце, идея адаптера довольна проста — подменить вызовы родной библиотеки (Common) на вызовы jQuery, при этом не изменяя код ZForms.

Например, код для добавления css-класса dom-элементу в адаптере выглядит следующим образом:

var Common = { Class : { ... add : function( oElement, sClassName ) { $(oElement).addClass(sClassName); }, ... }, ... };

7 июл. 2008 г.

Сайт еврогаража

Наконец-то доделали с Сашей Любавиным «сайт еврогаража».