├── README ├── cb.php ├── combo.php ├── cssmin.php ├── jsmin.php └── minify.php /README: -------------------------------------------------------------------------------- 1 | Combo Script - 自动合并/压缩脚本 2 | 3 | * Created by 李晶-拔赤(lijing00333@163.com) 4 | * License: http://www.opensource.org/licenses/mit-license.php 5 | 6 | 脚本使用: 7 | - 要求php5及以上版本 8 | - 程序在找不到本地文件的情况下,会去指定的cdn上找同名文件 9 | - 程序会自动转义-min文件为源文件,因此要约定-min文件和原文件要成对出现 10 | - 需要定义combo.php和minify.php中的$YOUR_CDN变量 11 | - 如果只是合并压缩local文件,则不必重置$YOUR_CDN变量 12 | - 这里提供cb.php,用来实现tbcdn的开发环境的模拟,apache的配置在cb.php中 13 | 14 | 合并文件 15 | - http://yourdomain.com/combo.php?app/js/1.js&app/js/2.js 16 | 17 | 合并压缩 18 | - http://yourdomain.com/minify.php?app/js/1.js&app/js/2.js 19 | 20 | 模拟淘宝CDN 21 | - http://a.tbcdn.cn/??1.js,2.js 22 | - http://a.tbcdn.cn/subdir/??1/js,2.js 23 | 24 | 文件列表: 25 | - combo.php 合并文件,不压缩 26 | - minify.php 合并压缩文件 27 | - cssmin.php 压缩css 28 | - jsmin.php 压缩js 29 | - cb.php 淘宝CDN合并文件策略的模拟 30 | 31 | php文件编码统一使用utf-8 32 | 33 | -------------------------------------------------------------------------------- /cb.php: -------------------------------------------------------------------------------- 1 | 'Content-Type: application/x-javascript', 49 | 'css' => 'Content-Type: text/css', 50 | 'jpg' => 'Content-Type: image/jpg', 51 | 'gif' => 'Content-Type: image/gif', 52 | 'png' => 'Content-Type: image/png', 53 | 'jpeg' => 'Content-Type: image/jpeg', 54 | 'swf' => 'Content-Type: application/x-shockwave-flash' 55 | ); 56 | 57 | //文件类型 58 | $type = ''; 59 | 60 | //原始请求文件完整路径数组 61 | $files = array(); 62 | 63 | //原始请求文件相对路径数组 64 | $tmp_files = array(); 65 | 66 | //过滤后的文件完整路径数组,即待抓取的文件列表 67 | $a_files = array(); 68 | 69 | //文件的最后修改时间 70 | $last_modified_time = 0; 71 | 72 | //获得当前目录,比如,/home/a.tbcdn.cn/ 73 | $pwd = getcwd().'/'; 74 | 75 | $request_headers = getallheaders(); 76 | 77 | // 输出结果使用的数组 78 | $R_files = array(); 79 | 80 | //得到请求的前缀 81 | $prefix = $_SERVER['SCRIPT_NAME']; 82 | 83 | // 处理请求中附带的文件列表,得到原始数据 84 | $split_a= explode("??",$_SERVER['REQUEST_URI']); 85 | $tmp_files = explode(",",$split_a[1]); 86 | 87 | if(preg_match('/,/',$split_a[1])){ 88 | $_tmp = explode(',',$split_a[1]); 89 | foreach($_tmp as $v){ 90 | $files[] = $prefix.$v; 91 | } 92 | }else{//单文件 93 | $files[] = $prefix.$split_a[1]; 94 | } 95 | 96 | // 得到需要读取的文件列表 97 | foreach ($files as $k){ 98 | //将开头的/和?去掉 99 | $k = preg_replace( 100 | array('/^\//','/\?.+$/'), 101 | array('',''), 102 | $k); 103 | 104 | if(!preg_match('/(\.js|\.css)$/',$k)){ 105 | continue; 106 | } 107 | while(preg_match('/[^\/]+\/\.\.\//',$k)){ 108 | $k = preg_replace( 109 | array('/[^\/]+\/\.\.\//'), 110 | array(''), 111 | $k,1); 112 | } 113 | 114 | $a_files[] = $k; 115 | } 116 | 117 | // 得到拼接文件的Last-Modified时间 118 | foreach ($a_files as $k){ 119 | if(file_exists($k)){ 120 | $filemtime = filemtime($k); 121 | if($filemtime && ($filemtime > $last_modified_time)){ 122 | $last_modified_time = $filemtime; 123 | } 124 | } 125 | } 126 | 127 | // 检查请求头的if-modified-since 128 | if (isset($request_headers['If-Modified-Since']) && 129 | (strtotime($request_headers['If-Modified-Since']) == $last_modified_time)) { 130 | // 如果客户端带有缓存 131 | header('Last-Modified: '.gmdate('D, d M Y H:i:s', $last_modified_time.' GMT'), true, 304); 132 | exit; 133 | } 134 | 135 | // 拼接文件,并应用通用规则 136 | foreach ($a_files as $k) { 137 | 138 | if(empty($type)) { 139 | $type = get_extend($k); 140 | } 141 | 142 | //文件存在 143 | if(file_exists($k)) { 144 | $R_files[] = file_get_contents($k); 145 | }else{ 146 | //文件不存在 147 | try{ 148 | $R_files[] = '/***** combined from productServer: http://a.tbcdn.cn/'.$k.' *****/'; 149 | $tfile = file($CDN.$k); 150 | if(!$tfile){ 151 | $unfound[] = 'http://a.tbcdn.cn/'.$k; 152 | } 153 | $R_files[] = join('',$tfile); 154 | //如果apache不支持file抓取远程文件,打开这个注释 155 | //$R_files[] = join('', get_contents($CDN.$k)); 156 | }catch(Exception $e){} 157 | } 158 | } 159 | 160 | header("Expires: " . date("D, j M Y H:i:s", strtotime("now + 10 years")) ." GMT"); 161 | header("Cache-Control: max-age=315360000"); 162 | header('Last-Modified: '.gmdate('D, d M Y H:i:s', $last_modified_time).' GMT'); 163 | header($header[$type]); 164 | $result = join("\n",$R_files); 165 | echo $result; 166 | echo "/* non published files:\n"; 167 | echo join("\n",$unfound); 168 | echo "\n*/"; 169 | ?> 170 | -------------------------------------------------------------------------------- /combo.php: -------------------------------------------------------------------------------- 1 | 'Content-Type: application/x-javascript', 49 | 'css' => 'Content-Type: text/css', 50 | 'jpg' => 'Content-Type: image/jpg', 51 | 'gif' => 'Content-Type: image/gif', 52 | 'png' => 'Content-Type: image/png', 53 | 'jpeg' => 'Content-Type: image/jpeg', 54 | 'swf' => 'Content-Type: application/x-shockwave-flash' 55 | ); 56 | $type = ''; 57 | foreach ($_REQUEST as $k => $v) { 58 | //最常见的替换规则 59 | $k = preg_replace( 60 | array('/_(js|css|gif|png|jpg|jpeg|swf)$/','/yui\/2_8_0r4/','/yui\/3_0_0/','/(\d+)_(\d+)_(\d+)/','/(\d+)_(\d+)/','/_v(\d)/'), 61 | array('.$1','yui/2.8.0r4','yui/3.0.0','$1.$2.$3','$1.$2','.v$1'), 62 | trim($k,'/') 63 | ); 64 | //在这里添加转换过头的各种情况 65 | $k = str_replace('global.v5.css','global_v5.css',$k); 66 | $k = str_replace('detail.v2.css','detail_v2.css',$k); 67 | $k = str_replace('cubee_combo','cubee.combo',$k); 68 | 69 | if(empty($type)) { 70 | $type = get_extend($k); 71 | } 72 | //文件存在 73 | if(file_exists($k)) { 74 | $in_str = file_get_contents($k); 75 | //处理文本 76 | if(preg_match('/js|css/',$type)){ 77 | //$files[] = file_get_contents($k); 78 | if($MINIFY == true && $type == 'js'){ 79 | $files[] = JSMin::minify($in_str); 80 | }else if($MINIFY == true && $type == 'css'){ 81 | $files[] = cssmin::minify($in_str); 82 | }else{ 83 | $files[] = $in_str; 84 | } 85 | }else{ 86 | //处理非文本 87 | $files[] = array($in_str); 88 | } 89 | }else{ 90 | //文件不存在 91 | $in_sta = file($YOUR_CDN.$k); 92 | //文本的处理 93 | if(preg_match('/js|css/',$type)){ 94 | $files[] = '/* '.$k.' */'; 95 | $inner_str = join('',$in_sta); 96 | if($MINIFY == true && $type == 'js'){ 97 | $files[] = JSMin::minify($inner_str); 98 | }else if($MINIFY == true && $type == 'css'){ 99 | $files[] = cssmin::minify($inner_str); 100 | }else{ 101 | $files[] = $inner_str; 102 | } 103 | }else{ 104 | //非文本的处理 105 | $files[] = $in_sta; 106 | } 107 | } 108 | } 109 | 110 | header("Expires: " . date("D, j M Y H:i:s", strtotime("now + 10 years")) ." GMT"); 111 | //文本的处理 112 | header($header[$type]);//文件类型 113 | if(preg_match('/js|css/',$type)){ 114 | $result = join("\n",$files); 115 | }else{ 116 | //非文本的处理 117 | $result = join("",$files[0]); 118 | } 119 | cache(md5($result));//etag,处理Etag是否多余? 120 | echo $result; 121 | ?> 122 | -------------------------------------------------------------------------------- /cssmin.php: -------------------------------------------------------------------------------- 1 | 7 | * include("cssmin.php"); 8 | * file_put_contents("path/to/target.css", cssmin::minify(file_get_contents("path/to/source.css"))); 9 | * 10 | * -- 11 | * 12 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | * -- 18 | * 19 | * @package cssmin 20 | * @author Joe Scylla 21 | * @copyright 2008 Joe Scylla 22 | * @license http://opensource.org/licenses/mit-license.php MIT License 23 | * @version 1.0 (2008-01-31) 24 | */ 25 | class cssmin 26 | { 27 | /** 28 | * Minifies stylesheet definitions 29 | * 30 | * @param string $v Stylesheet definitions as string 31 | * @return string Minified stylesheet definitions 32 | */ 33 | public static function minify($v) 34 | { 35 | $v = trim($v); 36 | $v = str_replace("\r\n", "\n", $v); 37 | $search = array("/\/\*[\d\D]*?\*\/|\t+/", "/\s+/", "/\}\s+/"); 38 | $replace = array(null, " ", "}\n"); 39 | $v = preg_replace($search, $replace, $v); 40 | $search = array("/\\;\s/", "/\s+\{\\s+/", "/\\:\s+\\#/", "/,\s+/i", "/\\:\s+\\\'/i", "/\\:\s+([0-9]+|[A-F]+)/i"); 41 | $replace = array(";", "{", ":#", ",", ":\'", ":$1"); 42 | $v = preg_replace($search, $replace, $v); 43 | $v = str_replace("\n", null, $v); 44 | return $v; 45 | } 46 | } 47 | ?> -------------------------------------------------------------------------------- /jsmin.php: -------------------------------------------------------------------------------- 1 | 41 | * @copyright 2002 Douglas Crockford (jsmin.c) 42 | * @copyright 2008 Ryan Grove (PHP port) 43 | * @license http://opensource.org/licenses/mit-license.php MIT License 44 | * @version 1.1.1 (2008-03-02) 45 | * @link http://code.google.com/p/jsmin-php/ 46 | */ 47 | 48 | class JSMin { 49 | const ORD_LF = 10; 50 | const ORD_SPACE = 32; 51 | 52 | protected $a = ''; 53 | protected $b = ''; 54 | protected $input = ''; 55 | protected $inputIndex = 0; 56 | protected $inputLength = 0; 57 | protected $lookAhead = null; 58 | protected $output = ''; 59 | 60 | // -- Public Static Methods -------------------------------------------------- 61 | 62 | public static function minify($js) { 63 | $jsmin = new JSMin($js); 64 | return $jsmin->min(); 65 | } 66 | 67 | // -- Public Instance Methods ------------------------------------------------ 68 | 69 | public function __construct($input) { 70 | $this->input = str_replace("\r\n", "\n", $input); 71 | $this->inputLength = strlen($this->input); 72 | } 73 | 74 | // -- Protected Instance Methods --------------------------------------------- 75 | 76 | protected function action($d) { 77 | switch($d) { 78 | case 1: 79 | $this->output .= $this->a; 80 | 81 | case 2: 82 | $this->a = $this->b; 83 | 84 | if ($this->a === "'" || $this->a === '"') { 85 | for (;;) { 86 | $this->output .= $this->a; 87 | $this->a = $this->get(); 88 | 89 | if ($this->a === $this->b) { 90 | break; 91 | } 92 | 93 | if (ord($this->a) <= self::ORD_LF) { 94 | throw new JSMinException('Unterminated string literal.'); 95 | } 96 | 97 | if ($this->a === '\\') { 98 | $this->output .= $this->a; 99 | $this->a = $this->get(); 100 | } 101 | } 102 | } 103 | 104 | case 3: 105 | $this->b = $this->next(); 106 | 107 | if ($this->b === '/' && ( 108 | $this->a === '(' || $this->a === ',' || $this->a === '=' || 109 | $this->a === ':' || $this->a === '[' || $this->a === '!' || 110 | $this->a === '&' || $this->a === '|' || $this->a === '?')) { 111 | 112 | $this->output .= $this->a . $this->b; 113 | 114 | for (;;) { 115 | $this->a = $this->get(); 116 | 117 | if ($this->a === '/') { 118 | break; 119 | } elseif ($this->a === '\\') { 120 | $this->output .= $this->a; 121 | $this->a = $this->get(); 122 | } elseif (ord($this->a) <= self::ORD_LF) { 123 | throw new JSMinException('Unterminated regular expression '. 124 | 'literal.'); 125 | } 126 | 127 | $this->output .= $this->a; 128 | } 129 | 130 | $this->b = $this->next(); 131 | } 132 | } 133 | } 134 | 135 | protected function get() { 136 | $c = $this->lookAhead; 137 | $this->lookAhead = null; 138 | 139 | if ($c === null) { 140 | if ($this->inputIndex < $this->inputLength) { 141 | $c = substr($this->input, $this->inputIndex, 1); 142 | $this->inputIndex += 1; 143 | } else { 144 | $c = null; 145 | } 146 | } 147 | 148 | if ($c === "\r") { 149 | return "\n"; 150 | } 151 | 152 | if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) { 153 | return $c; 154 | } 155 | 156 | return ' '; 157 | } 158 | 159 | protected function isAlphaNum($c) { 160 | return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; 161 | } 162 | 163 | protected function min() { 164 | $this->a = "\n"; 165 | $this->action(3); 166 | 167 | while ($this->a !== null) { 168 | switch ($this->a) { 169 | case ' ': 170 | if ($this->isAlphaNum($this->b)) { 171 | $this->action(1); 172 | } else { 173 | $this->action(2); 174 | } 175 | break; 176 | 177 | case "\n": 178 | switch ($this->b) { 179 | case '{': 180 | case '[': 181 | case '(': 182 | case '+': 183 | case '-': 184 | $this->action(1); 185 | break; 186 | 187 | case ' ': 188 | $this->action(3); 189 | break; 190 | 191 | default: 192 | if ($this->isAlphaNum($this->b)) { 193 | $this->action(1); 194 | } 195 | else { 196 | $this->action(2); 197 | } 198 | } 199 | break; 200 | 201 | default: 202 | switch ($this->b) { 203 | case ' ': 204 | if ($this->isAlphaNum($this->a)) { 205 | $this->action(1); 206 | break; 207 | } 208 | 209 | $this->action(3); 210 | break; 211 | 212 | case "\n": 213 | switch ($this->a) { 214 | case '}': 215 | case ']': 216 | case ')': 217 | case '+': 218 | case '-': 219 | case '"': 220 | case "'": 221 | $this->action(1); 222 | break; 223 | 224 | default: 225 | if ($this->isAlphaNum($this->a)) { 226 | $this->action(1); 227 | } 228 | else { 229 | $this->action(3); 230 | } 231 | } 232 | break; 233 | 234 | default: 235 | $this->action(1); 236 | break; 237 | } 238 | } 239 | } 240 | 241 | return $this->output; 242 | } 243 | 244 | protected function next() { 245 | $c = $this->get(); 246 | 247 | if ($c === '/') { 248 | switch($this->peek()) { 249 | case '/': 250 | for (;;) { 251 | $c = $this->get(); 252 | 253 | if (ord($c) <= self::ORD_LF) { 254 | return $c; 255 | } 256 | } 257 | 258 | case '*': 259 | $this->get(); 260 | 261 | for (;;) { 262 | switch($this->get()) { 263 | case '*': 264 | if ($this->peek() === '/') { 265 | $this->get(); 266 | return ' '; 267 | } 268 | break; 269 | 270 | case null: 271 | throw new JSMinException('Unterminated comment.'); 272 | } 273 | } 274 | 275 | default: 276 | return $c; 277 | } 278 | } 279 | 280 | return $c; 281 | } 282 | 283 | protected function peek() { 284 | $this->lookAhead = $this->get(); 285 | return $this->lookAhead; 286 | } 287 | } 288 | 289 | // -- Exceptions --------------------------------------------------------------- 290 | class JSMinException extends Exception {} 291 | ?> 292 | -------------------------------------------------------------------------------- /minify.php: -------------------------------------------------------------------------------- 1 | 'Content-Type: application/x-javascript', 48 | 'css' => 'Content-Type: text/css', 49 | 'jpg' => 'Content-Type: image/jpg', 50 | 'gif' => 'Content-Type: image/gif', 51 | 'png' => 'Content-Type: image/png', 52 | 'jpeg' => 'Content-Type: image/jpeg', 53 | 'swf' => 'Content-Type: application/x-shockwave-flash' 54 | ); 55 | $type = ''; 56 | foreach ($_REQUEST as $k => $v) { 57 | //最常见的替换规则 58 | $k = preg_replace( 59 | array('/_(js|css|gif|png|jpg|jpeg|swf)$/','/yui\/2_8_0r4/','/yui\/3_0_0/','/(\d+)_(\d+)_(\d+)/','/(\d+)_(\d+)/','/_v(\d)/'), 60 | array('.$1','yui/2.8.0r4','yui/3.0.0','$1.$2.$3','$1.$2','.v$1'), 61 | trim($k,'/') 62 | ); 63 | //在这里添加转换过头的各种情况 64 | $k = str_replace('global.v5.css','global_v5.css',$k); 65 | $k = str_replace('detail.v2.css','detail_v2.css',$k); 66 | $k = str_replace('cubee_combo','cubee.combo',$k); 67 | 68 | if(empty($type)) { 69 | $type = get_extend($k); 70 | } 71 | //文件存在 72 | if(file_exists($k)) { 73 | $in_str = file_get_contents($k); 74 | //处理文本 75 | if(preg_match('/js|css/',$type)){ 76 | //$files[] = file_get_contents($k); 77 | if($MINIFY == true && $type == 'js'){ 78 | $files[] = JSMin::minify($in_str); 79 | }else if($MINIFY == true && $type == 'css'){ 80 | $files[] = cssmin::minify($in_str); 81 | }else{ 82 | $files[] = $in_str; 83 | } 84 | }else{ 85 | //处理非文本 86 | $files[] = array($in_str); 87 | } 88 | }else{ 89 | //文件不存在 90 | $in_sta = file($YOUR_CDN.$k); 91 | //文本的处理 92 | if(preg_match('/js|css/',$type)){ 93 | $files[] = '/* http://a.tbcdn.cn/'.$k.' */'; 94 | $inner_str = join('',$in_sta); 95 | if($MINIFY == true && $type == 'js'){ 96 | $files[] = JSMin::minify($inner_str); 97 | }else if($MINIFY == true && $type == 'css'){ 98 | $files[] = cssmin::minify($inner_str); 99 | }else{ 100 | $files[] = $inner_str; 101 | } 102 | }else{ 103 | //非文本的处理 104 | $files[] = $in_sta; 105 | } 106 | } 107 | } 108 | 109 | header("Expires: " . date("D, j M Y H:i:s", strtotime("now + 10 years")) ." GMT"); 110 | //文本的处理 111 | header($header[$type]);//文件类型 112 | if(preg_match('/js|css/',$type)){ 113 | $result = join("",$files); 114 | }else{ 115 | //非文本的处理 116 | $result = join("",$files[0]); 117 | } 118 | cache(md5($result));//etag,处理Etag是否多余? 119 | echo $result; 120 | ?> 121 | --------------------------------------------------------------------------------