суббота, 24 марта 2012 г.

Управление ресурсами в Symfony2





На этой неделе, помимо прочих задач, я занимался клиентской оптимизацией на своем рабочем проекте. И узнал кое-что новенькое, чем и спешу с Вами поделиться.

Статья называется "Управление ресурсами в Symfony2". Поэтому хочу сразу оговориться, что я понимаю под словом "ресурс" (англ. asset). В данной статье под термином "ресурс" я подразумеваю HTTP ресурс, запрашиваемый браузером во время загрузки документа, т.е. это javascript файлы, css файлы, изображения и т.д.

Итак, приступим. В официальной документации по Symfony2 можно прочитать, что включать ресурсы в шаблоны нужно так:

// css
<link href="{{ asset('/css/main.css') }}" type="text/css" />


// javascript
<script src="{{ asset('/js/main.js') }}" type="text/javascript"></script>


// images
<img src="{{ asset('/images/header.png') }}" alt="" />

Но для чего же нужна функция asset()? Почему не желательно указывать в атрибутах href и src относительный путь к файлу на сервере? Давайте разберемся.

Я взял для демонстрации несколько файлов из Blueprint CSS Framework и подключил их в шаблоне таким образом:

<link href="{{ asset('/css/blueprint/reset.css') }}" type="text/css" media="screen, projection">
<link href="{{ asset('/css/blueprint/grid.css') }}" type="text/css" media="screen,  projection">
<link href="{{ asset('/css/blueprint/forms.css') }}" type="text/css" media="screen,  projection">
<link href="{{ asset('/css/blueprint/forms.css') }}" type="text/css" media="screen,  projection">
<link href="{{ asset('/css/blueprint/typography.css') }}" type="text/css" media="print">
<!--[if lt IE 8]><link href="{{ asset('/css/blueprint/ie.css') }}" type="text/css" media="screen, projection"><![endif]-->


Если больше ничего не настраивать в конфиге, то Symfony отрендерит приведенный кусок шаблона в такой html:

<link href="/css/blueprint/reset.css" type="text/css" media="screen, projection">
<link href="/css/blueprint/grid.css" type="text/css" media="screen,  projection">
<link href="/css/blueprint/forms.css" type="text/css" media="screen,  projection">
<link href="/css/blueprint/forms.css" type="text/css" media="screen,  projection">
<link href="/css/blueprint/typography.css" type="text/css" media="print">
<!--[if lt IE 8]><link href="/css/blueprint/ie.css" type="text/css" media="screen, projection"><![endif]-->

В принципе, этого достаточно. Но, Symfony2 может сделать для вас кое-какую дополнительную работу. Стоит всего лишь задать несколько опций в конфигурации проекта.

1. Множественные домены

Symfony2 позволяет задать список доменов, а точнее список базовых URI. Т.е. в конфигурации можно прописать адреса источников из которых доступна загрузка ресурсов для нашего проекта.

Так как у меня всего один сервер, да и тот локальный, я просто прописал несколько доменов в /etc/hosts, а затем добавил эти же алиасы в конфигурации виртуального хоста. Теперь в конфигурационном файле я могу задать базовые URI для загрузки ресурсов:

framework:
    templating:
        assets_base_urls:
            - http://asset1.test.local
            - http://asset2.test.local
            - http://asset3.test.local
            - http://asset4.test.local
            - http://asset5.test.local


Тогда наш кусочек шаблона выведется как-то так:

<link href="http://asset2.test.local/css/blueprint/reset.css" type="text/css" media="screen, projection">
<link href="http://asset3.test.local/css/blueprint/grid.css" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css" type="text/css" media="screen,  projection">
<link href="http://asset5.test.local/css/blueprint/typography.css" type="text/css" media="print">
<!--[if lt IE 8]><link href="http://asset5.test.local/css/blueprint/ie.css" type="text/css" media="screen, projection"><![endif]-->

Т.е. Symfony добавит к каждому относительному пути указанному в функции asset() один из базовых URI из конфига. Я не разбирался с тем, в каком порядке будут подставлены URI из конфигурационного файла, могу лишь сказать, что последовательность всегда одна и та же. Т.е. если мы использовали один домен для загрузки ресурса, то этот ресурс для пользователя всегда будет загружаться с выбранного домена. В другом случае эта фича не имела бы смысла, т.к. все ресурсы кешируются браузером. И браузер понимает ресурс с другого домена как уникальный и скачивает его вместо того, чтобы использовать закешированный.

Таким образом используя конфигурационную опцию assets_base_urls можно разнести ресурсы на несколько серверов или использовать CDN. Но даже, если у вас всего один сервер, как в моем случае. То все равно имеет смысл сконфигурировать для вашего сервера дополнительные домены и использовать их как алиасы для загрузки ресурсов. И сейчас объясню зачем.

Дело в том, что браузеры имеют ограничения на количество открытых соединений к одному домену. В таблице ниже приведено количество возможных одновременных соединений с одним доменом для разных браузеров (информация не проверена).


Browser HTTP/1.1 HTTP/1.0
IE 6,7 2 4
IE 8 6 6
Firefox 2 2 8
Firefox 3 6 6
Safari 3,4 4 4
Chrome 1,2 6 ?
Chrome 3 4 4
Chrome 4+ 6 ?
iPhone 2 4 ?
iPhone 3 6 ?
iPhone 4 4 ?
Opera 9.63,10.00alpha 4 4
Opera 10.51+ 8 ?



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

2. Сache busting


Как я уже говорил, все ресурсы кешируются на стороне клиента (т.е. браузером). Но что делать, если вы, скажем, обновили стили и пользователям нужно загрузить новую версию стилей, а старая версия находится в кеше. Решение в данном случае простое. В запрос нужно просто добавить какое-нибудь уникальное значение. URI ресурса в этом случае поменяется и браузер загрузит новую актуальную версию. И совсем не обязательно сообщать пользователям: "Мы там стили обновили. Почистите в Вашем браузере кеш, пожалуйста!" :)

В Symfony2 можно просто указать в конфиге версию ресурсов. Например, я добавил в свой конфиг такие строки:

framework:
    templating:
        assets_version: 1.0.0

и получил такой html:

<link href="http://asset2.test.local/css/blueprint/reset.css?1.0.0" type="text/css" media="screen, projection">
<link href="http://asset3.test.local/css/blueprint/grid.css?1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css?1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css?1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset5.test.local/css/blueprint/typography.css?1.0.0" type="text/css" media="print">
<!--[if lt IE 8]><link href="http://asset5.test.local/css/blueprint/ie.css?1.0.0" type="text/css" media="screen, projection"><![endif]-->

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

Но и это еще не все. Предположим, что нашему серверу нужно сохранить старые версии ресурсов в другой директории, к примеру обновления интерфейса происходит только для части пользователей. И чтобы получить актуальную версию ресурсов, нам нужно передать серверу именованный параметр. Пуcть, он будет называться "v". По этому параметру мы и будем определять какая версия файлов нужна.

Чтобы реализовать такое поведение, снова отредактируем конфиг:

framework:
    templating:
      assets_version_format  : %%s?v=%%s

И проверим, что же у нас получилось:

<link href="http://asset2.test.local/css/blueprint/reset.css?v=1.0.0" type="text/css" media="screen, projection">
<link href="http://asset3.test.local/css/blueprint/grid.css?v=1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css?v=1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset1.test.local/css/blueprint/forms.css?v=1.0.0" type="text/css" media="screen,  projection">
<link href="http://asset5.test.local/css/blueprint/typography.css?v=1.0.0" type="text/css" media="print">
<!--[if lt IE 8]><link href="http://asset5.test.local/css/blueprint/ie.css?v=1.0.0" type="text/css" media="screen, projection"><![endif]-->

Как вы поняли в параметре assets_version_format задается шаблон для sprintf(). Только знак процента нужно дважды указывать для экранирования.

На этом на сегодня все. И сделаем свои приложения лучше... с Symfony2!