[EXTENSION] ExtendedClientScript - Reduce your page loading times

Hi bettor,

I personally still use the version as in Yii extensions, works fine for me. Some others use samdarks version (see page 2 of this topic).

Please tell me if you have any issue with it, and I’ll look into it.

in my project i need also groups for differ js files. For example, for one language - one set, for other language - other set. That’s why i took last version from that thread and added groups:

i have to change registerScriptFile and registerCssFile to add groups. Usage,

registerCssFile($url, $media=’’, $group = ‘all’)

registerScriptFile($url, $position = 0, $group = ‘all’)

Yes, default group is always ‘all’.

P.S.: still need to solve problem with themes. Current version doesn’t support CSS grouping and theming :(




<?php

/**

 * Compress and cache used JS and CSS files.

 * Needs jsmin in helpers and csstidy in extensions.

 *

 * Ties into the 1.0.4 (or > SVN 813) Yii CClientScript functions.

 *

 * @author Maxximus <maxximus007@gmail.com>

 * @author Alexander Makarov <sam@rmcreative.ru>

 * @author Andrey gayvoronsky <plandem@gmail.com>

 *

 * @link http://www.yiiframework.com/

 * @copyright Copyright &copy; 2008-2009

 * @license http://www.yiiframework.com/license/

 * @version 0.7

 */

class ExtendedClientScript extends CClientScript {

	/**

	 * Compress all Javascript files with JSMin. JSMin must be installed as an extension in dir jsmin.

	 * code.google.com/p/jsmin-php/

	 */

	public $compressJs = false;

	/**

	 * Compress all CSS files with CssTidy. CssTidy must be installed as an extension in dir csstidy.

	 * Specific browserhacks will be removed, so don't add them in to be compressed CSS files

	 * csstidy.sourceforge.net

	 */

	public $compressCss = false;

	/**

	 * Combine all JS files into one.

	 *

	 * @var boolean

	 */

	public $combineJs = false;

	/**

	 * Combine all CSS files into one. Be careful with relative paths in CSS.

	 *

	 * @var boolean

	 */

	public $combineCss = false;

	/**

	 * Exclude certain files from inclusion. array('/path/to/excluded/file') Useful for fixed base

	 * and incidental additional JS.

	 */

	public $excludeFiles = array();

	/**

	 * Path where the combined/compressed file will be stored. Will use coreScriptUrl if not defined

	 */

	public $filePath;

	/**

	 * If true, all files to be included will be checked if they are modified.

	 * To enhance speed (eg production) set to false.

	 */

	public $autoRefresh = true;

	/**

	 * Relative Url where the combined/compressed file can be found

	 */

	public $fileUrl;

	/**

	 * Path where files can be found

	 */

	public $basePath;

	/**

	 * Used for garbage collection. If not accessed for that period: remove.

	 */

	public $ttlDays = 1;

	/**

	 * prefix for the combined/compressed files

	 */

	public $prefix = 'c_';

	/**

	 * CssTidy template. See CssTidy for more information

	 */

	public $cssTidyTemplate = "highest_compression";

	/**

	 * CssTidy parameters. See CssTidy for more information

	 */

	public $cssTidyConfig = array(

		'css_level' => 'CSS2.1',

		'discard_invalid_properties' => FALSE,

		'lowercase_s' => FALSE,

		'sort_properties' => FALSE,

		'sort_selectors' => FALSE,

		'preserve_css' => FALSE,

		'timestamp' => FALSE,

		'remove_bslash' => TRUE,

		'compress_colors' => TRUE,

		'compress_font-weight' => TRUE,

		'remove_last_,' => TRUE,

		'optimise_shorthands' => 1,

		'case_properties' => 1,

		'merge_selectors' => 2,

	);


	/**

	 * Used for grouping

	 */

	public $groups = array(

		'js'	=> array(), // array(url => group)

		'css'	=> array(), // array(url => group)

	);


	/**

	 * Will combine/compress JS and CSS if wanted/needed, and will continue with original

	 * renderHead afterwards

	 *

	 * @param string $output

	 */

	public function renderHead(&$output) {

		if ($this->combineJs) {

			if (isset($this->scriptFiles[parent::POS_HEAD]) && count($this->scriptFiles[parent::POS_HEAD]) !==  0) {

				$jsFiles = $this->scriptFiles[parent::POS_HEAD];

				if (!empty($this->excludeFiles)) {

					foreach ($jsFiles as &$fileName)

						(in_array($fileName, $this->excludeFiles)) AND $fileName = false;

					$jsFiles = array_filter($jsFiles);

				}

				$this->combineAndCompress('js', $jsFiles, parent::POS_HEAD);

			}

		}


		if ($this->combineCss) {

			if (count($this->cssFiles) !==  0) {

				foreach ($this->cssFiles as $url => $media)

					$cssFiles[$media][$url] = $url;

				foreach ($cssFiles as $media => $url)

					$this->combineAndCompress('css', $url, $media);

			}

		}

		parent::renderHead($output);

	}


	/**

	 * Will combine/compress JS if wanted/needed, and will continue with original

	 * renderBodyEnd afterwards

	 *

	 * @param string $output

	 */

	public function renderBodyBegin(&$output) {

		if ($this->combineJs) {

			if (isset($this->scriptFiles[parent::POS_BEGIN]) && count($this->scriptFiles[parent::POS_BEGIN]) !==  0) {

				$jsFiles = $this->scriptFiles[parent::POS_BEGIN];

				if (!empty($this->excludeFiles)) {

					foreach ($jsFiles as &$fileName)

						(in_array($fileName, $this->excludeFiles)) AND $fileName = false;

					$jsFiles = array_filter($jsFiles);

				}

				$this->combineAndCompress('js', $jsFiles, parent::POS_BEGIN);

			}

		}

		parent::renderBodyBegin($output);

	}


	/**

	 * Will combine/compress JS if wanted/needed, and will continue with original

	 * renderBodyEnd afterwards

	 *

	 * @param string $output

	 */

	public function renderBodyEnd(&$output) {

		if ($this->combineJs) {

			if (isset($this->scriptFiles[parent::POS_END]) && count($this->scriptFiles[parent::POS_END]) !==  0) {

				$jsFiles = $this->scriptFiles[parent::POS_END];

				if (!empty($this->excludeFiles)) {

					foreach ($jsFiles as &$fileName)

						(in_array($fileName, $this->excludeFiles)) AND $fileName = false;

					$jsFiles = array_filter($jsFiles);

				}

				$this->combineAndCompress('js', $jsFiles, parent::POS_END);

			}

		}

		parent::renderBodyEnd($output);

	}


	/**

	 *  Performs the actual combining and compressing

	 *

	 * @param string $type

	 * @param array $urls

	 * @param string $pos

	 */

	private function combineAndCompress($type, $urls, $pos) {

		$this->fileUrl or $this->fileUrl = $this->getCoreScriptUrl();

		$this->basePath or $this->basePath = Yii::getPathOfAlias('webroot');

		$this->filePath or $this->filePath = Yii::getPathOfAlias('webroot') . $this->fileUrl;


		$optionsHash = ($type == 'js')

				? md5($this->basePath . $this->compressJs . $this->ttlDays . $this->prefix)

				: md5($this->basePath . $this->compressCss . $this->ttlDays . $this->prefix . serialize($this->cssTidyConfig));


		$group_changes	= array();

		if ($this->autoRefresh) {

			$mtimes = array();

			foreach($this->groups[$type] as $group => $group_urls) {

				foreach($group_urls as $file) {

					$fileName = $this->basePath . '/' . trim($file, '/');

					if (file_exists($fileName)) {

						$mtimes[] = filemtime($fileName);

					}

				}


				$group_changes[$group] = md5(serialize($mtimes));

			}

		}


		$files		= array();

		$changes	= sizeof($group_changes);

		

		foreach($this->groups[$type] as $group => $group_urls) {

			$changedHash	= ($changes) ? $group_changes[$group]: '';

			$combineHash	= md5(implode('', $group_urls));

			$fileName		= $this->prefix . md5($combineHash . $optionsHash . $changedHash) . ".{$type}";

			$renewFile		= (file_exists($this->filePath . '/' . $fileName)) ? false : true;


			$files[$group]	= array(

				'hash'	=> $combineHash,

				'name'	=> $fileName,

				'renew'	=> $renewFile,

			);

		}


		$cleared	= false;

		foreach($files as $group => $c_file) {

			if($c_file['renew']) {

				if(!$cleared) {

					$this->garbageCollect($type);

					$cleared	= true;

				}


				$combinedFile = '';

				foreach($this->groups[$type][$group] as $file)

					$combinedFile .= file_get_contents($this->basePath . '/' . $file);


				if ($type == 'js' && $this->compressJs)

					$combinedFile = $this->minifyJs($combinedFile);


				if ($type == 'css' && $this->compressCss)

					$combinedFile = $this->minifyCss($combinedFile);


				file_put_contents($this->filePath . '/' . $c_file['name'], $combinedFile);

			}

		}


		foreach($this->groups[$type] as $group => $group_urls) {

			foreach($group_urls as $url) {

				$this->scriptMap[basename($url)] = $this->fileUrl . '/' . $files[$group]['name'];

			}

		}


		$this->remapScripts();

	}


	private function garbageCollect($type) {

		$files = CFileHelper::findFiles($this->filePath, array('fileTypes' => array($type), 'level' => 0));


		foreach ($files as $file) {

			if (strpos($file, $this->prefix) !== false && $this->fileTTL($file)) {

				unlink($file);

			}

		}

	}


	/**

	 * See if file is ready for deletion

	 *

	 * @param string $file

	 */

	private function fileTTL($file) {

		if (!file_exists($file)) return false;

		$ttl = $this->ttlDays * 60 * 60 * 24;


		return ((fileatime($file) + $ttl) < time()) ? true : false;

	}


	/**

	 * Minify javascript with JSMin

	 *

	 * @param string $js

	 */

	private function minifyJs($js) {

		Yii::import('application.extensions.jsmin.*');

		require_once('JSMin.php');

		return JSMin::minify($js);

	}


	/**

	 * Yii-ified version of CSS.php of the Minify package with fixed options

	 *

	 * @param string $css

	 */

	private function minifyCss($css) {

		Yii::import('application.extensions.csstidy.*');

		require_once('class.csstidy.php');


		$cssTidy = new csstidy();

		$cssTidy->load_template($this->cssTidyTemplate);


		foreach ($this->cssTidyConfig as $k => $v)

			$cssTidy->set_cfg($k, $v);


		$cssTidy->parse($css);

		return $cssTidy->print->plain();

	}


	/**

	 * Registers a javascript file.

	 * @param string URL of the javascript file

	 * @param integer the position of the JavaScript code. Valid values include the following:

	 * <ul>

	 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li>

	 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li>

	 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li>

	 * </ul>

	 * @param string group. Using for grouping (compressing/minimazing). Default group - 'all'

	 */

	public function registerScriptFile($url, $position = 0, $group = 'all') {

		parent::registerScriptFile($url, $position);


		if(!isset($this->groups['js'][$group]))

			$this->groups['js'][$group]	= array();


		$this->groups['js'][$group][]	= $url;

	}


	/**

	 * Registers a CSS file

	 * @param string URL of the CSS file

	 * @param string media that the CSS file should be applied to. If empty, it means all media types.

	 * @param string group. Using for grouping (compressing/minimazing). Default group - 'all'

	 */

	public function registerCssFile($url, $media='', $group = 'all') {

		parent::registerCssFile($url, $media);


		if(!isset($this->groups['css'][$group]))

			$this->groups['css'][$group]	= array();


		$this->groups['css'][$group][]	= $url;

	}

}




Looks good! Yes, I think CSS grouping could be interesting, especially when working with different media or themes.

For the JS combineAndCompress there could be another interesting option: http://labjs.com/. This will allow parallel loading of JS files, like ga.js from Google analytics.

Perhaps something like this could be something to be built in to Yii. Pls share your opinion!

Nice extension, but without support for relative CSS paths, it breaks many scripts. It would be great if you can add some regular expressions to automatically convert them.

Nice extension! Could someone advise a solution for the relative urls in css files e.g. jquery ui?

There is a new version supporting remote files (eg. excluding them from combining and compression) thanks to Kir. Relative paths is one of the things to desire, perhaps there will be a new version supporting this in the near future. I personally change all relative paths to absolute paths, something you should do anyway when using a CDN.

The PHP 5.3+ version, without the CssTidy issue will require another compressor since CssTidy is not developed anymore (or a rebuild of CssTidy). Perhaps cssmin will do?

Edit: the new version is based on the original one, not the one of Sam. Will combine soon.

I’m using cssmin / jsmin with a bit modified version of your extension. Works just fine.

Maybe it will be good putting it at GitHub. Probably will boost development process to some degree.

Version with CssMin is uploaded today. Some of the Sam parts are in too, but not the groups thing, didn’t work for me. It’s now an extension with CssMin and JsMin included in subdirs.

does anybody know why i get this in some pages and not others?

include(JSMin.php) [<a href=‘function.include’>function.include</a>]:

failed to open stream: No such file or directory

having $combineCss = true produces the error

Take a look at what the file is called…

Maybe it’s called JsMin.php instead?

File names are ok… :(

So if i empty assets dir after the error the page works, but others dont

it looks like the error is originated once i set the compressCss property to true …

Does anybody know if it has something to do with the way my css is written? perhaps errors? if so, is there a way for the minifier to detect it?

// UPDATE

i changed the way i was minifying css and now works, awesome extension.

There is a bug when you turn on the plan. JqueryUI JqueryTreeView and located in a subfolder of assets. And with the compression and union file is stored in the root of the problem and turns when referring to images, etc.

same error here.

Mistake at cssmin.php with yii

line 3706


if (class_exists("JSMin") && stripos($token->Value, "expression(") !== false)

after call this line when $combineCss = true

yii tries to load JSMin, but can’t find it and call error.

Fast solution is change line to


if (@class_exists("JSMin") && stripos($token->Value, "expression(") !== false)

nice solution is change ExtendedClientScript.php to load also JSMin if cssmin.php is called.

And question - if i don’t want combine scripts (js or css), just only compress - why this not work?

And suggestion, use feature of cssmin which called ImportImports

change code in method


combineAndCompress

from


if ($this->_renewFile) {

to last } of this if to


		if ($this->_renewFile)

		{

			$this->garbageCollect($type);

			$combinedFile = '';

			$importPath = false;


			foreach ($urls as $key => $file) {

			    $fileContents = file_get_contents($this->basePath.'/'.$file);

			    if ( $type == 'css' && strpos($fileContents, '@import') !== false ) {

				$importPath = dirname($this->basePath.'/'.$file);

			    }


			    if ($type == 'js' && $this->compressJs) {

				$fileContents = $this->minifyJs($fileContents);

			    }


			    if ($type == 'css' && $this->compressCss) {

				$fileContents = $this->minifyCss($fileContents, $importPath);

			    }

			    $combinedFile .= $fileContents;

			}


			file_put_contents($this->filePath.'/'.$fileName, $combinedFile);

		}

this code use combine after minify operations, it some highloaded, but give ability to use importImports

then change method


minifyCss

to


	private function minifyCss($css, $importPath)

	{

	    if ( $this->cssMinFilters['ImportImports'] && $importPath ) {

		$this->cssMinFilters['ImportImports'] = array('BasePath' => $importPath );

	    }

		Yii::import($this->cssMinPath);

		return cssmin::minify($css, $this->cssMinFilters, $this->cssMinPlugins);

	}

Great extension, big thanks for it!

Maxximus: I posted patch http://code.google.com/p/extendedclientscript/issues/detail?id=1 for this extension, please take a look at it.

Hello…

I put the htaccess file on the root of my yii web project which contains mod_headers.

localhost/yiiproject/[here]

But, when I tested in Yslow, it still gave me [color="#FF0000"]add expires header[/color] error

Does anyone know why Yii is like ‘ignoring’ the htaccess rule or just how to resolve errors that is given by Yslow?

thanks