靜態資源文件自動壓縮並替換成壓縮版本(大型網站優化技術)

這一次,我總結和分享一項大型網站優化技術,那就是在項目中自動壓縮靜態資源文件(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 }
複製代碼

   打印出來 $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裏的,與我們的應用服務器是分開的。這是我們線上項目的一部分優化解決方案,當然了,還有更多優化技術,我會在以後一一總結和分享出來,方便大家一起學習和交流。

  本次博文就分享到此,謝謝閱覽此博文的朋友們。

 

 

 

  如果此博文中有哪裏講得讓人難以理解,歡迎留言交流,若有講解錯的地方歡迎指出。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章