静态资源文件自动压缩并替换成压缩版本(大型网站优化技术)

  这一次,我总结和分享一项大型网站优化技术,那就是在项目中自动压缩静态资源文件(css、js),并让网站自动加载压缩后的资源文件。当然,这项技术在雅虎35条前端优化建议里也有记载,但它那只是给出一个理论的方案而已,并且采用的是外部压缩工具去压缩,而在我的项目中,是直接通过自己的程序自动化去压缩所有css、js文件,然后让页面直接加载所压缩后的资源,接下来直接进入主题。

  本次实验使用的是PHP脚本语言,版本是PHP5.6,是在LINUX下搭建的环境(网上搭建无论是搭建LAMP还是LNMP的教程都五花八门乱七八糟,下次我会总结和分享如何在LINUX下搭建服务器环境的博文,而且搭建的环境必须一次性搭建成功的)。所选用的框架是CI框架,所使用的模板是Smarty模板引擎。当然了,这些只是我所使用的环境而已,如果你是PHP开发者,假如你要测试下这次实验,那么,我建议你的PHP版本选用5.4以上,至于框架用什么都是可以的。而如果你不是PHP开发者(你是JSP或者是ASP开发者或者是其他开发者),那么你理解好这一思路后,完全可以在自己熟悉的语言里进行实验测试。

   一、原理图

  首先我画一张思路图,便于大家先理解。

  首先是资源压缩原理图:

  

 

  接着是资源文件替换的原理图:

  

  假如大家认真理解并且看懂这两张原理图的话,基本上也就掌握了我所分享的思路。假如还是不能理解的话,接下来我会结合代码,对以上原理图的每一步进行详细讲解。

  二、思路详细分析

  1.首先是调用该压缩的方法,你可以把该方法放在网站所要加载的公共类的地方,例如每次访问网站都会调用该压缩方法进行压缩。当然,这个只是在开发环境才会每次都调用,如果是线上的环境,在你的网站发一次新版本的时候,调用一次用来生成压缩版的静态资源就可以了。

 1 class MY_Controller extends CI_Controller {
 2     public function __construct() {
 3         parent::__construct();
 4 
 5         //压缩jscss资源文件
 6         $this->compressResHandle();
 7     }
 8     /**
 9      * 压缩js、css资源文件(优化)
10      * @return [type] [description]
11      */
12     private function compressResHandle() {
13         $this->load->library('ResMinifier');
14         //压缩指定文件夹下的资源文件
15         $this->resminifier->compressRes();
16     }
17 }

  2.接着就调用了 ResMinifier类里的 compressRes方法。在这里我先附上 ResMinifier这个类的代码,然后方便一步步进行分析讲解

  1 <?php 
  2 defined('BASEPATH') OR exit('No direct script access allowed');
  3 /**
  4  * 资源压缩类
  5  */
  6 class ResMinifier {
  7     /** 需要压缩的资源目录*/
  8     public $compressResDir = ['css', 'js'];
  9     /** 忽略压缩的路径,例如此处是js/icon开头的路径忽略压缩*/
 10     public $compressResIngorePrefix = ['js/icon'];
 11     /** 资源根目录*/
 12     public $resRootDir;
 13     /** 资源版本文件路径*/
 14     private $resStatePath;
 15 
 16     public function __construct() {
 17         $this->resRootDir = WEBROOT . 'www/';
 18         $this->resStatePath = WEBROOT . 'www/resState.php';
 19     }
 20 
 21     public function compressRes() {
 22         //获取存放版本的资源文件
 23         $resState = $this->getResState();
 24         $count = 0;
 25 
 26         //开始遍历需要压缩的资源目录
 27         foreach ($this->compressResDir as $resDir) {
 28             foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->resRootDir . $resDir , FilesystemIterator::SKIP_DOTS)) as $file) {
 29                 //获取该资源文件的绝对路径
 30                 $filePath = str_replace('\\', '/', $file->getRealPath());
 31                 //获取文件相对路径
 32                 $object = substr($filePath, strlen($this->resRootDir));
 33                 //计算文件的版本号
 34                 $state = $this->_getResStateVersion($filePath);
 35 
 36                 //获取文件的几个参数值
 37                 if (true !== $this->getObjectInfo($object, $minObject, $needCompress, $state, $extension)) {
 38                     continue;
 39                 }
 40 
 41                 //压缩文件的绝对路径
 42                 $minFilePath = str_replace('\\', '/', $this->resRootDir. $minObject);
 43 
 44                 //************此处p判断是最重要部分之一*****************//
 45                 //判断文件是否存在且已经改动过
 46                 if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) {
 47                     continue;
 48                 }
 49 
 50                 //确保/www/min/目录可写
 51                 $this->_ensureWritableDir(dirname($minFilePath));
 52 
 53                 if ($needCompress) {
 54                     $this->compressResFileAndSave($filePath, $minFilePath);
 55                 } else {
 56                     copy($filePath, $minFilePath);
 57                 }
 58 
 59 
 60                 $resState[$object] = $state;
 61                 $resState[$minObject] = '';
 62                 $count++;
 63 
 64                 if ($count == 50) {
 65                     $this->_saveResState($resState);
 66                     $count = 0;
 67                 }
 68 
 69             }
 70         }
 71         if($count) $this->_saveResState($resState);
 72     }
 73 
 74     public function getObjectInfo($object, &$minObject, &$needCompress, &$state, &$extension) {
 75         //获取资源绝对路径
 76         $filePath = $this->resRootDir . $object;
 77         //判断资源是否存在
 78         if (!file_exists($filePath)) return "资源文件不存在{$filePath}";
 79         //版本号
 80         $state = $this-> _getResStateVersion($filePath);
 81         //文件名后缀
 82         $extension = pathinfo($filePath, PATHINFO_EXTENSION);
 83         //是否要压缩
 84         $needCompress = true;
 85 
 86         //判断资源文件是否是以 .min.css或者.min.js结尾的
 87         //此类结尾一般都是已压缩过,例如jquery.min.js,就不必再压缩了
 88         if (str_end_with($object, '.min.'.$extension, true)) {
 89             //压缩后的资源存放路径,放在 /www/min/ 目录下
 90             $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension) - 4) . $state .'.'. $extension;
 91             $needCompress = false;
 92         } else if (in_array($extension, $this->compressResDir)) {
 93             //此处是需要压缩的文件目录
 94             $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension)) . $state . '.' . $extension;
 95             //看看是否是忽略的路径前缀
 96             foreach ($this->compressResIngorePrefix as $v) {
 97                 if (str_start_with($object, $v, true)) {
 98                     $needCompress = false;
 99                 }
100             }
101         } else {
102             $minObject = 'min/'.$object;
103             $needCompress = false;
104         }
105         return true;
106     }
107 
108 
109     /**
110      * 获取存放资源版本的文件
111      * 它是放在一个数组里
112      * $resState = array(
113      *         '文件路径' => '对应的版本号',
114      *         '文件路径' => '对应的版本号',
115      *         '文件路径' => '对应的版本号',
116      *     );
117      * @return [type] [description]
118      */
119     public function getResState() {
120         if (file_exists($this->resStatePath)) {
121             require $this->resStatePath;
122             return $resState;
123         }
124         return [];
125     }
126 
127     /**
128      * 计算文件的版本号,这个是根据计算文件MD5散列值得到版本号
129      * 只要文件内容改变了,所计算得到的散列值就会不一样
130      * 用于判断资源文件是否有改动过
131      * @param  [type] $filePath [description]
132      * @return [type]           [description]
133      */
134     public function _getResStateVersion($filePath) {
135         return base_convert(crc32(md5_file($filePath)), 10, 36);
136     }
137 
138     /**
139      * 确保目录可写
140      * @param  [type] $dir [description]
141      * @return [type]      [description]
142      */
143     private function _ensureWritableDir($dir) {
144         if (!file_exists($dir)) {
145             @mkdir($dir, 0777, true);
146             @chmod($dir, 0777);
147         } else if (!is_writable($dir)) {
148             @chmod($dir, 0777);
149             if (!is_writable($dir)) {
150                 show_error('目录'.$dir.'不可写');
151             }
152         }
153     }
154 
155     /**
156      * 将压缩后的资源文件写入到/www/min/下去
157      * @param  [type] $filePath    [description]
158      * @param  [type] $minFilePath [description]
159      * @return [type]              [description]
160      */
161     private function compressResFileAndSave($filePath, $minFilePath) {
162         if (!file_put_contents($minFilePath, $this->compressResFile($filePath))) {
163 
164             //$CI->exceptions->show_exception("写入文件{$minFilePath}失败");
165             show_error("写入文件{$minFilePath}失败", -1);
166         }
167     }
168 
169     /**
170      * 压缩资源文件
171      * @param  [type] $filePath [description]
172      * @return [type]           [description]
173      */
174     private function compressResFile($filePath) {
175         $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
176         if ($extension === 'js') {
177             require_once 'JShrink/Minifier.php';
178             return \JShrink\Minifier::minify(file_get_contents($filePath));
179         } else if ($extension ==='css') {
180             $content = file_get_contents($filePath);
181             $content = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $content);
182             $content = str_replace(["\r\n", "\r", "\n"], '', $content);
183             $content = preg_replace('/([{}),;:>])\s+/', '$1', $content);
184             $content = preg_replace('/\s+([{}),;:>])/', '$1', $content);
185             $content = str_replace(';}', '}', $content);
186             return $content;
187         } else {
188             //$CI->exceptions->show_exception("不支持压缩{extension}文件[$filePath]");
189             show_error("不支持压缩{extension}文件[$filePath]", -1);
190 
191         }
192     }
193 
194     private function _saveResState($resState) {
195         ksort($resState);
196         $content = "<?php\n\n\$resState = array(\n";
197         foreach ($resState as $k => $v) {
198             $content .= "\t '$k' => '$v',\n";
199         }
200         $content .= ");\n\n";
201         file_put_contents($this->resStatePath, $content); 
202     }
203 
204 }
点击打开 资源压缩类

  整个类大部分代码我都加了注释,方便大家快速理解。这里我也会对每一行代码进行解说。

  (1)

/** 需要压缩的资源目录*/
    public $compressResDir = ['css', 'js'];
    /** 忽略压缩的路径,例如此处是js/icon开头的路径忽略压缩*/
    public $compressResIngorePrefix = ['js/icon'];
    /** 资源根目录*/
    public $resRootDir;
    /** 资源版本文件路径*/
    private $resStatePath;

    public function __construct() {
        $this->resRootDir = WEBROOT . 'www/';
        $this->resStatePath = WEBROOT . 'www/resState.php';
    }

  $compressResDir变量是需要压缩的资源目录,假如你有新的处理目录,可以在此变量里假如新的目录名即可处理。附上我测试项目的目录图

  $compressResIngorePrefix 忽略被压缩的路径的路径前部分是该数组变量的字符串,例如 有一个资源路径为 js/icon/bg.js或者是js/icon_index.js或者是js/icon.header.js,假如在该数组中加入了 js/icon这个字符串,那么资源路径为js/icon开头的都会被忽略掉,也就是直接跳过,不用压缩。(因为资源文件里总有一些是不需要压缩的嘛)

  $resRootDir存放资源根目录的

  $resStatePath 这个是资源版本文件路径

  (2)进入compressRes() 方法,我们先分析前面这一段代码

public function compressRes() {
        //获取存放版本的资源文件
        $resState = $this->getResState();
        $count = 0;  

-------------------------------调用getResState() 讲解start------------------------------------------------------------- 

  这里首先是调用 $this->getResState() 方法来获取存放版本的资源文件,此处先跳到该方法看看是如何写的,其实就是包含该文件,然后返回里面存放版本号的数组,我们看注释可以知道该文件里存放版本号的格式(顺便附上图让大家看看)

 

 

   /**
     * 获取存放资源版本的文件
     * 它是放在一个数组里
     * $resState = array(
     *         '文件路径' => '对应的版本号',
     *         '文件路径' => '对应的版本号',
     *         '文件路径' => '对应的版本号',
     *     );
     * @return [type] [description]
     */
    public function getResState() {
        if (file_exists($this->resStatePath)) {
            require $this->resStatePath;
            return $resState;
        }
        return [];
    }

 

  (资源版本文件截图:)

 

-------------------------------调用getResState() 讲解end------------------------------------------------------------- 

 

  接着看compressRes()里的这一段代码 

 

//开始遍历需要压缩的资源目录
        foreach ($this->compressResDir as $resDir) {
            foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->resRootDir . $resDir , FilesystemIterator::SKIP_DOTS)) as $file) {
                //获取该资源文件的绝对路径
                $filePath = str_replace('\\', '/', $file->getRealPath());
                //获取文件相对路径
                $object = substr($filePath, strlen($this->resRootDir));
                //计算文件的版本号
                $state = $this->_getResStateVersion($filePath);

 

  第一个遍历的是js和css目录 第二个遍历是将js目录或者css目录里的文件都变成路径形式,

  例如获取文件的绝对路径 $filePath 的值是这样子的:

    /usr/local/apache2/htdocs/project/www/css/home/index.css

  而文件的相对路径$object是这样子的 :

    css/home/index.css

  这里就开始调用$this->_getResStateVersion($filePath)来计算文件的版本号

 

-------------------------------调用_getResStateVersion($filePath) 讲解start------------------------------------------------------------- 

/**
     * 计算文件的版本号,这个是根据计算文件MD5散列值得到版本号
     * 只要文件内容改变了,所计算得到的散列值就会不一样
     * 用于判断资源文件是否有改动过
     * @param  [type] $filePath [description]
     * @return [type]           [description]
     */
    public function _getResStateVersion($filePath) {
        return base_convert(crc32(md5_file($filePath)), 10, 36);
    }

-------------------------------调用_getResStateVersion($filePath) 讲解end-------------------------------------------------------------   

 

  或者到版本号后,再看下一段代码,这里开始调用$this->getObjectInfo()方法,这里获取到压缩文件的相对路径$minObject,是否需要压缩$needCompress,版本号$state,文件后缀$extension。

             //获取文件的几个参数值
                if (true !== $this->getObjectInfo($object, $minObject, $needCompress, $state, $extension)) {
                    continue;
                }
            

 

-------------------------------调用$this->getObjectInfo() 讲解start-------------------------------------------------------------  

   /**
     * 获取资源文件相关信息
     * @param  [type] $object       资源文件路径 (www/css/home/index.css)
     * @param  [type] $minObject    压缩资源文件路径 (www/min/css/home/index.ae123a.css)
     * @param  [type] $needCompress 是否需要压缩
     * @param  [type] $state        文件版本号
     * @param  [type] $extension    文件名后缀
     * @return [type]               [description]
     */
    public function getObjectInfo($object, &$minObject, &$needCompress, &$state, &$extension) {
        //获取资源绝对路径
        $filePath = $this->resRootDir . $object;
        //判断资源是否存在
        if (!file_exists($filePath)) return "资源文件不存在{$filePath}";
        //版本号
        $state = $this-> _getResStateVersion($filePath);
        //文件名后缀
        $extension = pathinfo($filePath, PATHINFO_EXTENSION);
        //是否要压缩
        $needCompress = true;

        //判断资源文件是否是以 .min.css或者.min.js结尾的
        //此类结尾一般都是已压缩过,例如jquery.min.js,就不必再压缩了
        if (str_end_with($object, '.min.'.$extension, true)) {
            //压缩后的资源存放路径,放在 /www/min/ 目录下
            $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension) - 4) . $state .'.'. $extension;
            $needCompress = false;
        } else if (in_array($extension, $this->compressResDir)) {
            //此处是需要压缩的文件目录
            $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension)) . $state . '.' . $extension;
            //看看是否是忽略的路径前缀
            foreach ($this->compressResIngorePrefix as $v) {
                if (str_start_with($object, $v, true)) {
                    $needCompress = false;
                }
            }
        } else {
            $minObject = 'min/'.$object;
            $needCompress = false;
        }
        return true;
    }

 

  这个方法里的每一行代码基本上都有注释了,所以就不一句句进行讲解了,这里主要看下面的判断部分:

    if (str_end_with($object, '.min.'.$extension, true))  这个判断是比较资源文件路径字串后面部分是否以 .min.$extension 结尾,例如是 jquery.min.js,这种文件本来就是
压缩过的文件,所以就不用再进行压缩处理了, $minObject 这个变量存放的是压缩后的资源文件路径。
  此处附上str_end_with()函数的代码:
/**
     * 判断 subject 是否以 search结尾, 参数指定是否忽略大小写
     * @param  [type]  $subject     [description]
     * @param  [type]  $search      [description]
     * @param  boolean $ignore_case [description]
     * @return [type]               [description]
     */
    function str_end_with($subject, $search, $ignore_case = false) {
        $len2 = strlen($search);
        if (0 === $len2) return true;
        $len1 = strlen($subject);
        if ($len2 > $len1) return false;
        if ($ignore_case) {
            return 0 === strcmp(substr($subject, $len1 - $len2), $search);
        } else {
            return 0 === strcasecmp(substr($subject, $len1 - $len2), $search);
        }
    }

  if (in_array($extension, $this->compressResDir),这个判断就是是否是需要处理的两个目录里的。

  然后里面的foreach ($this->compressResIngorePrefix as $v) { if (str_start_with($object, $v, true)) { $needCompress = false; } } 

这个是判断是否是以$this->compressResIngorePrefix属性定义的前面部分字串开头的路径,是的话就忽略压缩该资源文件。

  判断到最后else 就是说明该资源文件不需要压缩了,最后是返回$minObject,$needCompress,$state,$extension这四个变量。

-------------------------------调用$this->getObjectInfo() 讲解end------------------------------------------------------------- 

 

  到这里继续回来看 compressRes()方法里面的代码    

                //压缩文件的绝对路径
                $minFilePath = str_replace('\\', '/', $this->resRootDir. $minObject);

                //************此处p判断是最重要部分之一*****************//
                //判断文件是否存在且已经改动过
                if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) {
                    continue;
                }    

 

  这段代码首先是拼接出压缩文件的绝对路径,

  接着下面这个判断是关键的部分,通过这个判断就可以知道该资源文件是否被改动过,如果改动过的话,就重新对该资源文件进行压缩,假如没改动过,就继续处理下一个资源文件。看这里的判断:isset($resState[$object]) && $resState[$object] == $state,这个判断就是判断该文件路径是否存在  并且文件中对应的版本号和计算出的版本号是否还一致;isset($resState[$minObject]) && file_exists($minFilePath),这个是判断压缩文件路径是否存在,并且该压缩文件是否真实存在目录中。

 

  看下一段代码,如果能走到这一部分,说明目前的这个资源文件是被改动过的(代码修改过),那么此时就对文件进行压缩操作了

                //确保/www/min/目录可写
                $this->_ensureWritableDir(dirname($minFilePath));

                if ($needCompress) {
                    $this->compressResFileAndSave($filePath, $minFilePath);
                } else {
                    copy($filePath, $minFilePath);
                }

$this->_ensureWritableDir(),此方法是要保证新创建的www/min目录是可写的,这里附上代码:

 

-------------------------------调用$this->_ensureWritableDir() 讲解start------------------------------------------------------------- 

   /**
     * 确保目录可写
     * @param  [type] $dir [description]
     * @return [type]      [description]
     */
    private function _ensureWritableDir($dir) {
        if (!file_exists($dir)) {
            @mkdir($dir, 0777, true);
            @chmod($dir, 0777);
        } else if (!is_writable($dir)) {
            @chmod($dir, 0777);
            if (!is_writable($dir)) {
                show_error('目录'.$dir.'不可写');
            }
        }
    }

-------------------------------调用$this->_ensureWritableDir() 讲解end------------------------------------------------------------- 

  

  if ($needCompress),这个判断资源文件是否需要压缩,需要的话调用$this->compressResFileAndSave($filePath, $minFilePath);不需要的话,直接复制文件到压缩文件路径 copy($filePath, $minFilePath);

 

  先看$this->compressResFileAndSave()

-------------------------------调用$this->compressResFileAndSave() 讲解start-------------------------------------------------------------

    /**
     * 将压缩后的资源文件写入到/www/min/下去
     * @param  [type] $filePath    [description]
     * @param  [type] $minFilePath [description]
     * @return [type]              [description]
     */
    private function compressResFileAndSave($filePath, $minFilePath) {
        if (!file_put_contents($minFilePath, $this->compressResFile($filePath))) {

            //$CI->exceptions->show_exception("写入文件{$minFilePath}失败");
            show_error("写入文件{$minFilePath}失败", -1);
        }
    }

    /**
     * 压缩资源文件
     * @param  [type] $filePath [description]
     * @return [type]           [description]
     */
    private function compressResFile($filePath) {
        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
        if ($extension === 'js') {
            require_once 'JShrink/Minifier.php';
            return \JShrink\Minifier::minify(file_get_contents($filePath));
        } else if ($extension ==='css') {
            $content = file_get_contents($filePath);
            $content = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $content);
            $content = str_replace(["\r\n", "\r", "\n"], '', $content);
            $content = preg_replace('/([{}),;:>])\s+/', '$1', $content);
            $content = preg_replace('/\s+([{}),;:>])/', '$1', $content);
            $content = str_replace(';}', '}', $content);
            return $content;
        } else {
            //$CI->exceptions->show_exception("不支持压缩{extension}文件[$filePath]");
            show_error("不支持压缩{extension}文件[$filePath]", -1);

        }
    } 

  先压缩,再将压缩后的内容写入到 压缩文件路径里去。

  我们先看下这个压缩方法:

    $this->compressResFile($filePath); 此方法中分两类压缩,第一类时对js文件进行压缩,第二类的对css文件进行压缩。先说js压缩,这里是调用一个JShrink的类,它

一个用来压缩js文件的PHP类,百度可以找到,调用这个类的minify()这个方法就可以压缩了;而css的压缩利用正则替换来压缩,把那些空格换行什么的都去掉。到此就压缩成功

了,然后再将压缩后的资源写入到对应的压缩文件路径里去。

 

-------------------------------调用$this->compressResFileAndSave() 讲解end-------------------------------------------------------------

  

  接着继续看compressRes()这个方法里的代码,这里开始就是保存新的版本号到$resState数组里 $object=>$state,还有就是新的压缩路径$minObject,而这里$count++的作用是,当这个循环50次就将 $resState这个数组写入一次到 resState.php文件里,这里是出于严谨考虑而已,如果你不加这个 $count的处理这部分也可以,最后写入一次就行了。

 

                $resState[$object] = $state;
                $resState[$minObject] = '';
                $count++;

                if ($count == 50) {
                    $this->_saveResState($resState);
                    $count = 0;
                }

            }
        }
        if($count) $this->_saveResState($resState);         

 

  这里看$this->_saveResState($resState),这个方法就是将$resState数组写入到resState.php文件里去的方法。

 

-------------------------------调用$this->_saveResState($resState) 讲解start-------------------------------------------------------------

    private function _saveResState($resState) {
        ksort($resState);
        $content = "<?php\n\n\$resState = array(\n";
        foreach ($resState as $k => $v) {
            $content .= "\t '$k' => '$v',\n";
        }
        $content .= ");\n\n";
        file_put_contents($this->resStatePath, $content); 
    }    

-------------------------------调用$this->_saveResState($resState) 讲解end------------------------------------------------------------- 

   处理完后,看看所生成的文件,这里一个文件会有多个版本,旧版本没有删除掉,在开发环境下删不删除都没问题,这里为何不删除旧版本的压缩文件,这就涉及到在更新多个应用服务器代码时所要注意的问题里。在此我就多讲解一点吧,简单地举个例子吧,一般大型项目中的静态资源和模板文件是部署在不同的机器集群上的,上线的过程中,静态资源和页面文件的部署时间间隔可能会非常长,对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问,假如旧版本的静态资源删除了,但新版本的静态资源还没部署完成,那么用户就加载不到该静态资源,结果可想而知,所以,一般情况下我们会保留旧版本的静态资源,然后等所有一些部署完成了,再通过一定的脚本删除掉也没关系,其实,这些不必删除也是可以的,你想想,一个项目发一次版本,才会调用一次资源文件压缩方法,它只会对修改过的文件进行生成新版本号的静态文件而已。这些就看个人的做法了。

  我们可以打开看看,下面这个就是压缩后的文件的代码了,文件原大小为16K,压缩后大概少了5K,现在是11K,压缩比大概是2/3,假如在大型项目中,一个复杂点的页面会有很大的静态资源文件要加载,通过此方法,大大地提高了加载的速度。(可能有些朋友觉得压缩个几K或者十几K算什么,完全可以忽略,其实我想说的是,当你在大型项目中优化项目的时候,能够减少几K的代码,也给网站的性能提高了一大截)

 

  到此,资源压缩处理就分析完毕了。其实,有一定基础的朋友,可以直接看我分享的那个代码就可以了,假如理解不了,再看我上面这一步步的分析讲解,我是处于能看来到此博客的朋友,无论技术是好或者是稍弱,都能看懂,所以才对代码一步步地进行分析讲解。(希望各位多多支持小弟)

 -------------------------------------------------------------------------------------------------------------------------

  3. 接下来就是讲解如何替换压缩后的资源文件了。

  这个到Home.php

 1 <?php
 2 defined('BASEPATH') OR exit('No direct script access allowed');
 3 
 4 class Home extends MY_Controller {
 5     public function index() {
 6         $this->smartyData['test'] = 111;
 7         //这个默认是加载 www/css/home/index.css文件
 8         $this->addResLink('index.css');
 9         //这个默认是加载www/js/jquery.all.min.js文件
10         $this->addResLink('/jquery.all.min.js');
11         //这个默认是加载www/js/index.js文件
12         $this->addResLink('index.js');
13         $this->displayView('home/index.tpl');
14     }
15 }

  上面有加载三个资源文件,我们先看看$this->addResLink();这个方法,这个方法放在My_Controller.php里:

    /**
     * 资源路径
     * @param [type] $filePath [description]
     */
    protected function addResLink($filePath) {
        list($filePath, $query) = explode('?', $filePath . '?');
        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
        foreach ($this->_resLink as $v) {
            if (false === array_search($filePath, $this->_resLink[$extension])) {
                $this->_resLink[$extension][] = $query == null ? $filePath : $filePath .'?'. $query;
            }
        }

        return $this;
    }

  这里主要是判断了资源文件是css还是js,然后将其存放在 $this->_resLink这个属性里。

  那么此处我就先附上My_Controller.php这个父类的所有代码吧

  1 <?php
  2 defined('BASEPATH') OR exit('No direct script access allowed');
  3 
  4 class MY_Controller extends CI_Controller {
  5     public function __construct() {
  6         parent::__construct();
  7 
  8         //压缩jscss资源文件
  9         $this->compressResHandle();
 10     }
 11 
 12     //==========================使用SMARTY模板引擎================================//
 13     /* Smarty母版页文件路径 */
 14     protected $masterPage = 'default.tpl';
 15     /* 视图文件路径*/
 16     protected $smartyView;
 17     /* 要赋值给smarty视图的数据*/
 18     protected $smartyData = [];
 19     /* 资源文件*/
 20     protected $_resLink = ['js'=>[], 'css'=>[]];
 21 
 22     /**
 23      * 使用母版页输出一个视图
 24      * @return [type] [description]
 25      */
 26     protected function displayView($viewName = null, $masterPage = null) {
 27         //为空则选用默认母版
 28         if ($masterPage == null) $masterPage = $this->masterPage;
 29         //获取视图的输出内容
 30         $viewContent = $this->_fetchView($this->smartyData, $viewName, $masterPage);
 31 
 32         $output = '';
 33         
 34         //添加css Link
 35         foreach ($this->_resLink['css'] as $v) {
 36             $output .= res_link($v);
 37         }
 38 
 39         //内容部分
 40         $output .= $viewContent;
 41         //尾部添加js 链接
 42         foreach ($this->_resLink['js'] as $v) {
 43             $output .= res_link($v);
 44         }
 45         //发送最终输出结果以及服务器的 HTTP 头到浏览器
 46         
 47         $this->output->_display($output);
 48         return $output;
 49     }
 50 
 51     private function _fetchView($smartyData, &$viewName, &$masterPage) {
 52         if ($viewName == null) $viewName = $this->smartyView;
 53 
 54         if (empty($this->smarty)) {
 55             require_once SMARTY_DIR.'Smarty.class.php';
 56             $this->smarty = new Smarty();
 57             $this->smarty->setCompileDir(APPPATH . 'cache/');
 58             $this->smarty->setCacheDir(APPPATH . 'cache/');
 59         }
 60 
 61         //设置视图真实路径
 62         $this->_getViewDir(true, $viewName, $masterPage, $templateDir);
 63 
 64         foreach ($smartyData as $k => $v) {
 65             $this->smarty->assign($k, $v);
 66         }
 67 
 68         if (empty($masterPage)) {
 69             return $this->smarty->fetch($viewName);
 70         } else {
 71             $this->smarty->assign('VIEW_MAIN', $viewName);
 72             return $this->smarty->fetch($masterPage);
 73         }
 74     }
 75 
 76     /**
 77      * 资源路径
 78      * @param [type] $filePath [description]
 79      */
 80     protected function addResLink($filePath) {
 81         list($filePath, $query) = explode('?', $filePath . '?');
 82         $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
 83         foreach ($this->_resLink as $v) {
 84             if (false === array_search($filePath, $this->_resLink[$extension])) {
 85                 $this->_resLink[$extension][] = $query == null ? $filePath : $filePath .'?'. $query;
 86             }
 87         }
 88 
 89         return $this;
 90     }
 91 
 92     private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) {
 93         if ('/' === $viewName[0]) $viewName = substr($viewName, 1);
 94 
 95         //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去
 96         if ($masterPage) {
 97             $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage);
 98         }
 99 
100         //是否设置模板目录
101         if ($setTemplateDir) {
102             $templateDir = VIEWPATH;
103             $this->smarty->setTemplateDir($templateDir);
104         }
105     }
106 
107     /**
108      * 压缩js、css资源文件(优化)
109      * @return [type] [description]
110      */
111     private function compressResHandle() {
112         $this->load->library('ResMinifier');
113         //压缩指定文件夹下的资源文件
114         $this->resminifier->compressRes();
115     }
116 }
点击打开 My_Controller.php

   打印出来 $this->_resLink这个属性的结构是这样子的:

Array
(
    [js] => Array
        (
            [0] => /jquery.all.min.js
            [1] => index.js
        )

    [css] => Array
        (
            [0] => index.css
        )

)

  再回到Home.php里面调用 $this->displayView('home/index.tpl');

  我们看这个方法:

     /**
     * 使用母版页输出一个视图
     * @return [type] [description]
     */
    protected function displayView($viewName = null, $masterPage = null) {
        //为空则选用默认母版
        if ($masterPage == null) $masterPage = $this->masterPage;
        //获取视图的输出内容
        $viewContent = $this->_fetchView($this->smartyData, $viewName, $masterPage);

        $output = '';
        
        //添加css Link
        foreach ($this->_resLink['css'] as $v) {
            $output .= res_link($v);
        }

        //内容部分
        $output .= $viewContent;
        //尾部添加js 链接
        foreach ($this->_resLink['js'] as $v) {
            $output .= res_link($v);
        }
        //发送最终输出结果以及服务器的 HTTP 头到浏览器
        
        $this->output->_display($output);
        return $output;
    }

    private function _fetchView($smartyData, &$viewName, &$masterPage) {
        if ($viewName == null) $viewName = $this->smartyView;

        if (empty($this->smarty)) {
            require_once SMARTY_DIR.'Smarty.class.php';
            $this->smarty = new Smarty();
            $this->smarty->setCompileDir(APPPATH . 'cache/');
            $this->smarty->setCacheDir(APPPATH . 'cache/');
        }

        //设置视图真实路径
        $this->_getViewDir(true, $viewName, $masterPage, $templateDir);

        foreach ($smartyData as $k => $v) {
            $this->smarty->assign($k, $v);
        }

        if (empty($masterPage)) {
            return $this->smarty->fetch($viewName);
        } else {
            $this->smarty->assign('VIEW_MAIN', $viewName);
            return $this->smarty->fetch($masterPage);
        }
    }

  这一段代码没有一部分就是调用了Smarty模板引擎的内容,这个有关Smarty的知识我就不讲了,大家可以自己百度,这里主要讲 res_link() 这个函数,就是通过这个函数来进行资源文件替换的。先看这个函数的代码:

    /**
     * 输出 HttpHead 中的资源连接。 css/js 自动判断真实路径
     * @param  string  文件路径
     * @return string      
     */
    function res_link($file) {
        $file = res_path($file, $extension);

        if ($extension === 'css') {
           return '<link rel="stylesheet" type="text/css" href="' . $file . '"/>';
        } else if ($extension === 'js') {
            return '<script type="text/javascript" src="'.$file.'"></script>';
        } else {
            return false;
        }
    }
 

   此处最重要就是 res_path() 函数了,这个函数能自动路由资源的真实路径 。例如:index.css = > css/home/index.css

  该函数最重要的一个功能是替换资源的压缩版本。

  直接看代码:

   /**
     * 智能路由资源真实路径
     * @param  string      路径
     * @param  string      扩展名
     * @return string       真实路径
     */
    function res_path($file, &$extension) {
        //检查是否存在查询字符串
        list($file, $query) = explode('?', $file . '?');
        //取得扩展名
        $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
        //
        $file = str_replace('\\', '/', $file);
        //取得当前控制器名
        global $class;
        if ($class == null) exit('can not get class name');
        $className = strtolower($class);

        //此处的规则是这样:
        //例如,如果不加 / ,Home控制器对应的格式是: index.css,那么 此处的路径会变成css/home/index.css
        //假如有 / ,控制器的格式可以是 /main.css,那么此处的路径会变成 css/main.css(公用的css类)
        if ('/' !== $file[0]) {
            //index.css => css/home/index.css
            $object = $extension .'/'. $className .'/' . $file;
        } else {
            // /css/main.css 或者 /main.css => css/main.css
            $object = substr($file, 1);

            //若object是 main.css ,则自动加上 扩展名目录 => css/main.css
            if (0 !== strncasecmp($extension, $object, strlen($extension))) {
                $object = $extension . '/' . $object;
            }
        }
        //资源真实路径
        $filepath = WEBROOT.'www/'.$object;
        
        //替换压缩版本,这部分逻辑与文件压缩逻辑对应
        if (in_array($extension, array('css', 'js'))) {
            if(!str_start_with($object, 'min/') && file_exists(APPPATH.'libraries/ResMinifier.php')) {
                require_once APPPATH.'libraries/ResMinifier.php';
                $resminifier = new ResMinifier();
                //获取存放资源版本的文件的数组变量
                $resState = $resminifier->getResState();
                //计算得到当前文件版本号
                $state = $resminifier->_getResStateVersion($filepath);
                //判断该版本号是否存在
                if (isset($resState[$object])) {
                    //判断是否是.min.css或.min.js结尾
                    if (str_end_with($object, '.min.'.$extension)) {
                        //将版本号拼接上去,然后得到min的文件路径
                        $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension) - 4) . $state . '.' . $extension;
                    } else {
                        //将版本号拼接上去,然后得到min的文件路径
                        $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension)) . $state . '.' . $extension;
                    }
                    //判断min的路径是否存在在$resState里面
                     if (isset($resState[$minObject])) {
                        $object = $minObject;
                        $query = '';
                     }
                } 

            }
            
            $file = RES_BASE_URL . $object;
        }

        return ($query == null) ? $file : ($file .'?'. $query);

    }

  代码基本上都给了注释,方便大家容易去理解,前面一部分是智能路径css、js资源的路径,后面一部分是替换压缩版本,这一部分的逻辑其实和资源压缩那里的逻辑基本一样,就是通过资源文件路径,进行判断和处理,最后得到资源的压缩版本的路径,最后就将资源的压缩版本的路径返回去,放在'<link rel="stylesheet" type="text/css" href="' . $file . '"/>'里面。这样  ,就成功地将资源文件路径替换成了压缩版本的资源文件路径,并且在模板输出时,输出的是压缩后的资源文件。

  到此,资源替换的内容就到此讲解完毕。而整一项技术也分析到此。

  三、总结

  在这里我集中地附上本博文讲解中的几个文件代码:

  Home.php

 1 <?php
 2 defined('BASEPATH') OR exit('No direct script access allowed');
 3 
 4 class Home extends MY_Controller {
 5     public function index() {
 6         $this->smartyData['test'] = 111;
 7         //这个默认是加载 www/css/home/index.css文件
 8         $this->addResLink('index.css');
 9         //这个默认是加载www/js/jquery.all.min.js文件
10         $this->addResLink('/jquery.all.min.js');
11         //这个默认是加载www/js/index.js文件
12         $this->addResLink('index.js');
13         $this->displayView('home/index.tpl');
14     }
15 }
点击打开

  My_Controller.php

  1 <?php
  2 defined('BASEPATH') OR exit('No direct script access allowed');
  3 
  4 class MY_Controller extends CI_Controller {
  5     public function __construct() {
  6         parent::__construct();
  7 
  8         //压缩jscss资源文件
  9         $this->compressResHandle();
 10     }
 11 
 12     //==========================使用SMARTY模板引擎================================//
 13     /* Smarty母版页文件路径 */
 14     protected $masterPage = 'default.tpl';
 15     /* 视图文件路径*/
 16     protected $smartyView;
 17     /* 要赋值给smarty视图的数据*/
 18     protected $smartyData = [];
 19     /* 资源文件*/
 20     protected $_resLink = ['js'=>[], 'css'=>[]];
 21 
 22     /**
 23      * 使用母版页输出一个视图
 24      * @return [type] [description]
 25      */
 26     protected function displayView($viewName = null, $masterPage = null) {
 27         //为空则选用默认母版
 28         if ($masterPage == null) $masterPage = $this->masterPage;
 29         //获取视图的输出内容
 30         $viewContent = $this->_fetchView($this->smartyData, $viewName, $masterPage);
 31 
 32         $output = '';
 33         
 34         //添加css Link
 35         foreach ($this->_resLink['css'] as $v) {
 36             $output .= res_link($v);
 37         }
 38 
 39         //内容部分
 40         $output .= $viewContent;
 41         //尾部添加js 链接
 42         foreach ($this->_resLink['js'] as $v) {
 43             $output .= res_link($v);
 44         }
 45         //发送最终输出结果以及服务器的 HTTP 头到浏览器
 46         
 47         $this->output->_display($output);
 48         return $output;
 49     }
 50 
 51     private function _fetchView($smartyData, &$viewName, &$masterPage) {
 52         if ($viewName == null) $viewName = $this->smartyView;
 53 
 54         if (empty($this->smarty)) {
 55             require_once SMARTY_DIR.'Smarty.class.php';
 56             $this->smarty = new Smarty();
 57             $this->smarty->setCompileDir(APPPATH . 'cache/');
 58             $this->smarty->setCacheDir(APPPATH . 'cache/');
 59         }
 60 
 61         //设置视图真实路径
 62         $this->_getViewDir(true, $viewName, $masterPage, $templateDir);
 63 
 64         foreach ($smartyData as $k => $v) {
 65             $this->smarty->assign($k, $v);
 66         }
 67 
 68         if (empty($masterPage)) {
 69             return $this->smarty->fetch($viewName);
 70         } else {
 71             $this->smarty->assign('VIEW_MAIN', $viewName);
 72             return $this->smarty->fetch($masterPage);
 73         }
 74     }
 75 
 76     /**
 77      * 资源路径
 78      * @param [type] $filePath [description]
 79      */
 80     protected function addResLink($filePath) {
 81         list($filePath, $query) = explode('?', $filePath . '?');
 82         $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
 83         foreach ($this->_resLink as $v) {
 84             if (false === array_search($filePath, $this->_resLink[$extension])) {
 85                 $this->_resLink[$extension][] = $query == null ? $filePath : $filePath .'?'. $query;
 86             }
 87         }
 88 
 89         return $this;
 90     }
 91 
 92     private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) {
 93         if ('/' === $viewName[0]) $viewName = substr($viewName, 1);
 94 
 95         //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去
 96         if ($masterPage) {
 97             $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage);
 98         }
 99 
100         //是否设置模板目录
101         if ($setTemplateDir) {
102             $templateDir = VIEWPATH;
103             $this->smarty->setTemplateDir($templateDir);
104         }
105     }
106 
107     /**
108      * 压缩js、css资源文件(优化)
109      * @return [type] [description]
110      */
111     private function compressResHandle() {
112         $this->load->library('ResMinifier');
113         //压缩指定文件夹下的资源文件
114         $this->resminifier->compressRes();
115     }
116 }
点击打开

  ResMinifier.php

  1 <?php 
  2 defined('BASEPATH') OR exit('No direct script access allowed');
  3 /**
  4  * 资源压缩类
  5  */
  6 class ResMinifier {
  7     /** 需要压缩的资源目录*/
  8     public $compressResDir = ['css', 'js'];
  9     /** 忽略压缩的路径,例如此处是js/icon开头的路径忽略压缩*/
 10     public $compressResIngorePrefix = ['js/icon'];
 11     /** 资源根目录*/
 12     public $resRootDir;
 13     /** 资源版本文件路径*/
 14     private $resStatePath;
 15 
 16     public function __construct() {
 17         $this->resRootDir = WEBROOT . 'www/';
 18         $this->resStatePath = WEBROOT . 'www/resState.php';
 19     }
 20 
 21     public function compressRes() {
 22         //获取存放版本的资源文件
 23         $resState = $this->getResState();
 24         $count = 0;
 25 
 26         //开始遍历需要压缩的资源目录
 27         foreach ($this->compressResDir as $resDir) {
 28             foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->resRootDir . $resDir , FilesystemIterator::SKIP_DOTS)) as $file) {
 29                 //获取该资源文件的绝对路径
 30                 $filePath = str_replace('\\', '/', $file->getRealPath());
 31 
 32                 //获取文件相对路径
 33                 $object = substr($filePath, strlen($this->resRootDir));
 34 
 35                 //计算文件的版本号
 36                 $state = $this->_getResStateVersion($filePath);
 37 
 38                 //获取文件的几个参数值
 39                 if (true !== $this->getObjectInfo($object, $minObject, $needCompress, $state, $extension)) {
 40                     continue;
 41                 }
 42 
 43                 //压缩文件的绝对路径
 44                 $minFilePath = str_replace('\\', '/', $this->resRootDir. $minObject);
 45 
 46                 //************此处p判断是最重要部分之一*****************//
 47                 //判断文件是否存在且已经改动过
 48                 if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) {
 49                     continue;
 50                 }
 51 
 52                 //确保/www/min/目录可写
 53                 $this->_ensureWritableDir(dirname($minFilePath));
 54 
 55                 if ($needCompress) {
 56                     $this->compressResFileAndSave($filePath, $minFilePath);
 57                 } else {
 58                     copy($filePath, $minFilePath);
 59                 }
 60 
 61 
 62                 $resState[$object] = $state;
 63                 $resState[$minObject] = '';
 64                 $count++;
 65 
 66                 if ($count == 50) {
 67                     $this->_saveResState($resState);
 68                     $count = 0;
 69                 }
 70 
 71             }
 72         }
 73         if($count) $this->_saveResState($resState);
 74     }
 75 
 76     /**
 77      * 获取资源文件相关信息
 78      * @param  [type] $object       资源文件路径 (www/css/home/index.css)
 79      * @param  [type] $minObject    压缩资源文件路径 (www/min/css/home/index.ae123a.css)
 80      * @param  [type] $needCompress 是否需要压缩
 81      * @param  [type] $state        文件版本号
 82      * @param  [type] $extension    文件名后缀
 83      * @return [type]               [description]
 84      */
 85     public function getObjectInfo($object, &$minObject, &$needCompress, &$state, &$extension) {
 86         //获取资源绝对路径
 87         $filePath = $this->resRootDir . $object;
 88         //判断资源是否存在
 89         if (!file_exists($filePath)) return "资源文件不存在{$filePath}";
 90         //版本号
 91         $state = $this-> _getResStateVersion($filePath);
 92         //文件名后缀
 93         $extension = pathinfo($filePath, PATHINFO_EXTENSION);
 94         //是否要压缩
 95         $needCompress = true;
 96 
 97         //判断资源文件是否是以 .min.css或者.min.js结尾的
 98         //此类结尾一般都是已压缩过,例如jquery.min.js,就不必再压缩了
 99         if (str_end_with($object, '.min.'.$extension, true)) {
100             //压缩后的资源存放路径,放在 /www/min/ 目录下
101             $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension) - 4) . $state .'.'. $extension;
102             $needCompress = false;
103         } else if (in_array($extension, $this->compressResDir)) {
104             //此处是需要压缩的文件目录
105             $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension)) . $state . '.' . $extension;
106             //看看是否是忽略的路径前缀
107             foreach ($this->compressResIngorePrefix as $v) {
108                 if (str_start_with($object, $v, true)) {
109                     $needCompress = false;
110                 }
111             }
112         } else {
113             $minObject = 'min/'.$object;
114             $needCompress = false;
115         }
116         return true;
117     }
118 
119 
120     /**
121      * 获取存放资源版本的文件
122      * 它是放在一个数组里
123      * $resState = array(
124      *         '文件路径' => '对应的版本号',
125      *         '文件路径' => '对应的版本号',
126      *         '文件路径' => '对应的版本号',
127      *     );
128      * @return [type] [description]
129      */
130     public function getResState() {
131         if (file_exists($this->resStatePath)) {
132             require $this->resStatePath;
133             return $resState;
134         }
135         return [];
136     }
137 
138     /**
139      * 计算文件的版本号,这个是根据计算文件MD5散列值得到版本号
140      * 只要文件内容改变了,所计算得到的散列值就会不一样
141      * 用于判断资源文件是否有改动过
142      * @param  [type] $filePath [description]
143      * @return [type]           [description]
144      */
145     public function _getResStateVersion($filePath) {
146         return base_convert(crc32(md5_file($filePath)), 10, 36);
147     }
148 
149     /**
150      * 确保目录可写
151      * @param  [type] $dir [description]
152      * @return [type]      [description]
153      */
154     private function _ensureWritableDir($dir) {
155         if (!file_exists($dir)) {
156             @mkdir($dir, 0777, true);
157             @chmod($dir, 0777);
158         } else if (!is_writable($dir)) {
159             @chmod($dir, 0777);
160             if (!is_writable($dir)) {
161                 show_error('目录'.$dir.'不可写');
162             }
163         }
164     }
165 
166     /**
167      * 将压缩后的资源文件写入到/www/min/下去
168      * @param  [type] $filePath    [description]
169      * @param  [type] $minFilePath [description]
170      * @return [type]              [description]
171      */
172     private function compressResFileAndSave($filePath, $minFilePath) {
173         if (!file_put_contents($minFilePath, $this->compressResFile($filePath))) {
174 
175             //$CI->exceptions->show_exception("写入文件{$minFilePath}失败");
176             show_error("写入文件{$minFilePath}失败", -1);
177         }
178     }
179 
180     /**
181      * 压缩资源文件
182      * @param  [type] $filePath [description]
183      * @return [type]           [description]
184      */
185     private function compressResFile($filePath) {
186         $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
187         if ($extension === 'js') {
188             require_once 'JShrink/Minifier.php';
189             return \JShrink\Minifier::minify(file_get_contents($filePath));
190         } else if ($extension ==='css') {
191             $content = file_get_contents($filePath);
192             $content = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $content);
193             $content = str_replace(["\r\n", "\r", "\n"], '', $content);
194             $content = preg_replace('/([{}),;:>])\s+/', '$1', $content);
195             $content = preg_replace('/\s+([{}),;:>])/', '$1', $content);
196             $content = str_replace(';}', '}', $content);
197             return $content;
198         } else {
199             //$CI->exceptions->show_exception("不支持压缩{extension}文件[$filePath]");
200             show_error("不支持压缩{extension}文件[$filePath]", -1);
201 
202         }
203     }
204 
205     private function _saveResState($resState) {
206         ksort($resState);
207         $content = "<?php\n\n\$resState = array(\n";
208         foreach ($resState as $k => $v) {
209             $content .= "\t '$k' => '$v',\n";
210         }
211         $content .= ");\n\n";
212         file_put_contents($this->resStatePath, $content); 
213     }
214 
215 }
点击打开

  Common.php

  1 <?php 
  2     /**
  3      * 输出 HttpHead 中的资源连接。 css/js 自动判断真实路径
  4      * @param  string  文件路径
  5      * @return string      
  6      */
  7     function res_link($file) {
  8         $file = res_path($file, $extension);
  9 
 10         if ($extension === 'css') {
 11            return '<link rel="stylesheet" type="text/css" href="' . $file . '"/>';
 12         } else if ($extension === 'js') {
 13             return '<script type="text/javascript" src="'.$file.'"></script>';
 14         } else {
 15             return false;
 16         }
 17     }
 18 
 19     /**
 20      * 智能路由资源真实路径
 21      * @param  string      路径
 22      * @param  string      扩展名
 23      * @return string       真实路径
 24      */
 25     function res_path($file, &$extension) {
 26         //检查是否存在查询字符串
 27         list($file, $query) = explode('?', $file . '?');
 28         //取得扩展名
 29         $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
 30         //
 31         $file = str_replace('\\', '/', $file);
 32         //取得当前控制器名
 33         global $class;
 34         if ($class == null) exit('can not get class name');
 35         $className = strtolower($class);
 36 
 37         //此处的规则是这样:
 38         //例如,如果不加 / ,Home控制器对应的格式是: index.css,那么 此处的路径会变成css/home/index.css
 39         //假如有 / ,控制器的格式可以是 /main.css,那么此处的路径会变成 css/main.css(公用的css类)
 40         if ('/' !== $file[0]) {
 41             //index.css => css/home/index.css
 42             $object = $extension .'/'. $className .'/' . $file;
 43         } else {
 44             // /css/main.css 或者 /main.css => css/main.css
 45             $object = substr($file, 1);
 46 
 47             //若object是 main.css ,则自动加上 扩展名目录 => css/main.css
 48             if (0 !== strncasecmp($extension, $object, strlen($extension))) {
 49                 $object = $extension . '/' . $object;
 50             }
 51         }
 52         //资源真实路径
 53         $filepath = WEBROOT.'www/'.$object;
 54         
 55         //替换压缩版本,这部分逻辑与文件压缩逻辑对应
 56         if (in_array($extension, array('css', 'js'))) {
 57             if(!str_start_with($object, 'min/') && file_exists(APPPATH.'libraries/ResMinifier.php')) {
 58                 require_once APPPATH.'libraries/ResMinifier.php';
 59                 $resminifier = new ResMinifier();
 60                 //获取存放资源版本的文件的数组变量
 61                 $resState = $resminifier->getResState();
 62                 //计算得到当前文件版本号
 63                 $state = $resminifier->_getResStateVersion($filepath);
 64                 //判断该版本号是否存在
 65                 if (isset($resState[$object])) {
 66                     //判断是否是.min.css或.min.js结尾
 67                     if (str_end_with($object, '.min.'.$extension)) {
 68                         //将版本号拼接上去,然后得到min的文件路径
 69                         $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension) - 4) . $state . '.' . $extension;
 70                     } else {
 71                         //将版本号拼接上去,然后得到min的文件路径
 72                         $minObject = 'min/'.substr($object, 0, strlen($object) - strlen($extension)) . $state . '.' . $extension;
 73                     }
 74                     //判断min的路径是否存在在$resState里面
 75                      if (isset($resState[$minObject])) {
 76                         $object = $minObject;
 77                         $query = '';
 78                      }
 79                 } 
 80 
 81             }
 82             
 83             $file = RES_BASE_URL . $object;
 84         }
 85 
 86         return ($query == null) ? $file : ($file .'?'. $query);
 87 
 88     }
 89 
 90     /**
 91      * 判断 subject 是否以 search开头, 参数指定是否忽略大小写
 92      * @param  [type]  $subject     [description]
 93      * @param  [type]  $search      [description]
 94      * @param  boolean $ignore_case [description]
 95      * @return [type]               [description]
 96      */
 97     function str_start_with($subject, $search, $ignore_case = false) {
 98         $len2 = strlen($search);
 99         if (0 === $len2) return true;
100         $len1 = strlen($subject);
101         if ($len1 < $len2) return false;
102         if ($ignore_case) {
103             return 0 === strncmp($subject, $search, $len2);
104         } else {
105             return 0 === strncasecmp($subject, $search, $len2);
106         }
107     }
108 
109     /**
110      * 判断 subject 是否以 search结尾, 参数指定是否忽略大小写
111      * @param  [type]  $subject     [description]
112      * @param  [type]  $search      [description]
113      * @param  boolean $ignore_case [description]
114      * @return [type]               [description]
115      */
116     function str_end_with($subject, $search, $ignore_case = false) {
117         $len2 = strlen($search);
118         if (0 === $len2) return true;
119         $len1 = strlen($subject);
120         if ($len2 > $len1) return false;
121         if ($ignore_case) {
122             return 0 === strcmp(substr($subject, $len1 - $len2), $search);
123         } else {
124             return 0 === strcasecmp(substr($subject, $len1 - $len2), $search);
125         }
126     }
点击打开

  $resState.php(里面的代码是自动生成的)

 1 <?php
 2 
 3 $resState = array(
 4      'css/home/index.css' => 'gwy933',
 5      'js/echarts-all.min.js' => 'wqrf1c',
 6      'js/home/index.js' => 's2z6f5',
 7      'js/icon.js' => 'pgcyih',
 8      'js/icon_home.js' => 'zhl9iu',
 9      'js/ion.rangeSlider.min.js' => 'akq381',
10      'js/jquery-ui-autocomplete.js' => '8nzacv',
11      'js/jquery-ui.min.js' => 'i6tw8z',
12      'js/jquery.all.min.js' => 'd2w76v',
13      'js/jquery.city.js' => 'toxdrf',
14      'js/jquery.easydropdown.min.js' => '2ni3i0',
15      'js/jquery.matrix.js' => '3vrqkk',
16      'js/jquery.mobile.all.min.js' => 'ernu7r',
17      'js/jquery.qrcode.min.js' => 'yuhnsj',
18      'js/jquery.tinyscrollbar.min.js' => 'oakk3c',
19      'js/mobiscroll.custom.min.js' => 'kn8h2e',
20      'js/store.min.js' => 'n50jwr',
21      'js/swiper.animate1.0.2.min.js' => 'mm27zc',
22      'js/swiper.min.js' => 'jicwhh',
23      'min/css/home/index.6a4e83eb.css' => '',
24      'min/css/home/index.gwy933.css' => '',
25      'min/css/home/index.puzbnf.css' => '',
26      'min/css/home/index.thv8x7.css' => '',
27      'min/js/echarts-all.76025ee0.js' => '',
28      'min/js/echarts-all.wqrf1c.js' => '',
29      'min/js/home/index.65363d41.js' => '',
30      'min/js/home/index.s2z6f5.js' => '',
31      'min/js/icon.5bbd4db9.js' => '',
32      'min/js/icon.pgcyih.js' => '',
33      'min/js/icon_home.7fe74076.js' => '',
34      'min/js/icon_home.zhl9iu.js' => '',
35      'min/js/ion.rangeSlider.261d8ed1.js' => '',
36      'min/js/ion.rangeSlider.akq381.js' => '',
37      'min/js/jquery-ui-autocomplete.1f3bb62f.js' => '',
38      'min/js/jquery-ui-autocomplete.8nzacv.js' => '',
39      'min/js/jquery-ui.418e9683.js' => '',
40      'min/js/jquery-ui.i6tw8z.js' => '',
41      'min/js/jquery.all.2f248267.js' => '',
42      'min/js/jquery.all.d2w76v.js' => '',
43      'min/js/jquery.city.6b036feb.js' => '',
44      'min/js/jquery.city.toxdrf.js' => '',
45      'min/js/jquery.easydropdown.2ni3i0.js' => '',
46      'min/js/jquery.easydropdown.98fa138.js' => '',
47      'min/js/jquery.matrix.3vrqkk.js' => '',
48      'min/js/jquery.matrix.dfe2a44.js' => '',
49      'min/js/jquery.mobile.all.3539ebb7.js' => '',
50      'min/js/jquery.mobile.all.ernu7r.js' => '',
51      'min/js/jquery.qrcode.7d9738b3.js' => '',
52      'min/js/jquery.qrcode.yuhnsj.js' => '',
53      'min/js/jquery.tinyscrollbar.578e4cb8.js' => '',
54      'min/js/jquery.tinyscrollbar.oakk3c.js' => '',
55      'min/js/mobiscroll.custom.4a684f66.js' => '',
56      'min/js/mobiscroll.custom.kn8h2e.js' => '',
57      'min/js/store.536545cb.js' => '',
58      'min/js/store.n50jwr.js' => '',
59      'min/js/swiper.4650ad75.js' => '',
60      'min/js/swiper.animate1.0.2.517f82e8.js' => '',
61      'min/js/swiper.animate1.0.2.mm27zc.js' => '',
62      'min/js/swiper.jicwhh.js' => '',
63 );
点击打开

  另外附上JShrink这个PHP类的链接给大家下载 http://pan.baidu.com/s/1gd12JT5

  要是大家还是觉得不够OK的话,我直接将这个实验项目打包供大家下载下来学习和了解:http://pan.baidu.com/s/1o6OUYPO

    四、结语

  最后我来分享我们线上项目的具体实现方案:

  我们的项目分线上环境、开发环境和测试环境,在开发和测试环境中,我们每一次访问都会调用压缩文件的接口,然后再对生成的资源文件的大小是要做判断的,如果压缩后文件过小,就要求将该资源文件的代码合并到其他资源文件里去,以此减少不必要的HTTP请求(因为文件太小,资源的下载时间远远小于HTTP请求响应所消耗的时间);另一个是图片的处理,所有图片都要经过压缩才能通过(例如在:https://tinypng.com/  这个网站去压缩图片),在PC端,如果是小图标的话,使用图片合并的方式进行优化,详情可参考本人的这篇博文:http://www.cnblogs.com/it-cen/p/4618954.html    而在wap端的图片处理采用的是base64编码方式来处理图片,详情可以参考本人的这篇博文:http://www.cnblogs.com/it-cen/p/4624939.html  ,当页面输出时,会使用redis来缓存页面(为啥用内存来缓存而不是采用页面缓存,这个以后再分享给大家)。如果是线上环境,每发一次版本,才会调用一下资源文件压缩这个接口,并且线上的静态资源(css、js、图片)是存放在阿里云的OSS里的,与我们的应用服务器是分开的。这是我们线上项目的一部分优化解决方案,当然了,还有更多优化技术,我会在以后一一总结和分享出来,方便大家一起学习和交流。

  本次博文就分享到此,谢谢阅览此博文的朋友们。

 

 

 

  如果此博文中有哪里讲得让人难以理解,欢迎留言交流,若有讲解错的地方欢迎指出。

  

  如果您觉得您能在此博文学到了新知识,请为我顶一个,如文章中有解释错的地方,欢迎指出。

 

  互相学习,共同进步!

 

  

posted @ 2015-10-26 16:25  __kelly_  阅读(6372)  评论(7编辑  收藏  举报