Zepto или jQuery плюс Angular плюс Yii выдавать через CDN
Есть такой сложный проект, в котором фронтенд построен на базе Angular.js, который использует клиентскую библиотеку javascipt либо zepto.js, либо jquery.js в зависимости от версии браузера. Серверная часть построена на базе фреймворка Yii (PHP).
И вот сейчас я хочу раздавать файлы JS через сеть передачи данных (content delivery network или просто CDN). Опишу, с чем столкнулся и как решил.
Мной выбрана CDN с прозрачным кэшированием. Принцип её работы такой: когда клиент, то есть браузер, обращается в CDN, а там нет файла с таким URL, то файл извлекается из моего основного сайта. На примере: hostname — это имя моего основного сайта, а cdn.hostname — это имя моего сайта в сети CDN. Если в CDN по URL вида http://cdn.hostname/path/to/file.js файла нет, то он считывается по адресу http://hostname/path/to/file.js. После этого файл на некоторое время кэшируется многократно в сети географически распределённых серверов. Это основная фишка CDN — для каждого запроса он подбирает наиболее близко расположенный сервер, и это ускоряет получение ответа. Для браузера основной сайт просто начинает работать быстрее и всем это нравится: пользователям, поисковым роботам, администратору основного сервера.
Я описал очень простую схему. Её простота в том, что на стороне сервера не требуются дополнительные процедуры публикации и обновления файлов в CDN. Но есть сложность: чтобы после обновления сайта вся логика javascript работала без сбоев, нужно чтобы к страницам всегда подключались файлы JS самой последней версии. Значит я не могу использовать для JS разных версий один и тот же URL, он должен отличаться. К счастью, фреймворк Yii берёт на себя генерацию уникальных URL для статических файлов разных версий.
Вот пример ссылок на файл двух разных версий:
http://hostname/assets/920848f6/lib/js/package.min.js
http://hostname/assets/8b758709/lib/js/package.min.js
Для того, чтобы были созданы такие уникальные ссылки, используют так называемые ресурсы (assets). Файл package.min.js размещают в директории ресурсов, и имя этой директории зависит от внутреннего содержимого файла. Если файл изменился, то для него создаётся новая директория.
Вот такой код помещает файл /lib/js/package.min.js в ресурсы:
/** @var $assetManager CAssetManager */ $assetManager = Yii::app()->assetManager; $assetsPath = $assetManager->publish('/<absolute_path>/lib/js/package.min.js');
Уникальный путь директории сохраняется в переменной $assetsPath. Теперь можно использовать файл /lib/js/package.min.js с уникальным именем:
/** @var $clientScript CClientScript */ $clientScript = Yii::app()->clientScript; $clientScript->registerScriptFile($assetsPath.'/lib/js/package.min.js', CClientScript::POS_END);
После этого в странице HTML появится ссылка на скрипт:
<script type="text/javascript" src="http://hostname/assets/8b758709/package.min.js"></script>
Ради оптимизации по скорости мы используем javascript библиотеку zepto.js, которая работает замечательно везде, кроме Internet Explorer, для которого рекомендуется использовать jQuery. Zepto и jQuery совместимы: код, написанный под zepto, будет работать под jquery. Браузер сам решает, какую библиотеку ему использовать, потому что выбор происходит в таком небольшом скрипте:
<script> // это рекомендованный zepto способ выбора библиотеки document.write('<script src=' + ('__proto__' in {} ? 'zepto' : 'jquery') + '.js><\/script>') </script>
Поскольку выбор клиентской библиотеки выполняется в браузере, а не на сервере, то я не могу просто взять и подключить два скрипта через ClientScript
/** @var $clientScript CClientScript */ $clientScript = Yii::app()->clientScript; // так работать не будет $clientScript->registerScriptFile($assetsPath.'/zepto.js', CClientScript::POS_END); $clientScript->registerScriptFile($assetsPath.'/jquery.js', CClientScript::POS_END);
Это была первая сложность: не сервер, а браузер решает, какой javascript подключить.
Кроме того, в проекте также используется библиотека Angular.js и она требует, чтобы jquery или zepto загрузились до того, как загрузится angular. Это вторая проблема.
Чтобы решить это, я написал виджет для Yii ZeptoCoreJs
Логика виджета
<?php /** * Виджет для регистрации основных библиотек Javascript zepto или jquery, в зависимости от возможностей браузера */ class ZeptoCoreJs extends CWidget { protected $_assetsDir; public function run() { /** @var $assetManager CAssetManager */ $assetManager = Yii::app()->assetManager; // опубликовать в ресурсы все скрипты, которые есть в папке /assets/ - там jquery и zepto $this->_assetsDir = $assetManager->publish(dirname(__FILE__) . '/assets/'); // код выбора библиотеки $script = <<<SCRIPT if ('__proto__' in {}) { document.write('<script src="{$this->_assetsDir}/zepto/zepto.min.js"><\/script>'); } else { document.write('<script src="{$this->_assetsDir}/jquery/jquery.min.js"><\/script>'); } SCRIPT; // подключение к странице static $scriptInserted; if (!$scriptInserted) { echo CHtml::script($script); } $scriptInserted = true; } } ?>
Как использовать?
По-гавнокодерски. Встроить прямо в шаблон, без всяких событий.
В основной шаблон
/project/protected/views/layouts/main.php
<?php /** * Эта страница подключается в любых файлах разметки */ ?><!doctype html> <html lang="en-US" id="app" xmlns="http://www.w3.org/1999/xhtml"> <head> </head> <body> <?php // В переменной $content передаётся результат отрисовки лэйaута, например, //layout/1column ?> <?php echo $content; ?> <?php $this->widget('application.widgets.zeptocorejs.ZeptoCoreJs') // вставка выбора библиотеки перед закрывающимся тегом body?> </body> </html>
Распаковать в папку project/protected/widgets
Использование: в основном шаблоне сайта перед закрывающимся тегом <body> вставить следующий код:
<?php $this->widget("application.widgets.zeptocorejs.ZeptoCoreJs") ?>
А теперь про использование CDN. В файле /project/protected/config/main.php нужно указать другой базовый URL к ресурсам
<?php // This is the main Web application configuration. Any writable // CWebApplication properties can be configured here. return array( // ... // компоненты 'components' => array( // ... // ресурсы для статики отдельных модулей/расширений/виджетов 'assetManager' => array( 'class' => 'CAssetManager', 'basePath' => realpath(APP_PATH . '/www/static/assets'), // CDN подключается только в режиме "без отладки" 'baseUrl' => YII_DEBUG ? '/assets/' : 'http://cdn.hostname/assets/', ), // ... // ... );
Теперь в HTML ссылки на javascript будут выглядеть так:
<script type="text/javascript"> if ('__proto__' in {}) { document.write('<script src="http://cdn.hostname/assets/920848f6/zepto/zepto.min.js"><\/script>'); } else { document.write('<script src="http://cdn.hostname/assets/920848f6/jquery/jquery.min.js"><\/script>'); } </script>
А в настройках CDN указать, что при отсутствии файла http://cdn.hostname/assets/920848f6/zepto/zepto.min.js его нужно получить по адресу http://hostname/assets/920848f6/zepto/zepto.min.js
Теперь я все проблемы решил.
Ссылки по теме:
- jquery.js
- zepto.js
- angular.js
- Yii, документация по CClientScript, CAssetManager
- Используемый мной сервис CDN cdn77.com и описание настройки проксирования статического контента