FEATURE Added Requirements::combine_files() to reduce HTTP requests by concatenating javascript and css files. Uses JSMin library to further minify the payload by default.

Merged revisions 55913 via svnmerge from 
svn://svn.silverstripe.com/silverstripe/modules/sapphire/branches/2.2.0-mesq

........
  r55913 | gmunn | 2008-06-10 10:30:14 +1200 (Tue, 10 Jun 2008) | 1 line
  
  javascript combined files and google CDN implemented
........


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@58316 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-07-17 23:32:31 +00:00
parent 211aa73914
commit 60a0a04c39
8 changed files with 230 additions and 3 deletions

View File

@ -6,14 +6,36 @@
* @subpackage view * @subpackage view
*/ */
class Requirements { class Requirements {
private static $javascript = array(); private static $javascript = array();
private static $css = array(); private static $css = array();
private static $customScript = array(); private static $customScript = array();
private static $customCSS = array(); private static $customCSS = array();
private static $customHeadTags = ""; private static $customHeadTags = "";
private static $disabled = array(); private static $disabled = array();
private static $blocked = array(); private static $blocked = array();
/**
* See {@link combine_files()}.
*
* @var array $files_to_combine
*/
public static $files_to_combine = array();
/**
* Using the JSMin library to minify any
* javascript file passed to {@link combine_files()}.
*
* @var boolean
*/
public static $combine_js_with_jsmin = true;
/** /**
* Register the given javascript file as required. * Register the given javascript file as required.
* Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js' * Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js'
@ -175,15 +197,25 @@ class Requirements {
/** /**
* Update the given HTML content with the appropriate include tags for the registered * Update the given HTML content with the appropriate include tags for the registered
* requirements. * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
* including a <head> tag. The requirements will insert before the closing <head> tag automatically.
*
* @todo Calculate $prefix properly * @todo Calculate $prefix properly
*
* @param string $templateFilePath Absolute path for the *.ss template file
* @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}.
* @return string HTML content thats augumented with the requirements before the closing <head> tag.
*/ */
static function includeInHTML($templateFile, $content) { static function includeInHTML($templateFilePath, $content) {
if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML"); if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML");
if(strpos($content, '</head') !== false && (Requirements::$javascript || Requirements::$css || Requirements::$customScript || Requirements::$customHeadTags)) { if(strpos($content, '</head') !== false && (Requirements::$javascript || Requirements::$css || Requirements::$customScript || Requirements::$customHeadTags)) {
$prefix = Director::absoluteBaseURL(); $prefix = Director::absoluteBaseURL();
$requirements = ''; $requirements = '';
$jsRequirements = '';
// Combine files - updates Requirements::$javascript and Requirements::$css
self::process_combined_includes();
foreach(array_diff_key(self::$javascript,self::$blocked) as $file => $dummy) { foreach(array_diff_key(self::$javascript,self::$blocked) as $file => $dummy) {
if(substr($file,0,7) == 'http://' || Director::fileExists($file)) { if(substr($file,0,7) == 'http://' || Director::fileExists($file)) {
@ -198,6 +230,9 @@ class Requirements {
$requirements .= "\n//]]>\n</script>\n"; $requirements .= "\n//]]>\n</script>\n";
} }
} }
$jsRequirements=$requirements;
foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) { foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) {
if(Director::fileExists($file)) { if(Director::fileExists($file)) {
$media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : ""; $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : "";
@ -219,6 +254,139 @@ class Requirements {
} }
} }
/**
* Concatenate several css or javascript files into a single dynamically generated
* file (stored in {@link Director::baseFolder()}). This increases performance
* by fewer HTTP requests.
*
* The combined file is regenerated
* based on every file modification time. Optionally a rebuild can be triggered
* by appending ?flush=1 to the URL.
* If all files to be combined are javascript, we use the external JSMin library
* to minify the javascript. This can be controlled by {@link $combine_js_with_jsmin}.
*
* All combined files will have a comment on the start of each concatenated file
* denoting their original position. For easier debugging, we recommend to only
* minify javascript if not in development mode ({@link Director::isDev()}).
*
* CAUTION: You're responsible for ensuring that the load order for combined files
* is retained - otherwise combining javascript files can lead to functional errors
* in the javascript logic, and combining css can lead to wrong styling inheritance.
* Depending on the javascript logic, you also have to ensure that files are not included
* in more than one combine_files() call.
*
* Example for combined JavaScript:
* <code>
* Requirements::combine_files(
* 'foobar.js',
* array(
* 'mysite/javascript/foo.js',
* 'mysite/javascript/bar.js',
* )
* );
* </code>
*
* Example for combined CSS:
* <code>
* Requirements::combine_files(
* 'foobar.css',
* array(
* 'mysite/javascript/foo.css',
* 'mysite/javascript/bar.css',
* )
* );
* </code>
*
* @see http://code.google.com/p/jsmin-php/
*
* @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()} by default)
* @param array $files Array of filenames relative to the webroot
*/
static function combine_files($combinedFileName, $files){
self::$files_to_combine[$combinedFileName] = $files;
}
/**
* See {@link combine_files()}.
*/
static function process_combined_includes() {
// Make a map of files that could be potentially combined
$combinerCheck = array();
foreach(self::$files_to_combine as $combinedFile => $sourceItems) {
foreach($sourceItems as $sourceItem) {
if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
user_error("Requirements::process_combined_includes - file '$sourceItem' appears in two combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
}
$combinerCheck[$sourceItem] = $combinedFile;
}
}
// Figure out which ones apply to this pageview
$combinedFiles = array();
$newJSRequirements = array();
$newCSSRequirements = array();
foreach(Requirements::$javascript as $file => $dummy) {
if(isset($combinerCheck[$file])) {
$newJSRequirements[$combinerCheck[$file]] = true;
$combinedFiles[$combinerCheck[$file]] = true;
} else {
$newJSRequirements[$file] = true;
}
}
foreach(Requirements::$css as $file => $params) {
if(isset($combinerCheck[$file])) {
$newCSSRequirements[$combinerCheck[$file]] = true;
$combinedFiles[$combinerCheck[$file]] = true;
} else {
$newCSSRequirements[$file] = $params;
}
}
Requirements::$javascript = $newJSRequirements;
Requirements::$css = $newCSSRequirements;
// Process the combined files
if($combinedFiles) {
$base = Director::baseFolder() . '/';
foreach($combinedFiles as $combinedFile => $dummy) {
$fileList = self::$files_to_combine[$combinedFile];
// Determine if we need to build the combined include
if(file_exists($base . $combinedFile) && !isset($_GET['flush'])) {
$srcLastMod = 0;
foreach($fileList as $file) {
$srcLastMod = max(filemtime($base . $file), $srcLastMod);
}
$refresh = $srcLastMod > filemtime($base . $combinedFile);
} else {
$refresh = true;
}
// Rebuild, if necessary
if($refresh) {
$combinedData = "";
foreach($fileList as $file) {
$fileContent = file_get_contents($base . $file);
if(stripos($file, '.js') && self::$combine_js_with_jsmin) {
$fileContent = JSMin::minify($fileContent);
}
$combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n";
}
if(!file_exists(dirname($base . $combinedFile)))
mkdir(dirname($base . $combinedFile), Filesystem::$folder_create_mask, true);
$fh = fopen($base . $combinedFile, 'w');
fwrite($fh, $combinedData);
fclose($fh);
}
}
}
}
static function get_custom_scripts() { static function get_custom_scripts() {
$requirements = ""; $requirements = "";

View File

@ -0,0 +1,53 @@
<?php
/**
* @package sapphire
* @subpackage tests
*
* @todo Test that order of combine_files() is correct
*/
class RequirementsTest extends SapphireTest {
static $html_template = '<html><head></head><body></body></html>';
function testCombinedJavascript() {
// require files normally (e.g. called from a FormField instance)
Requirements::javascript('sapphire/tests/forms/a.js');
Requirements::javascript('sapphire/tests/forms/b.js');
Requirements::javascript('sapphire/tests/forms/c.js');
// require two of those files as combined includes
Requirements::combine_files(
'bc.js',
array(
'sapphire/tests/forms/b.js',
'sapphire/tests/forms/c.js'
)
);
$combinedFilePath = Director::baseFolder() . '/' . 'bc.js';
$html = Requirements::includeInHTML(false, self::$html_template);
/* COMBINED JAVASCRIPT FILE IS INCLUDED IN HTML HEADER */
$this->assertTrue((bool)preg_match('/src=".*\/bc\.js"/', $html));
/* COMBINED JAVASCRIPT FILE EXISTS */
$this->assertTrue(file_exists($combinedFilePath));
/* COMBINED JAVASCRIPT HAS CORRECT CONTENT */
$this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false));
$this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('c')") !== false));
/* COMBINED FILES ARE NOT INCLUDED TWICE */
$this->assertFalse((bool)preg_match('/src=".*\/b\.js"/', $html));
$this->assertFalse((bool)preg_match('/src=".*\/c\.js"/', $html));
/* NORMAL REQUIREMENTS ARE STILL INCLUDED */
$this->assertTrue((bool)preg_match('/src=".*\/a\.js"/', $html));
unlink($combinedFilePath);
}
}
?>

1
tests/forms/a.css Normal file
View File

@ -0,0 +1 @@
.a {color: #f00;}

1
tests/forms/a.js Normal file
View File

@ -0,0 +1 @@
alert('a');

1
tests/forms/b.css Normal file
View File

@ -0,0 +1 @@
.b {color: #0ff;}

1
tests/forms/b.js Normal file
View File

@ -0,0 +1 @@
alert('b');

1
tests/forms/c.css Normal file
View File

@ -0,0 +1 @@
.c {color: #0ff;}

1
tests/forms/c.js Normal file
View File

@ -0,0 +1 @@
alert('c');