您的位置:奥门新浦京网址 > Wed前段 > 英国卫报的个性离线页面是这样做的,缓存机制

英国卫报的个性离线页面是这样做的,缓存机制

发布时间:2019-10-20 03:41编辑:Wed前段浏览(129)

    连不上网?英国卫报的个性离线页面是这样做的

    2015/11/20 · HTML5 · Service Worker, 离线页面

    本文由 伯乐在线 - Erucy 翻译,weavewillg 校稿。未经许可,禁止转载!
    英文出处:Oliver Ash。欢迎加入翻译组。

    我们是如何使用 service worker 来为 theguardian.com 构建一个自定义的离线页面。

    图片 1

    theguardian.com 的离线页面。插图:Oliver Ash

    你正在通往公司路上的地铁里,在手机上打开了 Guardian 应用。地铁被隧道包围着,不过这个应用可以如常运行,即使没有网络连接,你也能获得完整的功能,除了显示的内容可能有点旧。如果你尝试在网站上也这么干,可惜它完全没法加载:

    图片 2

    安卓版 Chrome 的离线页面

    Chrome 中的这个彩蛋,很多人都不知道》

    Chrome 在离线页面上有个隐藏的游戏(桌面版上按空格键,手机版上点击那只恐龙),这多少能减轻一点你的烦躁。不过我们可以做得更好。

    Service workers 允许网站作者拦截自己站点的所有网络请求,这也就意味着我们可以提供完善的离线体验,就像原生应用一样。在 Guardian 网站,我们最近上线了一个自定义的离线体验功能。当用户离线的时候,他们会看到一个带有 Guardian 标识的页面,上面带有一个简短的离线提示,还有一个填字游戏,他们可以在等待网络连接的时候玩玩这个找点乐子。这篇博客解释了我们是如何构建它的,不过在开始之前,你可以先自己试试看。

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

    2015/11/26 · HTML5 · 静态资源

    原文出处: Kelly   

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

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

    一、原理图

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

    首先是资源压缩原理图:

    图片 3

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

    图片 4

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

    二、思路详细分析

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

    class MY_Controller extends CI_Controller { public function __construct() { parent::__construct(); //压缩jscss资源文件 $this->compressResHandle(); } /** * 压缩js、css资源文件(优化) * @return [type] [description] */ private function compressResHandle() { $this->load->library('ResMinifier'); //压缩指定文件夹下的资源文件 $this->resminifier->compressRes(); } }

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

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

    PHP

    <?php defined('BASEPATH') OR exit('No direct script access allowed'); /** * 资源压缩类 */ class ResMinifier { /** 需要压缩的资源目录*/ 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'; } public function compressRes() { //获取存放版本的资源文件 $resState = $this->getResState(); $count = 0; //开始遍历需要压缩的资源目录 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); //获取文件的几个参数值 if (true !== $this->getObjectInfo($object, $minObject, $needCompress, $state, $extension)) { continue; } //压缩文件的绝对路径 $minFilePath = str_replace('\', '/', $this->resRootDir. $minObject); //************此处p判断是最重要部分之一*****************// //判断文件是否存在且已经改动过 if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) { continue; } //确保/www/min/目录可写 $this->_ensureWritableDir(dirname($minFilePath)); if ($needCompress) { $this->compressResFileAndSave($filePath, $minFilePath); } else { copy($filePath, $minFilePath); } $resState[$object] = $state; $resState[$minObject] = ''; $count++; if ($count == 50) { $this->_saveResState($resState); $count = 0; } } } if($count) $this->_saveResState($resState); } 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; } /** * 获取存放资源版本的文件 * 它是放在一个数组里 * $resState = array( * '文件路径' => '对应的版本号', * '文件路径' => '对应的版本号', * '文件路径' => '对应的版本号', * ); * @return [type] [description] */ public function getResState() { if (file_exists($this->resStatePath)) { require $this->resStatePath; return $resState; } return []; } /** * 计算文件的版本号,这个是根据计算文件MD5散列值得到版本号 * 只要文件内容改变了,所计算得到的散列值就会不一样 * 用于判断资源文件是否有改动过 * @param [type] $filePath [description] * @return [type] [description] */ public function _getResStateVersion($filePath) { return base_convert(crc32(md5_file($filePath)), 10, 36); } /** * 确保目录可写 * @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.'不可写'); } } } /** * 将压缩后的资源文件写入到/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 JShrinkMinifier::minify(file_get_contents($filePath)); } else if ($extension ==='css') { $content = file_get_contents($filePath); $content = preg_replace('!/*[^*]**+([^/][^*]**+)*/!', '', $content); $content = str_replace(["rn", "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); } } private function _saveResState($resState) { ksort($resState); $content = "<?phpnn$resState = array(n"; foreach ($resState as $k => $v) { $content .= "t '$k' => '$v',n"; } $content .= ");nn"; file_put_contents($this->resStatePath, $content); } } 点击打开 资源压缩类

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

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

    (1)

    PHP

    /** 需要压缩的资源目录*/ 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'; }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /** 需要压缩的资源目录*/
        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变量是需要压缩的资源目录,假如你有新的处理目录,可以在此变量里假如新的目录名即可处理。附上我测试项目的目录图

    图片 5

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

    $resRootDir存放资源根目录的

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

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

    PHP

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

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

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

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

    PHP

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
         * 获取存放资源版本的文件
         * 它是放在一个数组里
         * $resState = array(
         *         '文件路径' => '对应的版本号',
         *         '文件路径' => '对应的版本号',
         *         '文件路径' => '对应的版本号',
         *     );
         * @return [type] [description]
         */
        public function getResState() {
            if (file_exists($this->resStatePath)) {
                require $this->resStatePath;
                return $resState;
            }
            return [];
        }

    (资源版本文件截图:)

    图片 6

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

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

    PHP

    //开始遍历需要压缩的资源目录 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);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //开始遍历需要压缩的资源目录
            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————————————————————

    PHP

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
         * 计算文件的版本号,这个是根据计算文件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。

    PHP

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

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

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

    PHP

    /** * 获取资源文件相关信息 * @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; }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /**
         * 获取资源文件相关信息
         * @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()函数的代码:

    PHP

    /** * 判断 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); } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
         * 判断 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()方法里面的代码

    PHP

    //压缩文件的绝对路径 $minFilePath = str_replace('\', '/', $this->resRootDir. $minObject); //************此处p判断是最重要部分之一*****************// //判断文件是否存在且已经改动过 if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) { continue; }

    1
    2
    3
    4
    5
    6
    7
    8
    //压缩文件的绝对路径
                    $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),这个是判断压缩文件路径是否存在,并且该压缩文件是否真实存在目录中。

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

    PHP

    //确保/www/min/目录可写 $this->_ensureWritableDir(dirname($minFilePath)); if ($needCompress) { $this->compressResFileAndSave($filePath, $minFilePath); } else { copy($filePath, $minFilePath); }

    1
    2
    3
    4
    5
    6
    7
    8
    //确保/www/min/目录可写
                    $this->_ensureWritableDir(dirname($minFilePath));
     
                    if ($needCompress) {
                        $this->compressResFileAndSave($filePath, $minFilePath);
                    } else {
                        copy($filePath, $minFilePath);
                    }

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

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

    PHP

    /** * 确保目录可写 * @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.'不可写'); } } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
         * 确保目录可写
         * @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————————————————————-

    PHP

    /** * 将压缩后的资源文件写入到/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 JShrinkMinifier::minify(file_get_contents($filePath)); } else if ($extension ==='css') { $content = file_get_contents($filePath); $content = preg_replace('!/*[^*]**+([^/][^*]**+)*/!', '', $content); $content = str_replace(["rn", "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); } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    /**
         * 将压缩后的资源文件写入到/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 JShrinkMinifier::minify(file_get_contents($filePath));
            } else if ($extension ==='css') {
                $content = file_get_contents($filePath);
                $content = preg_replace('!/*[^*]**+([^/][^*]**+)*/!', '', $content);
                $content = str_replace(["rn", "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的处理这部分也可以,最后写入一次就行了。

    PHP

    $resState[$object] = $state; $resState[$minObject] = ''; $count++; if ($count == 50) { $this->_saveResState($resState); $count = 0; } } } if($count) $this->_saveResState($resState);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $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————————————————————-

    PHP

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private function _saveResState($resState) {
            ksort($resState);
            $content = "<?phpnn$resState = array(n";
            foreach ($resState as $k => $v) {
                $content .= "t '$k' => '$v',n";
            }
            $content .= ");nn";
            file_put_contents($this->resStatePath, $content);
        }

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

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

    图片 7

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

    图片 8

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

    ————————————————————————————————————————-

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

    这个到Home.php

    PHP

    <?php defined('BASEPATH') OR exit('No direct script access allowed'); class Home extends MY_Controller { public function index() { $this->smartyData['test'] = 111; //这个默认是加载 www/css/home/index.css文件 $this->addResLink('index.css'); //这个默认是加载www/js/jquery.all.min.js文件 $this->addResLink('/jquery.all.min.js'); //这个默认是加载www/js/index.js文件 $this->addResLink('index.js'); $this->displayView('home/index.tpl'); } }

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

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

    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; }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
         * 资源路径
         * @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这个父类的所有代码吧

    PHP

    <?php defined('BASEPATH') OR exit('No direct script access allowed'); class MY_Controller extends CI_Controller { public function __construct() { parent::__construct(); //压缩jscss资源文件 $this->compressResHandle(); } //==========================使用SMARTY模板引擎================================// /* Smarty母版页文件路径 */ protected $masterPage = 'default.tpl'; /* 视图文件路径*/ protected $smartyView; /* 要赋值给smarty视图的数据*/ protected $smartyData = []; /* 资源文件*/ protected $_resLink = ['js'=>[], 'css'=>[]]; /** * 使用母版页输出一个视图 * @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); } } /** * 资源路径 * @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; } private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) { if ('/' === $viewName[0]) $viewName = substr($viewName, 1); //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去 if ($masterPage) { $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage); } //是否设置模板目录 if ($setTemplateDir) { $templateDir = VIEWPATH; $this->smarty->setTemplateDir($templateDir); } } /** * 压缩js、css资源文件(优化) * @return [type] [description] */ private function compressResHandle() { $this->load->library('ResMinifier'); //压缩指定文件夹下的资源文件 $this->resminifier->compressRes(); } } 点击打开 My_Controller.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    <?php
    defined('BASEPATH') OR exit('No direct script access allowed');
     
    class MY_Controller extends CI_Controller {
        public function __construct() {
            parent::__construct();
     
            //压缩jscss资源文件
            $this->compressResHandle();
        }
     
        //==========================使用SMARTY模板引擎================================//
        /* Smarty母版页文件路径 */
        protected $masterPage = 'default.tpl';
        /* 视图文件路径*/
        protected $smartyView;
        /* 要赋值给smarty视图的数据*/
        protected $smartyData = [];
        /* 资源文件*/
        protected $_resLink = ['js'=>[], 'css'=>[]];
     
        /**
         * 使用母版页输出一个视图
         * @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);
            }
        }
     
        /**
         * 资源路径
         * @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;
        }
     
        private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) {
            if ('/' === $viewName[0]) $viewName = substr($viewName, 1);
     
            //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去
            if ($masterPage) {
                $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage);
            }
     
            //是否设置模板目录
            if ($setTemplateDir) {
                $templateDir = VIEWPATH;
                $this->smarty->setTemplateDir($templateDir);
            }
        }
     
        /**
         * 压缩js、css资源文件(优化)
         * @return [type] [description]
         */
        private function compressResHandle() {
            $this->load->library('ResMinifier');
            //压缩指定文件夹下的资源文件
            $this->resminifier->compressRes();
        }
    }
     
    点击打开 My_Controller.php

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

    PHP

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Array
    (
        [js] => Array
            (
                [0] => /jquery.all.min.js
                [1] => index.js
            )
     
        [css] => Array
            (
                [0] => index.css
            )
     
    )

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

    我们看这个方法:

    PHP

    /** * 使用母版页输出一个视图 * @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); } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    /**
         * 使用母版页输出一个视图
         * @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() 这个函数,就是通过这个函数来进行资源文件替换的。先看这个函数的代码:

    PHP

    /** * 输出 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; } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
         * 输出 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

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

    直接看代码:

    PHP

    /** * 智能路由资源真实路径 * @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); }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    /**
         * 智能路由资源真实路径
         * @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

    PHP

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

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

    My_Controller.php

    PHP

    <?php defined('BASEPATH') OR exit('No direct script access allowed'); class MY_Controller extends CI_Controller { public function __construct() { parent::__construct(); //压缩jscss资源文件 $this->compressResHandle(); } //==========================使用SMARTY模板引擎================================// /* Smarty母版页文件路径 */ protected $masterPage = 'default.tpl'; /* 视图文件路径*/ protected $smartyView; /* 要赋值给smarty视图的数据*/ protected $smartyData = []; /* 资源文件*/ protected $_resLink = ['js'=>[], 'css'=>[]]; /** * 使用母版页输出一个视图 * @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); } } /** * 资源路径 * @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; } private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) { if ('/' === $viewName[0]) $viewName = substr($viewName, 1); //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去 if ($masterPage) { $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage); } //是否设置模板目录 if ($setTemplateDir) { $templateDir = VIEWPATH; $this->smarty->setTemplateDir($templateDir); } } /** * 压缩js、css资源文件(优化) * @return [type] [description] */ private function compressResHandle() { $this->load->library('ResMinifier'); //压缩指定文件夹下的资源文件 $this->resminifier->compressRes(); } } 点击打开

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    <?php
    defined('BASEPATH') OR exit('No direct script access allowed');
     
    class MY_Controller extends CI_Controller {
        public function __construct() {
            parent::__construct();
     
            //压缩jscss资源文件
            $this->compressResHandle();
        }
     
        //==========================使用SMARTY模板引擎================================//
        /* Smarty母版页文件路径 */
        protected $masterPage = 'default.tpl';
        /* 视图文件路径*/
        protected $smartyView;
        /* 要赋值给smarty视图的数据*/
        protected $smartyData = [];
        /* 资源文件*/
        protected $_resLink = ['js'=>[], 'css'=>[]];
     
        /**
         * 使用母版页输出一个视图
         * @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);
            }
        }
     
        /**
         * 资源路径
         * @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;
        }
     
        private function _getViewDir($setTemplateDir, &$viewName, &$masterPage = null, &$templateDir) {
            if ('/' === $viewName[0]) $viewName = substr($viewName, 1);
     
            //是否使用模板,有,则路由到 /views/master_page/*****.tpl下去
            if ($masterPage) {
                $masterPage = '/' === $masterPage[0] ? substr($masterPage, 1) : ('master_page' .'/'. $masterPage);
            }
     
            //是否设置模板目录
            if ($setTemplateDir) {
                $templateDir = VIEWPATH;
                $this->smarty->setTemplateDir($templateDir);
            }
        }
     
        /**
         * 压缩js、css资源文件(优化)
         * @return [type] [description]
         */
        private function compressResHandle() {
            $this->load->library('ResMinifier');
            //压缩指定文件夹下的资源文件
            $this->resminifier->compressRes();
        }
    }
     
    点击打开

    ResMinifier.php

    PHP

    <?php defined('BASEPATH') OR exit('No direct script access allowed'); /** * 资源压缩类 */ class ResMinifier { /** 需要压缩的资源目录*/ 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'; } public function compressRes() { //获取存放版本的资源文件 $resState = $this->getResState(); $count = 0; //开始遍历需要压缩的资源目录 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); //获取文件的几个参数值 if (true !== $this->getObjectInfo($object, $minObject, $needCompress, $state, $extension)) { continue; } //压缩文件的绝对路径 $minFilePath = str_replace('\', '/', $this->resRootDir. $minObject); //************此处p判断是最重要部分之一*****************// //判断文件是否存在且已经改动过 if (isset($resState[$object]) && $resState[$object] == $state && isset($resState[$minObject]) && file_exists($minFilePath)) { continue; } //确保/www/min/目录可写 $this->_ensureWritableDir(dirname($minFilePath)); if ($needCompress) { $this->compressResFileAndSave($filePath, $minFilePath); } else { copy($filePath, $minFilePath); } $resState[$object] = $state; $resState[$minObject] = ''; $count++; if ($count == 50) { $this->_saveResState($resState); $count = 0; } } } if($count) $this->_saveResState($resState); } /** * 获取资源文件相关信息 * @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; } /** * 获取存放资源版本的文件 * 它是放在一个数组里 * $resState = array( * '文件路径' => '对应的版本号', * '文件路径' => '对应的版本号', * '文件路径' => '对应的版本号', * ); * @return [type] [description] */ public function getResState() { if (file_exists($this->resStatePath)) { require $this->resStatePath; return $resState; } return []; } /** * 计算文件的版本号,这个是根据计算文件MD5散列值得到版本号 * 只要文件内容改变了,所计算得到的散列值就会不一样 * 用于判断资源文件是否有改动过 * @param [type] $filePath [description] * @return [type] [description] */ public function _getResStateVersion($filePath) { return base_convert(crc32(md5_file($filePath)), 10, 36); } /** * 确保目录可写 * @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.'不可写'); } } } /** * 将压缩后的资源文件写入到/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 JShrinkMinifier::minify(file_get_contents($filePath)); } else if ($extension ==='css') { $content = file_get_contents($filePath); $content = preg_replace('!/*[^*]**+([^/][^*]**+)*/!', '', $content); $content = str_replace(["rn", "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); } } private function _saveResState($resState) { ksort($resState); $content = "<?phpnn$resState = array(n"; foreach ($resState as $k => $v) { $content .= "t '$k' => '$v',n"; } $content .= ");nn"; file_put_contents($this->resStatePath, $content); } } 点击打开

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

    Common.php

    PHP

    <?php /** * 输出 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; } } /** * 智能路由资源真实路径 * @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); } /** * 判断 subject 是否以 search开头, 参数指定是否忽略大小写 * @param [type] $subject [description] * @param [type] $search [description] * @param boolean $ignore_case [description] * @return [type] [description] */ function str_start_with($subject, $search, $ignore_case = false) { $len2 = strlen($search); if (0 === $len2) return true; $len1 = strlen($subject); if ($len1 < $len2) return false; if ($ignore_case) { return 0 === strncmp($subject, $search, $len2); } else { return 0 === strncasecmp($subject, $search, $len2); } } /** * 判断 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); } } 点击打开

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    <?php
        /**
         * 输出 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;
            }
        }
     
        /**
         * 智能路由资源真实路径
         * @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);
     
        }
     
        /**
         * 判断 subject 是否以 search开头, 参数指定是否忽略大小写
         * @param  [type]  $subject     [description]
         * @param  [type]  $search      [description]
         * @param  boolean $ignore_case [description]
         * @return [type]               [description]
         */
        function str_start_with($subject, $search, $ignore_case = false) {
            $len2 = strlen($search);
            if (0 === $len2) return true;
            $len1 = strlen($subject);
            if ($len1 < $len2) return false;
            if ($ignore_case) {
                return 0 === strncmp($subject, $search, $len2);
            } else {
                return 0 === strncasecmp($subject, $search, $len2);
            }
        }
     
        /**
         * 判断 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);
            }
        }
     
    点击打开

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

    XHTML

    <?php $resState = array( 'css/home/index.css' => 'gwy933', 'js/echarts-all.min.js' => 'wqrf1c', 'js/home/index.js' => 's2z6f5', 'js/icon.js' => 'pgcyih', 'js/icon_home.js' => 'zhl9iu', 'js/ion.rangeSlider.min.js' => 'akq381', 'js/jquery-ui-autocomplete.js' => '8nzacv', 'js/jquery-ui.min.js' => 'i6tw8z', 'js/jquery.all.min.js' => 'd2w76v', 'js/jquery.city.js' => 'toxdrf', 'js/jquery.easydropdown.min.js' => '2ni3i0', 'js/jquery.matrix.js' => '3vrqkk', 'js/jquery.mobile.all.min.js' => 'ernu7r', 'js/jquery.qrcode.min.js' => 'yuhnsj', 'js/jquery.tinyscrollbar.min.js' => 'oakk3c', 'js/mobiscroll.custom.min.js' => 'kn8h2e', 'js/store.min.js' => 'n50jwr', 'js/swiper.animate1.0.2.min.js' => 'mm27zc', 'js/swiper.min.js' => 'jicwhh', 'min/css/home/index.6a4e83eb.css' => '', 'min/css/home/index.gwy933.css' => '', 'min/css/home/index.puzbnf.css' => '', 'min/css/home/index.thv8x7.css' => '', 'min/js/echarts-all.76025ee0.js' => '', 'min/js/echarts-all.wqrf1c.js' => '', 'min/js/home/index.65363d41.js' => '', 'min/js/home/index.s2z6f5.js' => '', 'min/js/icon.5bbd4db9.js' => '', 'min/js/icon.pgcyih.js' => '', 'min/js/icon_home.7fe74076.js' => '', 'min/js/icon_home.zhl9iu.js' => '', 'min/js/ion.rangeSlider.261d8ed1.js' => '', 'min/js/ion.rangeSlider.akq381.js' => '', 'min/js/jquery-ui-autocomplete.1f3bb62f.js' => '', 'min/js/jquery-ui-autocomplete.8nzacv.js' => '', 'min/js/jquery-ui.418e9683.js' => '', 'min/js/jquery-ui.i6tw8z.js' => '', 'min/js/jquery.all.2f248267.js' => '', 'min/js/jquery.all.d2w76v.js' => '', 'min/js/jquery.city.6b036feb.js' => '', 'min/js/jquery.city.toxdrf.js' => '', 'min/js/jquery.easydropdown.2ni3i0.js' => '', 'min/js/jquery.easydropdown.98fa138.js' => '', 'min/js/jquery.matrix.3vrqkk.js' => '', 'min/js/jquery.matrix.dfe2a44.js' => '', 'min/js/jquery.mobile.all.3539ebb7.js' => '', 'min/js/jquery.mobile.all.ernu7r.js' => '', 'min/js/jquery.qrcode.7d9738b3.js' => '', 'min/js/jquery.qrcode.yuhnsj.js' => '', 'min/js/jquery.tinyscrollbar.578e4cb8.js' => '', 'min/js/jquery.tinyscrollbar.oakk3c.js' => '', 'min/js/mobiscroll.custom.4a684f66.js' => '', 'min/js/mobiscroll.custom.kn8h2e.js' => '', 'min/js/store.536545cb.js' => '', 'min/js/store.n50jwr.js' => '', 'min/js/swiper.4650ad75.js' => '', 'min/js/swiper.animate1.0.2.517f82e8.js' => '', 'min/js/swiper.animate1.0.2.mm27zc.js' => '', 'min/js/swiper.jicwhh.js' => '', ); 点击打开

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

     

    另外附上JShrink这个PHP类的链接给大家下载 

    要是大家还是觉得不够OK的话,我直接将这个实验项目打包供大家下载下来学习和了解:

    四、结语

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

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

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

    1 赞 1 收藏 评论

    图片 9

    H5 缓存机制浅析,移动端 Web 加载性能优化

    2015/12/14 · HTML5 · IndexedDB, 性能, 移动前端

    本文作者: 伯乐在线 - 腾讯bugly 。未经作者许可,禁止转载!
    欢迎加入伯乐在线 专栏作者。

    试试看

    你需要一个支持 Service Worker 和 fetch API 的浏览器。截止到本文编写时只有 Chrome(手机版和桌面版)同时支持这两种 API(译者注:Opera 目前也支持这两者),不过 Firefox 很快就要支持了(在每日更新的版本中已经支持了),除了 Safari 之外的所有浏览器也都在跃跃欲试。此外,service worker 只能注册在使用了 HTTPS 的网站上,theguardian.com 已经开始逐步迁移到 HTTPS,所以我们只能在网站的 HTTPS 部分提供离线体验。就目前来说,我们选择了 开发者博客 作为我们用来测试的地方。所以如果你是在我们网站的 开发者博客 部分阅读这篇文章的话,很走运。

    当你使用支持的浏览器访问我们的 开发者博客 中的页面的时候,一切就准备妥当了。断开你的网络连接,然后刷新一下页面。如果你自己没条件尝试的话,可以看一下这段 演示视频(译者注:需梯子)。

    1 H5 缓存机制介绍

    H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性。离线存储(也可称为缓存机制)是其中一个非常重要的特性。H5 引入的离线存储,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。

    H5 应用程序缓存为应用带来三个优势:

    • 离线浏览 用户可在应用离线时使用它们
    • 速度 已缓存资源加载得更快
    • 减少服务器负载 浏览器将只从服务器下载更新过或更改过的资源。

    根据标准,到目前为止,H5 一共有6种缓存机制,有些是之前已有,有些是 H5 才新加入的。

    1. 浏览器缓存机制
    2. Dom Storgage(Web Storage)存储机制
    3. Web SQL Database 存储机制
    4. Application Cache(AppCache)机制
    5. Indexed Database (IndexedDB)
    6. File System API

    下面我们首先分析各种缓存机制的原理、用法及特点;然后针对 Anroid 移动端 Web 性能加载优化的需求,看如果利用适当缓存机制来提高 Web 的加载性能。


    工作原理

    通过一段简单的 JavaScript,我们可以指示浏览器在用户访问页面的时候立即注册我们自己的 service worker。目前支持 service worker 的浏览器很少,所以为了避免错误,我们需要使用特性检测。

    JavaScript

    if (navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js'); }

    1
    2
    3
    if (navigator.serviceWorker) {
        navigator.serviceWorker.register('/service-worker.js');
    }

    Service worker 安装事件的一部分,我们可以使用 新的缓存 API 来缓存我们网站中的各种内容,比如 HTML、CSS 和 JavaScript:

    JavaScript

    var staticCacheName = 'static'; var version = 1; function updateCache() { return caches.open(staticCacheName + version) .then(function (cache) { return cache.addAll([ '/offline-page.html', '/assets/css/main.css', '/assets/js/main.js' ]); }); }; self.addEventListener('install', function (event) { event.waitUntil(updateCache()); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var staticCacheName = 'static';
    var version = 1;
     
    function updateCache() {
        return caches.open(staticCacheName + version)
            .then(function (cache) {
                return cache.addAll([
                    '/offline-page.html',
                    '/assets/css/main.css',
                    '/assets/js/main.js'
                ]);
            });
    };
     
    self.addEventListener('install', function (event) {
        event.waitUntil(updateCache());
    });

    当安装完成后,service worker 可以监听和控制 fetch 事件,让我们可以完全控制之后网站中产生的所有网络请求。

    JavaScript

    self.addEventListener('fetch', function (event) { event.respondWith(fetch(event.request)); });

    1
    2
    3
    self.addEventListener('fetch', function (event) {
        event.respondWith(fetch(event.request));
    });

    在这里我们有很灵活的空间可以发挥,比如下面这个点子,可以通过代码来生成我们自己的请求响应:

    JavaScript

    self.addEventListener('fetch', function (event) { var response = new Response('<h1>Hello, World!</h1>', { headers: { 'Content-Type': 'text/html' } }); event.respondWith(response); });

    1
    2
    3
    4
    5
    self.addEventListener('fetch', function (event) {
        var response = new Response('&lt;h1&gt;Hello, World!&lt;/h1&gt;',
            { headers: { 'Content-Type': 'text/html' } });
        event.respondWith(response);
    });

    还有这个,如果在缓存中找到了请求相应的缓存,我们可以直接从缓存中返回它,如果没找到的话,再通过网络获取响应内容:

    JavaScript

    self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request) .then(function (response) { return response || fetch(event.request); }) ); });

    1
    2
    3
    4
    5
    6
    7
    8
    self.addEventListener('fetch', function (event) {
        event.respondWith(
            caches.match(event.request)
                .then(function (response) {
                    return response || fetch(event.request);
                })
        );
    });

    那么我们如何使用这些功能来提供离线体验呢?

    首先,在 service worker 安装过程中,我们需要把离线页面需要的 HTML 和资源文件通过 service worker 缓存下来。在缓存中,我们加载了自己开发的 填字游戏 的 React应用 页面。之后,我们会拦截所有访问 theguardian.com 网络请求,包括网页、以及页面中的资源文件。处理这些请求的逻辑大致如下:

    1. 当我们检测到传入请求是指向我们的 HTML 页面时,我们总是会想要提供最新的内容,所以我们会尝试把这个请求通过网络发送给服务器。
      1. 当我们从服务器得到了响应,就可以直接返回这个响应。
      2. 如果网络请求抛出了异常(比如因为用户掉线了),我们捕获这个异常,然后使用缓存的离线 HTML 页面作为响应内容。
    2. 否则,当我们检测到请求的不是 HTML 的话,我们会从缓存中查找响应的请求内容。
      1. 如果找到了缓存内容,我们可以直接返回缓存的内容。
      2. 否则,我们会尝试把这个请求通过网络发送给服务器。

    在代码中,我们使用了 新的缓存 API(它是 Service Worker API 的一部分)以及 fetch 功能(用于生成网络请求),如下所示:

    JavaScript

    var doesRequestAcceptHtml = function (request) { return request.headers.get('Accept') .split(',') .some(function (type) { return type === 'text/html'; }); }; self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request)) { // HTML pages fallback to offline page event.respondWith( fetch(request) .catch(function () { return caches.match('/offline-page.html'); }) ); } else { // Default fetch behaviour // Cache first for all other requests event.respondWith( caches.match(request) .then(function (response) { return response || fetch(request); }) ); } });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var doesRequestAcceptHtml = function (request) {
        return request.headers.get('Accept')
            .split(',')
            .some(function (type) { return type === 'text/html'; });
    };
     
    self.addEventListener('fetch', function (event) {
        var request = event.request;
        if (doesRequestAcceptHtml(request)) {
            // HTML pages fallback to offline page
            event.respondWith(
                fetch(request)
                    .catch(function () {
                        return caches.match('/offline-page.html');
                    })
            );
        } else {
            // Default fetch behaviour
            // Cache first for all other requests
            event.respondWith(
                caches.match(request)
                    .then(function (response) {
                        return response || fetch(request);
                    })
            );
        }
    });

    就只需要这么多!theguardian.com 上的 所有代码都是在 GitHub 上开源 的,所以你可以去那儿查看我们的 service worker 的完整版本,或者直接从生产环境上访问 。

    我们有充足的理由为这些新的浏览器技术欢呼喝彩,因为它可以用来让你的网站像今天的原生应用一样,拥有完善的离线体验。未来当 theguardian.com 完全迁移到 HTTPS 之后,离线页面的重要性会明显增加,我们可以提供更加完善的离线体验。设想一下你在上下班路上网络很差的时候访问 theguardian.com,你会看到专门为你订制的个性化内容,它们是在你之前访问网站时由浏览器缓存下来的。它在安装过程中也不会产生任何不便,你所需要的只是访问这个网站而已,不像原生应用,还需要用户有一个应用商店的账号才能安装。Service worker 同样可以帮助我们提升网站的加载速度,因为网站的框架能够被可靠地缓存下来,就像原生应用一样。

    如果你对 service worker 很感兴趣,想要了解更多内容的话,开发者 Matt Gaunt(Chrome的忠实支持者)写了一篇更加详细地 介绍 Service Worker的文章。

    打赏支持我翻译更多好文章,谢谢!

    打赏译者

    2 H5 缓存机制原理分析

    打赏支持我翻译更多好文章,谢谢!

    图片 10

    1 赞 收藏 评论

    2.1 浏览器缓存机制

    浏览器缓存机制是指通过 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制文件缓存的机制。这应该是 WEB 中最早的缓存机制了,是在 HTTP 协议中实现的,有点不同于 Dom Storage、AppCache 等缓存机制,但本质上是一样的。可以理解为,一个是协议层实现的,一个是应用层实现的。

    Cache-Control 用于控制文件在本地缓存有效时长。最常见的,比如服务器回包:Cache-Control:max-age=600 表示文件在本地应该缓存,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。

    Last-Modified 是标识文件在服务器上的最新更新时间。下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。

    Cache-Control 通常与 Last-Modified 一起使用。一个用于控制缓存有效时间,一个在缓存失效后,向服务查询是否有更新。

    Cache-Control 还有一个同功能的字段:Expires。Expires 的值一个绝对的时间点,如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在这个时间点之前,缓存都是有效的。

    Expires 是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段,功能一样,都是控制缓存的有效时间。当这两个字段同时出现时,Cache-Control 是高优化级的。

    Etag 也是和 Last-Modified 一样,对文件进行标识的字段。不同的是,Etag 的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有更新回包304,有更新回包200。Etag 和 Last-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。

    另外有两种特殊的情况:

    • 手动刷新页面(F5),浏览器会直接认为缓存已经过期(可能缓存还没有过期),在请求中加上字段:Cache-Control:max-age=0,发包向服务器查询是否有文件是否有更新。
    • 强制刷新页面(Ctrl+F5),浏览器会直接忽略本地的缓存(有缓存也会认为本地没有缓存),在请求中加上字段:Cache-Control:no-cache(或 Pragma:no-cache),发包向服务重新拉取文件。

    下面是通过 Google Chrome 浏览器(用其他浏览器+抓包工具也可以)自带的开发者工具,对一个资源文件不同情况请求与回包的截图。

    首次请求:200

    图片 11

    缓存有效期内请求:200(from cache)

    图片 12

    缓存过期后请求:304(Not Modified)

    图片 13

    一般浏览器会将缓存记录及缓存文件存在本地 Cache 文件夹中。Android 下 App 如果使用 Webview,缓存的文件记录及文件内容会存在当前 app 的 data 目录中。

    分析:Cache-Control 和 Last-Modified 一般用在 Web 的静态资源文件上,如 JS、CSS 和一些图像文件。通过设置资源文件缓存属性,对提高资源文件加载速度,节省流量很有意义,特别是移动网络环境。但问题是:缓存有效时长该如何设置?如果设置太短,就起不到缓存的使用;如果设置的太长,在资源文件有更新时,浏览器如果有缓存,则不能及时取到最新的文件。

    Last-Modified 需要向服务器发起查询请求,才能知道资源文件有没有更新。虽然服务器可能返回304告诉没有更新,但也还有一个请求的过程。对于移动网络,这个请求可能是比较耗时的。有一种说法叫“消灭304”,指的就是优化掉304的请求。

    抓包发现,带 if-Modified-Since 字段的请求,如果服务器回包304,回包带有 Cache-Control:max-age 或 Expires 字段,文件的缓存有效时间会更新,就是文件的缓存会重新有效。304回包后如果再请求,则又直接使用缓存文件了,不再向服务器查询文件是否更新了,除非新的缓存时间再次过期。

    另外,Cache-Control 与 Last-Modified 是浏览器内核的机制,一般都是标准的实现,不能更改或设置。以 QQ 浏览器的 X5为例,Cache-Control 与 Last-Modified 缓存不能禁用。缓存容量是12MB,不分HOST,过期的缓存会最先被清除。如果都没过期,应该优先清最早的缓存或最快到期的或文件大小最大的;过期缓存也有可能还是有效的,清除缓存会导致资源文件的重新拉取。

    还有,浏览器,如 X5,在使用缓存文件时,是没有对缓存文件内容进行校验的,这样缓存文件内容被修改的可能。

    分析发现,浏览器的缓存机制还不是非常完美的缓存机制。完美的缓存机制应该是这样的:

    1. 缓存文件没更新,尽可能使用缓存,不用和服务器交互;
    2. 缓存文件有更新时,第一时间能使用到新的文件;
    3. 缓存的文件要保持完整性,不使用被修改过的缓存文件;
    4. 缓存的容量大小要能设置或控制,缓存文件不能因为存储空间限制或过期被清除。
      以X5为例,第1、2条不能同时满足,第3、4条都不能满足。

    在实际应用中,为了解决 Cache-Control 缓存时长不好设置的问题,以及为了”消灭304“,Web前端采用的方式是:

    1. 在要缓存的资源文件名中加上版本号或文件 MD5值字串,如 common.d5d02a02.js,common.v1.js,同时设置 Cache-Control:max-age=31536000,也就是一年。在一年时间内,资源文件如果本地有缓存,就会使用缓存;也就不会有304的回包。
    2. 如果资源文件有修改,则更新文件内容,同时修改资源文件名,如 common.v2.js,html页面也会引用新的资源文件名。

    通过这种方式,实现了:缓存文件没有更新,则使用缓存;缓存文件有更新,则第一时间使用最新文件的目的。即上面说的第1、2条。第3、4条由于浏览器内部机制,目前还无法满足。

    本文由奥门新浦京网址发布于Wed前段,转载请注明出处:英国卫报的个性离线页面是这样做的,缓存机制

    关键词: