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>
Скачать : ZeptoCoreJs (v1.0)
Распаковать в папку 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

Теперь я все проблемы решил.

Ссылки по теме:

Павел Волынцев

Уже более 15 лет занимаюсь разработкой веб-проектов. Fullstack Senior Developer. IT евангелист — доношу свет знаний об информационных технологиях. Профессиональные цели: Дать людям возможность дать людям больше.

Читайте также:

  • pavel_volyntsev

    Вот такая ерунда. Виджет писал 30 минут, а пост в блоге оформлял 2 часа.

  • Дмитрий Ершов

    Добрый день. Поставил ваше решение к себе, раньше в папке assets создавали директории вида 83s43ew теперь они просто копируются в корень но cdn работает. Подскажите как настроить чтоб директории хранились в какой то папке и отдавались по cdn. Спасибо!

    • Вопрос в этой настройке скорее всего
      ‘basePath’ => realpath(APP_PATH . ‘/www/static/assets’)
      Надо проверить что туда записалось

      • Дмитрий Ершов

        там пишу /var/www/site.ru/static/assets не помогает все в корень кидает

        • попробуй www подставить — у тебя есть такая директория?

          /var/www/site.ru/www/static/assets
          ——————— ^^^^^

          • Дмитрий Ершов

            создал не помогает в нее ничего не пишет, все в корень кидает

          • Найди меня в скайп

          • Дмитрий Ершов

            отправил запрос

          • Разобрались с настройками ‘basePath’ и ‘baseUrl’
            И познакомились с отличной платформой https://ruhotels.pro — рекомендую для поиска отелей.