Pull to refresh

Фокус с HeadScript (сборка в один файл)

Reading time 8 min
Views 3.2K
Если сделать так:
<?php $this->headScript()->appendFile('/js/my1.js');?>
<?php $this->headScript()->appendFile('/js/my2.js');?>
<?php $this->headScript()->captureStart() ?>
  var action = '<?php echo $this->baseUrl ?>';
<?php $this->headScript()->captureEnd() ?>

<?php echo $this->headScript(); ?>
<?php echo $this->magicHeadScript(); ?>


то вместо чего-то типа такого:
<script type="text/javascript" src="/js/my1.js"></script>
<script type="text/javascript" src="/js/my2.js"></script>
<script type="text/javascript">
  var action = '/123';
</script>

получим на выходе:
<script type="text/javascript" src="/cache/js/1b1004a203..._compressed.js"></script>


Cекрет фокуса


/**
* @license Public domain
*/
class My_View_Helper_MagicHeadScript extends Zend_View_Helper_HeadScript
{
  private static $cacheDir;
  private static $combine = 1;
  private static $compress = 1;
  private static $symlinks = array();
  
  private $_cache = array();
  
  static public function setConfig($cacheDir, $combine = 1, $compress = 1, $symlinks = array())
  {
    self::$cacheDir = rtrim($dir, '/') . '/';
    self::$symlinks = $symlinks;
    self::$combine = $combine;
    self::$compress = $compress;
  }
  
  public function magicHeadScript()
  {
    if (self::$combine) {
      return $this->toString();
    } else {
      return $this->view->headScript();
    }
  }
  
  public function itemToString($item, $indent, $escapeStart, $escapeEnd)
  {
    $attrString = '';
    if (!empty($item->attributes)) {
      foreach ($item->attributes as $key => $value) {
        if (!$this->arbitraryAttributesAllowed()
          && !in_array($key, $this->_optionalAttributes))
        {
          continue;
        }
        if ('defer' == $key) {
          $value = 'defer';
        }
        $attrString .= sprintf(' %s="%s"', $key, ($this->_autoEscape) ? $this->_escape($value) : $value);
      }
    }

    $type = ($this->_autoEscape) ? $this->_escape($item->type) : $item->type;
    $html = $indent . '<script type="' . $type . '"' . $attrString . '>';
    if (!empty($item->source)) {
       $html .= PHP_EOL . $indent . '  ' . $escapeStart . PHP_EOL . $item->source . $indent . '  ' . $escapeEnd . PHP_EOL . $indent;
    }
    $html .= '</script>';

    if (isset($item->attributes['conditional'])
      && !empty($item->attributes['conditional'])
      && is_string($item->attributes['conditional']))
    {
      $html = '<!--[if ' . $item->attributes['conditional'] . ']> ' . $html . '<![endif]-->';
    }

    return $html;
  }

  public function searchJsFile($src)
  {
    $path = $_SERVER['DOCUMENT_ROOT'] . $src;
    if (is_readable($path)) {
      return $path;
    }
    foreach (self::$symlinks as $virtualPath => $realPath) {
      $path = str_replace($virtualPath, $realPath, "/$src");
      if (is_readable($path)) {
        return $path;
      }
    }
    return false;
  }  
  
  public function isCachable($item)
  {
    if (isset($item->attributes['conditional'])
      && !empty($item->attributes['conditional'])
      && is_string($item->attributes['conditional']))
    {
      return false;
    }
    
    if (!empty($item->source) && false===strpos($item->source, '//@non-cache')) {
      return true;
    }
    
    if (!isset($item->attributes['src']) || !$this->searchJsFile($item->attributes['src'])) {
      return false;
    }
    return true;
  }
  
  public function cache($item)
  {
    if (!empty($item->source)) {
      $this->_cache[] = $item->source;
    } else {
      $filePath = $this->searchJsFile($item->attributes['src']);
      $this->_cache[] = array(
        'filepath' => $filePath,
        'mtime' => filemtime($filePath)
      );
    }
  }
  
  public function toString($indent = null)
  {
    $headScript = $this->view->headScript();
    
    $indent = (null !== $indent)
        ? $headScript->getWhitespace($indent)
        : $headScript->getIndent();

    if ($this->view) {
      $useCdata = $this->view->doctype()->isXhtml() ? true : false;
    } else {
      $useCdata = $headScript->useCdata ? true : false;
    }
    $escapeStart = ($useCdata) ? '//<![CDATA[' : '//<!--';
    $escapeEnd  = ($useCdata) ? '//]]>'    : '//-->';

    $items = array();
    $headScript->getContainer()->ksort();
    foreach ($headScript as $item) {
      if (!$headScript->_isValid($item)) {
        continue;
      }
      if (!$this->isCachable($item)) {
        $items[] = $this->itemToString($item, $indent, $escapeStart, $escapeEnd);
      } else {
        $this->cache($item);
      }
    }
    
    array_unshift($items, $this->itemToString($this->getCompiledItem(), $indent, $escapeStart, $escapeEnd));

    $return = implode($headScript->getSeparator(), $items);
    return $return;
  }
  
  private function getCompiledItem()
  {
    $filename = md5(serialize($this->_cache));
    $path = self::$cacheDir . $filename . (self::$compress? '_compressed' : '') . '.js';
    if (!file_exists($path)) {
      //...debug("Combine javascripts to $path...");
      mkdir(dirname($path), 0777, true);
      $jsContent = '';
      foreach ($this->_cache as $js) {
        if (is_array($js)) {
          $jsContent .= file_get_contents($js['filepath']) . "\n\n";
          //...debug($js['filepath'] . ' ... OK');
        } else {
          $jsContent .= $js . "\n\n";
          //...debug('Inline JavaScript ... OK');
        }
      }
      if ($compress) {
        $jsContent = JSMin::minify($jsContent);
      }
      file_put_contents($path, $jsContent);
    }
    
    $url = str_replace($_SERVER['DOCUMENT_ROOT'], '', $path);
    $item = $this->createData('text/javascript', array('src'=>$url));
    return $item;
  }
}

* This source code was highlighted with Source Code Highlighter.


Комментарии


Смысл работы хелпера заключается в обходе всех добавленных скриптов, вычислении хеша с учетом названий файлов, даты модифицирования скриптов (mtime), текстов встроенных скриптов, сборки всего этого в один файл и сжатии (опционально) напоследок с помощью JSMin

Если какой-либо встроенный скрипт кешировать не нужно, т.к., например, он часто меняется, то нужно добавить //@non-cache:
<?php $this->headScript()->captureStart() ?>
  //@non-cache
  var ip = <?php echo $ip ?>
<?php $this->headScript()->captureEnd() ?>


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

Не забудьте правильно разместить плагин и указать потом Zend_View addHelperPath

Если требуется сжатие скриптов, нужно положить в доступное по __autoload (либо добавить require) место класс JSMin

Перед непосредственным использованием хелпера нужно где-то указать, в какую папку скрипт должен записывать собранный файл:
My_View_Helper_MagicHeadScript::setConfig('/path/to/cache');

После модификации JS-скриптов никаких дополнительных телодвижений производить не нужно — хеш изменится и хелпер сам сделает новую сборку файла (и он уже будет с другим названием, поэтому проблемы с клиентским кешированием не будет). Естественно сборка производится однократно при первом обращении.

Аналогичное решение MagicHeadLink для сборки CSS-файлов, но только использующее Minify можно скачать ниже (лень отдельной статьей оформлять — утомился раскрашивать исходники :) ).


Скачать


MagicHeadScript + JSMin:
mhs.zip (4 Кб)

MagicHeadLink + Minify (урезанный):
mhl.zip (8 Кб)
Tags:
Hubs:
+46
Comments 129
Comments Comments 129

Articles