现在的位置: 首页 > Web设计> 正文
网站SEO优化之CSS JS代码合并
2013年08月30日 Web设计 暂无评论 ⁄ 被围观 3,285+

在优化网站的过程中,其中一个规则就是尽量减少HTTP的请求数量,比如图片的合并,JS,CSS代码的合并等,本文介绍了一款PHP的自动合并脚本,可以在网站中使用,只要将原来需要引用JS和CSS的地方,换成该脚本的函数调用,那么脚本就会自动将过个请求合并成一个请求,重而提高网站的打开速度。

Combining Assets

One of the basic tenets of making a web-page load quickly is reducing the number of HTTP requests that your page makes. One very common way of doing this, is assuring that all of your CSS files are combined into one request (ideally in the <head> tag), and all of your javscript files are combined into one request (ideally at the bottom of the <body> tag).

The Solution

I’ve worked with several large sites which each had their own solutions, but recently I found myself needing to speed up a fairly simple site: Burndown for Trello.

To make things simple (and so that I’d never have to write this again), I made a StaticResourcessystem which will allow me to put just one PHP script into the root directory of the site, and use a few simple calls to add files to the header or footer of the page. The script requires no setup, installation, or configuration to run correctly. However, it has some optional advanced settings (which we’ll discuss at the end).

Usage

Usage is very simple. Just make a call to add files to the system, so that they’re included in the header or footer. Both local and external files are added with the same kind of call.
StaticResources - how to add files
PHP

1
2
3
StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");
StaticResources::addFooterFile("jquery-ui.js");
StaticResources::addFooterFile("http://code.jquery.com/ui/1.9.1/jquery-ui.js");
StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");
StaticResources::addFooterFile("jquery-ui.js");
StaticResources::addFooterFile("http://code.jquery.com/ui/1.9.1/jquery-ui.js");

After you’ve added the files, make sure that somewhere in your code you print the HTML which will contain all of the files that were added
StaticResources - printing the script and style tags
PHP

1
2
3
4
5
// put this at the bottom of the HEAD tag
print StaticResources::getHeaderHtml();
 
// put this at the bottom of the BODY tag
print StaticResources::getFooterHtml();
// put this at the bottom of the HEAD tag
print StaticResources::getHeaderHtml();

// put this at the bottom of the BODY tag
print StaticResources::getFooterHtml();

Here’s an example file of how you’d include a bunch of JS / CSS using this StaticResources system. It obviously won’t work if you don’t create JS/CSS files in the locations referenced, but this is very simple code to show the system in action.

StaticResources - EXAMPLE USAGE PAGE
PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
 
include 'static.php';
 
// You can add header files any time before the StaticResources::getHeaderHtml() call.
StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");
 
// You can add footer files any time before the StaticResources::getHeaderHtml() call.
StaticResources::addFooterFile("jquery-1.9.1.js");
 
// For files that won't change (like a specific version of jQuery), it's often better to host it on your
// own server instead of making a separate HTTP request to the CDN.
StaticResources::addFooterFile("jquery-ui.js");
 
?><!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>StaticResources example page</title>
        <?php
 
        // You can add header files any time before the StaticResources::getHeaderHtml() call.
        StaticResources::addHeaderFile("jquery-ui.min.css");
 
        // A good place to output the HTML is right at the end of the HEAD tag.
        // getHeaderHtml() returns a string (to be more flexible with various PHP frameworks)
        // so be sure to print its result.
        print StaticResources::getHeaderHtml();
        ?>
    </head>
    <body>
        <header>
            Created for <a href='https://burndownfortrello.com'>Burndown for Trello</a>.
        </header>
 
        <article>
            <h1>StaticResources example page</h1>
            <p>This page is an example of loading static resources in combined files.</p>
        </article>
 
        <footer>
            © 2013 - Sean Colombo
        </footer>
        <?php
 
        // For files that won't be changing
        StaticResources::addFooterFile("stripeIntegration.js");
 
        // To add a file from another site, just make sure to use the full URL.
        // Since there is no file extension, we have to pass in the filetype as a second parameter.
        StaticResources::addFooterFile("https://www.google.com/jsapi", "js");
 
        // Output the footerHtml at the bottom of the page. It doesn't have to be in the FOOTER tag, it should
        // be done as far down on the page as possible (close to the end of the BODY tag).
        print StaticResources::getFooterHtml();
 
        ?>
    </body>
</html>
<?php

include 'static.php';

// You can add header files any time before the StaticResources::getHeaderHtml() call.
StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");

// You can add footer files any time before the StaticResources::getHeaderHtml() call.
StaticResources::addFooterFile("jquery-1.9.1.js");

// For files that won't change (like a specific version of jQuery), it's often better to host it on your
// own server instead of making a separate HTTP request to the CDN.
StaticResources::addFooterFile("jquery-ui.js");

?><!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>StaticResources example page</title>
        <?php

        // You can add header files any time before the StaticResources::getHeaderHtml() call.
        StaticResources::addHeaderFile("jquery-ui.min.css");

        // A good place to output the HTML is right at the end of the HEAD tag.
        // getHeaderHtml() returns a string (to be more flexible with various PHP frameworks)
        // so be sure to print its result.
        print StaticResources::getHeaderHtml();
        ?>
    </head>
    <body>
        <header>
            Created for <a href='https://burndownfortrello.com'>Burndown for Trello</a>.
        </header>

        <article>
            <h1>StaticResources example page</h1>
            <p>This page is an example of loading static resources in combined files.</p>
        </article>

        <footer>
            © 2013 - Sean Colombo
        </footer>
        <?php

        // For files that won't be changing
        StaticResources::addFooterFile("stripeIntegration.js");

        // To add a file from another site, just make sure to use the full URL.
        // Since there is no file extension, we have to pass in the filetype as a second parameter.
        StaticResources::addFooterFile("https://www.google.com/jsapi", "js");

        // Output the footerHtml at the bottom of the page. It doesn't have to be in the FOOTER tag, it should
        // be done as far down on the page as possible (close to the end of the BODY tag).
        print StaticResources::getFooterHtml();

        ?>
    </body>
</html>

The Code

Without further delay, this is the code of static.php that you should put in the main (eg: public_html) directory of your app.
static.php - THE ACTUAL CODE
PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
<?php
/**
* @author Sean Colombo
* @date 20130817
*
* This script is designed to be a generalized (not site-specific) tool
* for including static JS/CSS assets in PHP web-apps.  It's a single file,
* requires no configuration to start using it, and makes delivery of static
* assets significantly faster.
*
* BEST PRACTICE:
* Ideally, any javascript that doesn't NEED to be loaded higher up in the page should be
* loaded in the footer (added using addFooterFile()) so that the page can render while the
* javascript is still being requested from the server.
* However, since the page cannot render properly until the CSS is loaded, any CSS used for the initial
* page-layout should be put into the header. Otherwise the page will start out looking unstyled, then
* will re-flow (flicker or adjust) after the CSS file is loaded.
*
* The performance benefits include:
* - Making less HTTP-requests results in a lot less setup/teardown and latency-time being wasted.
* - Good caching headers by default (makes subsequent page-requests faster since the user won't have to
*   make another request for the file they already have cached).
* - Minification: NOTE: NOT DONE YET - this will make the js/css files significantly smaller which decreases
*   the download-time.
*
* This script should be placed in the root directory of your site.  It is both an includable
* script (for building the list of files) and an endpoint (for actually serving up combined files).
*
* Remote files are currently sent as separate requests from local files - they are not bundled into
* the same package.  However, the implementation details are transparent - no special action needs to
* be taken for remote files. Just add them like you'd add a resource that's on your own server, and as
* long as you include the full URL, they will be handled properly.  Example:
*        // Local and remote files are included the same way.
*        StaticResources::addHeaderFile("js/myFile.js");
*        StaticResources::addHeaderFile("http://cdn.google.com/someOtherFile.js");
*
* == USAGE ==
* To output the header files - YOU MUST ADD THIS CALL. PUT IT RIGHT BEFORE THE END OF THE <HEAD> TAG.
*        print StaticResources::getHeaderHtml();
*
* To output the footer files - YOU MUST ADD THIS CALL. PUT IT RIGHT BEFORE THE END OF THE <BODY> TAG.
*        print StaticResources::getFooterHtml();
*
* To add files to the HEAD tag or to the bottom of the page:
*        StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");
*        StaticResources::addFooterFile("jquery-ui.js");
*        StaticResources::addFooterFile("http://code.jquery.com/ui/1.9.1/jquery-ui.js");
*
* To add a file which doesn't have an extension (eg: ".js") at the end.
*        StaticResources::addFooterFile("https://www.google.com/jsapi", "js");
*
* Optional: to specify that this file should be served from a different URL (eg: a Content Delivery Network).
* Put this right below where you include() this static.php file:
*        StaticResources::setRootUrl("http://cdn.example.com/"); // use a trailing slash
*
*
*
* TODO: Add minification
* TODO: LATER: Add options to allow the files to use HTML5 async and deferred attributes.
*/
 
/**
* For security reasons, we limit the types of files that can be outputted.
* If this wasn't here, someone could request each of your .php files and read all of
* your source-code (and password files, etc.), so please only change this if you understand
* the implications.
*
* NOTE: See STATICRESOURCES_ALLOWED_FILE_TYPES below for the actual array.
*/
 
// Need to define this way (serialized and inside of define()) because arrays can't be class-constants.
define("STATICRESOURCES_ALLOWED_FILE_TYPES", serialize( array( "js", "css" ))); // all types must be lowercase!
 
// Character(s) used in the URL to separate file names. This should be a character that's not
// allowed in filenames, otherwise a real file could be split into two filenames.
// For example: "bob-vila.txt" would be interpreted as "bob" and "vila.txt" if the delimiter was "-".
// There is no perfect result for this (since different characters are allowed on different operating systems).
define("STATICRESOURCES_FILE_DELIMITER", "||");
 
// If this script is being run as the page (rather than just included) then serve up the files
// being requested.
if( $_SERVER['SCRIPT_FILENAME'] == __FILE__ ){
    $files = (isset($_GET['files']) ? $_GET['files'] : "");
    $files = explode(STATICRESOURCES_FILE_DELIMITER, $files); // turn it into an array.
 
    if(count($files) == 0){
        print "/* NOTE: No files found in the URL.\n";
        print "Please specify the file names in the 'file' parameter, separated by \"".STATICRESOURCES_FILE_DELIMITER."\"\n";
        print "See ".basename(__FILE__)." for documentation on how to use this system.\n";
        print "*/\n";
    } else {
        $fileType = ""; // don't know the fileType until we examine the file names.
 
        foreach($files as $fileName){
            $fileName = StaticResources::sanitizeFileName($fileName);
 
            $ext = StaticResources::getFileExtension($fileName);
            if( (!empty($fileType)) && ($ext != $fileType) ){
                // Warn the user that they're mixing file-types.
                print "\n\n/* ===== WARNING: \"$fileName\" is of wrong file type. ";
                print "It is a '$ext' file, but this is a '$fileType' file. Skipped. ===== */\n\n\n\n";
 
                // Don't output this file since it's the wrong type. Skip to next file.
                continue;
            } else {
                // If fileType is still empty, this is the first file encountered.  Set correct headers.
                if(empty($fileType)){
                    $fileType = $ext;
 
                    // Set the content-type headers
                    $allowMimeSniffing = false; // usually, listen to our Content-types instead of sniffing Mime-type. Only exception is when we don't set a Content-type.
                    if($fileType == "css"){
                        header("Content-type: text/css");
                    } else if($fileType == "js"){
                        header("Content-type: application/javascript");
                    } else {
                        $allowMimeSniffing = true;
                        print "/* WARNING: Don't know what Content-type header to set for files with extension: '$fileType' */\n";
                    }
                    if(!$allowMimeSniffing){
                        // Disables IE mime-type sniffing so it always listens to Content-type.
                        header("X-Content-Type-Options: nosniff");
                    }
 
                    // Set reasonable caching headers.
                    // LATER: Could add a configuration array for tweaking the expiration by file-type. This is a reasonable default though.
                    $SECONDS_IN_MONTH = 60*60*24*30;
                    header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + $SECONDS_IN_MONTH)); // needs to be after session_start if session_start is used.
                }
            }
 
            // If there is a query-string, chop that off before looking for the file.
            if(strpos($fileName, "?") !== false){
                $fileName = substr($fileName, 0, strpos($fileName, "?"));
            }
 
            // Read, preprocess (eg: minify), and print the file contents.
            print "/* File: \"$fileName\" */\n";
            @$content = file_get_contents($fileName);
            if($content === false){
                print "/* FILE NOT FOUND */\n\n";
            } else {
 
                // TODO: PRE-PROCESS THE CONTENT.  MINIFICATION, ETC.
                // TODO: PRE-PROCESS THE CONTENT.  MINIFICATION, ETC.
 
                // Prints the content.
                print "$content\n\n";
            }
        }
    }
}
 
/**
* Helper class for creating tags which will import several static resources in a single
* request.
*
* The most common way to interface with this class is through the static functions at the
* bottom of the class (which will all operate on the same singleton).
*/
class StaticResources {
    const ERR_WRONG_FILETYPE = "Error in StaticResources: Tried to add a file with a file-type that isn't allowed.  File had extension '.%s' which is not in ALLOWED_FILE_TYPES in %s\n";
    const ERR_CANT_OUTPUT_FILETYPE = "Error in StaticResources: Don't know how to include code for files of type '%s'\n";
    const ERR_ALREADY_OUTPUT_HEADER = "Error in StaticResources: Tried to add a head file after the head files have already been retrieved. The file that was added late was: \"%s\".";
    const ERR_ALREADY_OUTPUT_FOOTER = "Error in StaticResources: Tried to add a footer file after the footer files have already been retrieved. The file that was added late was: \"%s\".";
 
    private static $ALLOWED_FILE_TYPES; // we have to initialize this in the constructor because arrays can't be class-constants.
    private $staticResourcesRootUrl = "./";
 
    // NOTE: Inclusion-order matters for both js and css, so never sort the array of fileNames... always output them in the order
    // received.
    private $_headerFilesByType = array('js' => array(), 'css' => array());
    private $_footerFilesByType = array('js' => array(), 'css' => array());
 
    private $_headerAlreadyReturned = false;
    private $_footerAlreadyReturned = false;
 
    /**
     * Basic constructor - note that most of the interaction with this class will
     * be done using static functions which grab a singleton.
     */
    public function __construct(){
        self::$ALLOWED_FILE_TYPES = unserialize( STATICRESOURCES_ALLOWED_FILE_TYPES );
 
        foreach(self::$ALLOWED_FILE_TYPES as $type){
            $this->_headerFilesByType[$type] = array();
            $this->_footerFilesByType[$type] = array();
        }
    } // end constructor
 
    /**
     * If your static resources are served from another domain (such as cdn.yoursite.com), then set
     * that here. This string is prepended to the filename (eg: "static.php") when building the URL for the combined-file.
     */
    public function mySetRootUrl($rootUrl){
        $this->staticResourcesRootUrl = $rootUrl;
    }
 
    /**
     * Adds the filename to the list of files to include in the head tag for its given
     * filetype.  If the fileName does not have an extension of one of the ALLOWED_FILE_TYPES,
     * then the file will NOT be added and a warning will be given instead.
     *
     * For these files to be used, the calling code must remember to print the results of
     * getHeaderHtml() in the <head> tag.
     *
     * NOTE: The fileName must be structured relative to where THIS (StaticResources) file is,
     * and should not be in normal URL format, but rather in a format that this file can be read
     * from disk.  So doing "/directory/filename.ext" to get to the root of public_html will not work
     * unless the StaticResources file happens to be in public_html.  If this sounds confusing,
     * the simplest fix is to put this StaticResources file in your public_html directory, then you
     * can write file names exactly the way you would have written them in <script></script> tags.
     *
     * @param fileType - if this is empty, the fileType will be detected from the filename. Sometimes
     * the filename might not have an extension (some JS libraries don't have file extensions) so you may
     * need to specify this value. Should be "js" or "css".
     */
    public function myAddHeaderFile($fileName, $fileType="", $warnIfAddedLate=true){
        // Make sure this file wasn't added after the header files were already displayed.
        if( $warnIfAddedLate && $this->_headerAlreadyReturned){
            $errorString = sprintf(self::ERR_ALREADY_OUTPUT_HEADER, $fileName);
            trigger_error($errorString, E_USER_WARNING);
        }
 
        // If this file type is allowed, then remember it for later.
        if( $this->fileIsAllowed($fileName) ){
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $this->headerFilesByType[ $ext ][] = $fileName;
        } else {
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $errorString = sprintf(self::ERR_WRONG_FILETYPE, $ext, __FILE__);
            trigger_error($errorString, E_USER_WARNING);
        }
    } // end addHeaderFile()
 
    /**
     * Adds the filename to the list of files to include at the bottom of the BODY tag.
     * If the fileName does not have an extension of one of the ALLOWED_FILE_TYPES,
     * then the file will NOT be added and a warning will be given instead.
     *
     * For these files to be used, the calling code must remember to print the results of
     * getFooterHtml() somewhere very late in the document, ideally at the very bottom of
     * the <body> tag (right before </body>).
     *
     * @param fileType - if this is empty, the fileType will be detected from the filename. Sometimes
     * the filename might not have an extension (some JS libraries don't have file extensions) so you may
     * need to specify this value. Should be "js" or "css".
     */
    public function myAddFooterFile($fileName, $fileType="", $warnIfAddedLate=true){
        // Make sure this file wasn't added after the footer files were already displayed.
        if( $warnIfAddedLate && $this->_footerAlreadyReturned){
            $errorString = sprintf(self::ERR_ALREADY_OUTPUT_FOOTER, $fileName);
            trigger_error($errorString, E_USER_WARNING);
        }
 
        // If this file type is allowed, then remember it for later.
        if( $this->fileIsAllowed($fileName) ){
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $this->footerFilesByType[ $ext ][] = $fileName;
        } else {
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $errorString = sprintf(self::ERR_WRONG_FILETYPE, $ext, __FILE__);
            trigger_error($errorString, E_USER_WARNING);
        }
    } // end addFooterFile()
 
    /**
     * Returns a string which contains the HTML that's needed to
     * import the files in the HEAD tag.  This should be called exactly
     * once (and it's results should be printed in the <head> tag) on every
     * page.
     */
    public function myGetHeaderHtml(){
        $html = "<!-- StaticResources::getHeaderHtml() -->\n" . $this->getHtmlForArray( $this->headerFilesByType );
        $_headerAlreadyReturned = true;
        return $html;
    } // end getHeaderHtml()
 
    /**
     * Returns a string which contains the HTML that's needed to
     * import the files at the bottom of the BODY tag.  This should be called exactly
     * once (and it's results should be printed at the bottom of the <body> tag - right
     * before </body>) on every page.
     */
    public function myGetFooterHtml(){
        $html = "<!-- StaticResources::getFooterHtml() -->\n" . $this->getHtmlForArray( $this->footerFilesByType );
        $_footerAlreadyReturned = true;
        return $html;
    } // end getFooterHtml()
 
    /**
     * Given an associative array of static resources whose keys are
     * resource types and whose values are arrays of fileNames (local
     * and/or remote), this will return a string which contains the
     * HTML that's needed to import those files.
     */
    private function getHtmlForArray($filesByType){
        $html = "";
 
        // The URL of this script (which will act as an endpoint and serve up the actual content.
        $url = $this->staticResourcesRootUrl . basename(__FILE__);
        foreach($filesByType as $fileType => $files){
            $localFiles = array();
 
            foreach($files as $fileName){
                if(StaticResources::isRemoteFile($fileName)){
                    // Add the HTML for including the remote file.
                    if($fileType == "css"){
                        $html .= "        <link rel=\"stylesheet\" href=\"$fileName\"/>\n";
                    } else if($fileType == "js"){
                        $html .= "        <script src=\"$fileName\"></script>\n";
                    } else {
                        // Each file type needs to be included a certain way, and we don't recognize this fileType.
                        $errorString = sprintf(self::ERR_CANT_OUTPUT_FILETYPE, $fileType);
                        trigger_error($errorString, E_USER_WARNING);
                    }
                } else {
                    $localFiles[] = $fileName;
                }
            }
 
            // Output the HTML which makes the request for the combined-file of all local files of the same fileType.
            if(count($localFiles) > 0){
 
                // TODO: TWEAK SO THAT THE DELIMITER ISN'T URL-ENCODED. MAKES IT MORE READABLE AND SHORTER.
                // TODO: TWEAK SO THAT THE DELIMITER ISN'T URL-ENCODED. MAKES IT MORE READABLE AND SHORTER.
 
                $fullUrl = $url . "?files=".rawurlencode( implode(STATICRESOURCES_FILE_DELIMITER, $localFiles) );
 
                if($fileType == "css"){
                    $html .= "        <link rel=\"stylesheet\" href=\"$fullUrl\"/>\n";
                } else if($fileType == "js"){
                    $html .= "        <script src=\"$fullUrl\"></script>\n";
                } else {
                    // Each file type needs to be included a certain way, and we don't recognize this fileType.
                    $errorString = sprintf(self::ERR_CANT_OUTPUT_FILETYPE, $fileType);
                    trigger_error($errorString, E_USER_WARNING);
                }
            }
        }
 
        return $html;
    } // end getHtmlForArray()
 
    /**
     * Returns true if the given fileName is allowed to be included, false otherwise.
     * The reason a file may not be allowed is that it's of the wrong file-type. One
     * reason for this is that we don't want attackers to request file types that may
     * contain password-files or source code that some users of this script might not
     * want to make public, etc..
     */
    private function fileIsAllowed($fileName){
        $fileIsAllowed = true;
        if( !StaticResources::isRemoteFile($fileName)){
            $fileExtension = strtolower( StaticResources::getFileExtension($fileName) );
            $fileIsAllowed = in_array($fileExtension, self::$ALLOWED_FILE_TYPES);
        }
        return $fileIsAllowed;
    } // end fileIsAllowed()
 
    /**
     * Returns true if the fileName is from another site and false if it is from this site.
     */
    public static function isRemoteFile($fileName){
        // If it starts with a protocol (ftp://, http://, https://, etc.) then it is remote (not local).
        return (0 <  preg_match("/^[a-z0-9]+:\/\//i", $fileName));
    }
 
    /**
     * If the 'fileName' is a local file, this will return that fileName in such a way
     * that the fileName can be loaded from disk safely (without allowing the user to
     * jump out of the current directory with "../" or absolute directories such
     * as "/usr/bin/").
     */
    public static function sanitizeFileName($fileName){
        // Only local files need to be sanitized.
        if( !StaticResources::isRemoteFile($fileName)){
            // Make sure the user can't get above the current directory using "../".
            while(strpos($fileName, "../") !== false){
                $fileName = str_replace("../", "", $fileName);
            }
 
            // Starting out with current directory avoids abusing absolute paths such as "/usr/bin"
            if(strpos($fileName, "./") !== 0){ // if path already starts with ./, don't duplicate it.
                if(strpos($fileName, "/") === 0){ // path already starts with "/", just turn it into "./".
                    $fileName = ".$fileName";
                } else {
                    $fileName = "./$fileName"; // all other paths that start 'normally' (not "./" or "/").
                }
            }
        }
 
        return $fileName;
    }
 
    public static function getFileExtension($fileName){
        // If there is a query-string, chop that off before looking for the extension.
        if(strpos($fileName, "?") !== false){
            $fileName = substr($fileName, 0, strpos($fileName, "?"));
        }
 
        return pathinfo($fileName, PATHINFO_EXTENSION);
    }
 
    // ----- STATIC HELPERS -----
    /**
     * Gets a singleton object of the StaticResources type to make it easy for the
     * script to use StaticResources throughout the web-app without passing the object
     * around.  This is probably the most common use-case.
     */
    public static function getSingleton(){
        global $staticResourcesSingleton;
        if(empty($staticResourcesSingleton)){
            $staticResourcesSingleton = new StaticResources();
        }
        return $staticResourcesSingleton;
    } // end getSingleton()
 
    public static function addHeaderFile($fileName, $fileType="", $warnIfLateAdded=true){
        $singleton = StaticResources::getSingleton();
        $singleton->myAddHeaderFile($fileName, $fileType, $warnIfLateAdded);
    }
    public static function addFooterFile($fileName, $fileType="", $warnIfLateAdded=true){
        $singleton = StaticResources::getSingleton();
        $singleton->myAddFooterFile($fileName, $fileType, $warnIfLateAdded);
    }
    public static function getHeaderHtml(){
        $singleton = StaticResources::getSingleton();
        return $singleton->myGetHeaderHtml();
    }
    public static function getFooterHtml(){
        $singleton = StaticResources::getSingleton();
        return $singleton->myGetFooterHtml();
    }
    public static function setRootUrl( $rootUrl ){
        $singleton = StaticResources::getSingleton();
        $singleton->mySetRootUrl($rootUrl);
    }
 
} // end class StaticResources
<?php
/**
* @author Sean Colombo
* @date 20130817
*
* This script is designed to be a generalized (not site-specific) tool
* for including static JS/CSS assets in PHP web-apps.  It's a single file,
* requires no configuration to start using it, and makes delivery of static
* assets significantly faster.
*
* BEST PRACTICE:
* Ideally, any javascript that doesn't NEED to be loaded higher up in the page should be
* loaded in the footer (added using addFooterFile()) so that the page can render while the
* javascript is still being requested from the server.
* However, since the page cannot render properly until the CSS is loaded, any CSS used for the initial
* page-layout should be put into the header. Otherwise the page will start out looking unstyled, then
* will re-flow (flicker or adjust) after the CSS file is loaded.
*
* The performance benefits include:
* - Making less HTTP-requests results in a lot less setup/teardown and latency-time being wasted.
* - Good caching headers by default (makes subsequent page-requests faster since the user won't have to
*   make another request for the file they already have cached).
* - Minification: NOTE: NOT DONE YET - this will make the js/css files significantly smaller which decreases
*   the download-time.
*
* This script should be placed in the root directory of your site.  It is both an includable
* script (for building the list of files) and an endpoint (for actually serving up combined files).
*
* Remote files are currently sent as separate requests from local files - they are not bundled into
* the same package.  However, the implementation details are transparent - no special action needs to
* be taken for remote files. Just add them like you'd add a resource that's on your own server, and as
* long as you include the full URL, they will be handled properly.  Example:
*        // Local and remote files are included the same way.
*        StaticResources::addHeaderFile("js/myFile.js");
*        StaticResources::addHeaderFile("http://cdn.google.com/someOtherFile.js");
*
* == USAGE ==
* To output the header files - YOU MUST ADD THIS CALL. PUT IT RIGHT BEFORE THE END OF THE <HEAD> TAG.
*        print StaticResources::getHeaderHtml();
*
* To output the footer files - YOU MUST ADD THIS CALL. PUT IT RIGHT BEFORE THE END OF THE <BODY> TAG.
*        print StaticResources::getFooterHtml();
*
* To add files to the HEAD tag or to the bottom of the page:
*        StaticResources::addHeaderFile("css/GoogleWebfont_Tauri_Latin.css");
*        StaticResources::addFooterFile("jquery-ui.js");
*        StaticResources::addFooterFile("http://code.jquery.com/ui/1.9.1/jquery-ui.js");
*
* To add a file which doesn't have an extension (eg: ".js") at the end.
*        StaticResources::addFooterFile("https://www.google.com/jsapi", "js");
*
* Optional: to specify that this file should be served from a different URL (eg: a Content Delivery Network).
* Put this right below where you include() this static.php file:
*        StaticResources::setRootUrl("http://cdn.example.com/"); // use a trailing slash
*
*
*
* TODO: Add minification
* TODO: LATER: Add options to allow the files to use HTML5 async and deferred attributes.
*/

/**
* For security reasons, we limit the types of files that can be outputted.
* If this wasn't here, someone could request each of your .php files and read all of
* your source-code (and password files, etc.), so please only change this if you understand
* the implications.
*
* NOTE: See STATICRESOURCES_ALLOWED_FILE_TYPES below for the actual array.
*/

// Need to define this way (serialized and inside of define()) because arrays can't be class-constants.
define("STATICRESOURCES_ALLOWED_FILE_TYPES", serialize( array( "js", "css" ))); // all types must be lowercase!

// Character(s) used in the URL to separate file names. This should be a character that's not
// allowed in filenames, otherwise a real file could be split into two filenames.
// For example: "bob-vila.txt" would be interpreted as "bob" and "vila.txt" if the delimiter was "-".
// There is no perfect result for this (since different characters are allowed on different operating systems).
define("STATICRESOURCES_FILE_DELIMITER", "||");

// If this script is being run as the page (rather than just included) then serve up the files
// being requested.
if( $_SERVER['SCRIPT_FILENAME'] == __FILE__ ){
    $files = (isset($_GET['files']) ? $_GET['files'] : "");
    $files = explode(STATICRESOURCES_FILE_DELIMITER, $files); // turn it into an array.

    if(count($files) == 0){
        print "/* NOTE: No files found in the URL.\n";
        print "Please specify the file names in the 'file' parameter, separated by \"".STATICRESOURCES_FILE_DELIMITER."\"\n";
        print "See ".basename(__FILE__)." for documentation on how to use this system.\n";
        print "*/\n";
    } else {
        $fileType = ""; // don't know the fileType until we examine the file names.

        foreach($files as $fileName){
            $fileName = StaticResources::sanitizeFileName($fileName);

            $ext = StaticResources::getFileExtension($fileName);
            if( (!empty($fileType)) && ($ext != $fileType) ){
                // Warn the user that they're mixing file-types.
                print "\n\n/* ===== WARNING: \"$fileName\" is of wrong file type. ";
                print "It is a '$ext' file, but this is a '$fileType' file. Skipped. ===== */\n\n\n\n";

                // Don't output this file since it's the wrong type. Skip to next file.
                continue;
            } else {
                // If fileType is still empty, this is the first file encountered.  Set correct headers.
                if(empty($fileType)){
                    $fileType = $ext;

                    // Set the content-type headers
                    $allowMimeSniffing = false; // usually, listen to our Content-types instead of sniffing Mime-type. Only exception is when we don't set a Content-type.
                    if($fileType == "css"){
                        header("Content-type: text/css");
                    } else if($fileType == "js"){
                        header("Content-type: application/javascript");
                    } else {
                        $allowMimeSniffing = true;
                        print "/* WARNING: Don't know what Content-type header to set for files with extension: '$fileType' */\n";
                    }
                    if(!$allowMimeSniffing){
                        // Disables IE mime-type sniffing so it always listens to Content-type.
                        header("X-Content-Type-Options: nosniff");
                    }

                    // Set reasonable caching headers.
                    // LATER: Could add a configuration array for tweaking the expiration by file-type. This is a reasonable default though.
                    $SECONDS_IN_MONTH = 60*60*24*30;
                    header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + $SECONDS_IN_MONTH)); // needs to be after session_start if session_start is used.
                }
            }

            // If there is a query-string, chop that off before looking for the file.
            if(strpos($fileName, "?") !== false){
                $fileName = substr($fileName, 0, strpos($fileName, "?"));
            }

            // Read, preprocess (eg: minify), and print the file contents.
            print "/* File: \"$fileName\" */\n";
            @$content = file_get_contents($fileName);
            if($content === false){
                print "/* FILE NOT FOUND */\n\n";
            } else {

                // TODO: PRE-PROCESS THE CONTENT.  MINIFICATION, ETC.
                // TODO: PRE-PROCESS THE CONTENT.  MINIFICATION, ETC.

                // Prints the content.
                print "$content\n\n";
            }
        }
    }
}

/**
* Helper class for creating tags which will import several static resources in a single
* request.
*
* The most common way to interface with this class is through the static functions at the
* bottom of the class (which will all operate on the same singleton).
*/
class StaticResources {
    const ERR_WRONG_FILETYPE = "Error in StaticResources: Tried to add a file with a file-type that isn't allowed.  File had extension '.%s' which is not in ALLOWED_FILE_TYPES in %s\n";
    const ERR_CANT_OUTPUT_FILETYPE = "Error in StaticResources: Don't know how to include code for files of type '%s'\n";
    const ERR_ALREADY_OUTPUT_HEADER = "Error in StaticResources: Tried to add a head file after the head files have already been retrieved. The file that was added late was: \"%s\".";
    const ERR_ALREADY_OUTPUT_FOOTER = "Error in StaticResources: Tried to add a footer file after the footer files have already been retrieved. The file that was added late was: \"%s\".";

    private static $ALLOWED_FILE_TYPES; // we have to initialize this in the constructor because arrays can't be class-constants.
    private $staticResourcesRootUrl = "./";

    // NOTE: Inclusion-order matters for both js and css, so never sort the array of fileNames... always output them in the order
    // received.
    private $_headerFilesByType = array('js' => array(), 'css' => array());
    private $_footerFilesByType = array('js' => array(), 'css' => array());

    private $_headerAlreadyReturned = false;
    private $_footerAlreadyReturned = false;

    /**
     * Basic constructor - note that most of the interaction with this class will
     * be done using static functions which grab a singleton.
     */
    public function __construct(){
        self::$ALLOWED_FILE_TYPES = unserialize( STATICRESOURCES_ALLOWED_FILE_TYPES );

        foreach(self::$ALLOWED_FILE_TYPES as $type){
            $this->_headerFilesByType[$type] = array();
            $this->_footerFilesByType[$type] = array();
        }
    } // end constructor

    /**
     * If your static resources are served from another domain (such as cdn.yoursite.com), then set
     * that here. This string is prepended to the filename (eg: "static.php") when building the URL for the combined-file.
     */
    public function mySetRootUrl($rootUrl){
        $this->staticResourcesRootUrl = $rootUrl;
    }

    /**
     * Adds the filename to the list of files to include in the head tag for its given
     * filetype.  If the fileName does not have an extension of one of the ALLOWED_FILE_TYPES,
     * then the file will NOT be added and a warning will be given instead.
     *
     * For these files to be used, the calling code must remember to print the results of
     * getHeaderHtml() in the <head> tag.
     *
     * NOTE: The fileName must be structured relative to where THIS (StaticResources) file is,
     * and should not be in normal URL format, but rather in a format that this file can be read
     * from disk.  So doing "/directory/filename.ext" to get to the root of public_html will not work
     * unless the StaticResources file happens to be in public_html.  If this sounds confusing,
     * the simplest fix is to put this StaticResources file in your public_html directory, then you
     * can write file names exactly the way you would have written them in <script></script> tags.
     *
     * @param fileType - if this is empty, the fileType will be detected from the filename. Sometimes
     * the filename might not have an extension (some JS libraries don't have file extensions) so you may
     * need to specify this value. Should be "js" or "css".
     */
    public function myAddHeaderFile($fileName, $fileType="", $warnIfAddedLate=true){
        // Make sure this file wasn't added after the header files were already displayed.
        if( $warnIfAddedLate && $this->_headerAlreadyReturned){
            $errorString = sprintf(self::ERR_ALREADY_OUTPUT_HEADER, $fileName);
            trigger_error($errorString, E_USER_WARNING);
        }

        // If this file type is allowed, then remember it for later.
        if( $this->fileIsAllowed($fileName) ){
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $this->headerFilesByType[ $ext ][] = $fileName;
        } else {
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $errorString = sprintf(self::ERR_WRONG_FILETYPE, $ext, __FILE__);
            trigger_error($errorString, E_USER_WARNING);
        }
    } // end addHeaderFile()

    /**
     * Adds the filename to the list of files to include at the bottom of the BODY tag.
     * If the fileName does not have an extension of one of the ALLOWED_FILE_TYPES,
     * then the file will NOT be added and a warning will be given instead.
     *
     * For these files to be used, the calling code must remember to print the results of
     * getFooterHtml() somewhere very late in the document, ideally at the very bottom of
     * the <body> tag (right before </body>).
     *
     * @param fileType - if this is empty, the fileType will be detected from the filename. Sometimes
     * the filename might not have an extension (some JS libraries don't have file extensions) so you may
     * need to specify this value. Should be "js" or "css".
     */
    public function myAddFooterFile($fileName, $fileType="", $warnIfAddedLate=true){
        // Make sure this file wasn't added after the footer files were already displayed.
        if( $warnIfAddedLate && $this->_footerAlreadyReturned){
            $errorString = sprintf(self::ERR_ALREADY_OUTPUT_FOOTER, $fileName);
            trigger_error($errorString, E_USER_WARNING);
        }

        // If this file type is allowed, then remember it for later.
        if( $this->fileIsAllowed($fileName) ){
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $this->footerFilesByType[ $ext ][] = $fileName;
        } else {
            $ext = (empty($fileType) ? StaticResources::getFileExtension($fileName) : strtolower($fileType));
            $errorString = sprintf(self::ERR_WRONG_FILETYPE, $ext, __FILE__);
            trigger_error($errorString, E_USER_WARNING);
        }
    } // end addFooterFile()

    /**
     * Returns a string which contains the HTML that's needed to
     * import the files in the HEAD tag.  This should be called exactly
     * once (and it's results should be printed in the <head> tag) on every
     * page.
     */
    public function myGetHeaderHtml(){
        $html = "<!-- StaticResources::getHeaderHtml() -->\n" . $this->getHtmlForArray( $this->headerFilesByType );
        $_headerAlreadyReturned = true;
        return $html;
    } // end getHeaderHtml()

    /**
     * Returns a string which contains the HTML that's needed to
     * import the files at the bottom of the BODY tag.  This should be called exactly
     * once (and it's results should be printed at the bottom of the <body> tag - right
     * before </body>) on every page.
     */
    public function myGetFooterHtml(){
        $html = "<!-- StaticResources::getFooterHtml() -->\n" . $this->getHtmlForArray( $this->footerFilesByType );
        $_footerAlreadyReturned = true;
        return $html;
    } // end getFooterHtml()

    /**
     * Given an associative array of static resources whose keys are
     * resource types and whose values are arrays of fileNames (local
     * and/or remote), this will return a string which contains the
     * HTML that's needed to import those files.
     */
    private function getHtmlForArray($filesByType){
        $html = "";

        // The URL of this script (which will act as an endpoint and serve up the actual content.
        $url = $this->staticResourcesRootUrl . basename(__FILE__);
        foreach($filesByType as $fileType => $files){
            $localFiles = array();

            foreach($files as $fileName){
                if(StaticResources::isRemoteFile($fileName)){
                    // Add the HTML for including the remote file.
                    if($fileType == "css"){
                        $html .= "        <link rel=\"stylesheet\" href=\"$fileName\"/>\n";
                    } else if($fileType == "js"){
                        $html .= "        <script src=\"$fileName\"></script>\n";
                    } else {
                        // Each file type needs to be included a certain way, and we don't recognize this fileType.
                        $errorString = sprintf(self::ERR_CANT_OUTPUT_FILETYPE, $fileType);
                        trigger_error($errorString, E_USER_WARNING);
                    }
                } else {
                    $localFiles[] = $fileName;
                }
            }

            // Output the HTML which makes the request for the combined-file of all local files of the same fileType.
            if(count($localFiles) > 0){

                // TODO: TWEAK SO THAT THE DELIMITER ISN'T URL-ENCODED. MAKES IT MORE READABLE AND SHORTER.
                // TODO: TWEAK SO THAT THE DELIMITER ISN'T URL-ENCODED. MAKES IT MORE READABLE AND SHORTER.

                $fullUrl = $url . "?files=".rawurlencode( implode(STATICRESOURCES_FILE_DELIMITER, $localFiles) );

                if($fileType == "css"){
                    $html .= "        <link rel=\"stylesheet\" href=\"$fullUrl\"/>\n";
                } else if($fileType == "js"){
                    $html .= "        <script src=\"$fullUrl\"></script>\n";
                } else {
                    // Each file type needs to be included a certain way, and we don't recognize this fileType.
                    $errorString = sprintf(self::ERR_CANT_OUTPUT_FILETYPE, $fileType);
                    trigger_error($errorString, E_USER_WARNING);
                }
            }
        }

        return $html;
    } // end getHtmlForArray()

    /**
     * Returns true if the given fileName is allowed to be included, false otherwise.
     * The reason a file may not be allowed is that it's of the wrong file-type. One
     * reason for this is that we don't want attackers to request file types that may
     * contain password-files or source code that some users of this script might not
     * want to make public, etc..
     */
    private function fileIsAllowed($fileName){
        $fileIsAllowed = true;
        if( !StaticResources::isRemoteFile($fileName)){
            $fileExtension = strtolower( StaticResources::getFileExtension($fileName) );
            $fileIsAllowed = in_array($fileExtension, self::$ALLOWED_FILE_TYPES);
        }
        return $fileIsAllowed;
    } // end fileIsAllowed()

    /**
     * Returns true if the fileName is from another site and false if it is from this site.
     */
    public static function isRemoteFile($fileName){
        // If it starts with a protocol (ftp://, http://, https://, etc.) then it is remote (not local).
        return (0 <  preg_match("/^[a-z0-9]+:\/\//i", $fileName));
    }

    /**
     * If the 'fileName' is a local file, this will return that fileName in such a way
     * that the fileName can be loaded from disk safely (without allowing the user to
     * jump out of the current directory with "../" or absolute directories such
     * as "/usr/bin/").
     */
    public static function sanitizeFileName($fileName){
        // Only local files need to be sanitized.
        if( !StaticResources::isRemoteFile($fileName)){
            // Make sure the user can't get above the current directory using "../".
            while(strpos($fileName, "../") !== false){
                $fileName = str_replace("../", "", $fileName);
            }

            // Starting out with current directory avoids abusing absolute paths such as "/usr/bin"
            if(strpos($fileName, "./") !== 0){ // if path already starts with ./, don't duplicate it.
                if(strpos($fileName, "/") === 0){ // path already starts with "/", just turn it into "./".
                    $fileName = ".$fileName";
                } else {
                    $fileName = "./$fileName"; // all other paths that start 'normally' (not "./" or "/").
                }
            }
        }

        return $fileName;
    }

    public static function getFileExtension($fileName){
        // If there is a query-string, chop that off before looking for the extension.
        if(strpos($fileName, "?") !== false){
            $fileName = substr($fileName, 0, strpos($fileName, "?"));
        }

        return pathinfo($fileName, PATHINFO_EXTENSION);
    }

    // ----- STATIC HELPERS -----
    /**
     * Gets a singleton object of the StaticResources type to make it easy for the
     * script to use StaticResources throughout the web-app without passing the object
     * around.  This is probably the most common use-case.
     */
    public static function getSingleton(){
        global $staticResourcesSingleton;
        if(empty($staticResourcesSingleton)){
            $staticResourcesSingleton = new StaticResources();
        }
        return $staticResourcesSingleton;
    } // end getSingleton()

    public static function addHeaderFile($fileName, $fileType="", $warnIfLateAdded=true){
        $singleton = StaticResources::getSingleton();
        $singleton->myAddHeaderFile($fileName, $fileType, $warnIfLateAdded);
    }
    public static function addFooterFile($fileName, $fileType="", $warnIfLateAdded=true){
        $singleton = StaticResources::getSingleton();
        $singleton->myAddFooterFile($fileName, $fileType, $warnIfLateAdded);
    }
    public static function getHeaderHtml(){
        $singleton = StaticResources::getSingleton();
        return $singleton->myGetHeaderHtml();
    }
    public static function getFooterHtml(){
        $singleton = StaticResources::getSingleton();
        return $singleton->myGetFooterHtml();
    }
    public static function setRootUrl( $rootUrl ){
        $singleton = StaticResources::getSingleton();
        $singleton->mySetRootUrl($rootUrl);
    }

} // end class StaticResources

Using this script with a CDN

One drawback of serving a combined file is that your server has to read in all of the files, then print them all. This makes the call slightly slower than it has to be. One solution is to use a CDN to cache the files on your site. This way, even if it takes your server a second or so to generate the file the first time, it can be served to the next user instantly, from the CDN.

For Burndown for Trello, we use Fastly as our CDN (disclosure: my fiance works there, but we use it because it’s free for one backend and it’s crazy fast).

The code to make this work with a CDN is very simple: one extra line below the include call:

How to serve the static assets from a CDN
PHP

1
2
include 'static.php';
StaticResources::setRootUrl("http://b4t.global.ssl.fastly.net/");
include 'static.php';
StaticResources::setRootUrl("http://b4t.global.ssl.fastly.net/");

Code minification

Another common performance trick is to use a minifier to reduce file-size. This hasn’t been added to the script yet, but may come in the future. If you have a favorite minifier, I’ve conspicuously commented in the code where it would make sense to run the minifier.

Minification takes a little while to run, so it’s highly recommended that you get a CDN if you’re using minificaiton.

The End

Hope you found the script useful. If you use it on your site, please link to it in the comments!
Thanks,

参考文章:

1. http://bluelinegamestudios.com/blog/posts/simple-single-file-php-script-for-combining-js-css-assets

给我留言

留言无头像?


×
腾讯微博