mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
211aa73914
commit
60a0a04c39
@ -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 = "";
|
||||||
|
|
||||||
|
53
tests/forms/RequirementsTest.php
Normal file
53
tests/forms/RequirementsTest.php
Normal 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
1
tests/forms/a.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.a {color: #f00;}
|
1
tests/forms/a.js
Normal file
1
tests/forms/a.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
alert('a');
|
1
tests/forms/b.css
Normal file
1
tests/forms/b.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.b {color: #0ff;}
|
1
tests/forms/b.js
Normal file
1
tests/forms/b.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
alert('b');
|
1
tests/forms/c.css
Normal file
1
tests/forms/c.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.c {color: #0ff;}
|
1
tests/forms/c.js
Normal file
1
tests/forms/c.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
alert('c');
|
Loading…
Reference in New Issue
Block a user