减少HTTP请求之合并图片详解(大型网站优化技术)

jopen 8年前

 

一、相关知识讲解

看过雅虎的前端优化35条建议,都知道优化前端是有多么重要。页面的加载速度直接影响到用户的体验。80%的终端用户响应时间都花在了前端上,其中大部分时间都在下载页面上的各种组件:图片,样式表,脚本,Flash等等。

减少组件数必然能够减少页面提交的HTTP请求数。这是让页面更快的关键。减少页面组件数的一种方式是简化页面设计。但有没有一种方法可以在构建复杂的页面同时加快响应时间呢?嗯,确实有鱼和熊掌兼得的办法。

这里我们就拿雅虎的第一条建议:尽量减少HTTP请求数里的减少图片请求数量 进行讲解。

我们都知道,一个网站的一个页面可能有很多小图标,例如一些按钮、箭头等等。当加载html文档时,只要遇到有图片的,都会自动建立起HTTP请 求下载,然后将图片下载到页面上,这些小图片可能也就是十几K大甚至1K都不到,假如我们的一个页面有一百个小图标,我们在加载页面时,就要发送100个 HTTP请求,如果你的网站访问量很大并发量也很高,假如上百万访问量,那发起的请求就是千万级别了,服务器是有一定的压力的,并且一个用户的一个页面要 发起那么多请求,是很耗时的。

所以,我们优化的方案就是:将这些十几K、几K的小图标合并在一张图片里,然后用CSS的background-image和background-position属性来定位要显示的部分。

、代码实现

1、思路:

将一个文件夹里的图标,自动生成在一张图片里面,同时自动生成对应的css文件,我们只要在HTML里的标签中添加相应的属性值就能显示图片了。

2、实现过程:

XHTML

<?php      //自己定义一个根目录      define('ROOT', $_SERVER['DOCUMENT_ROOT'].'iconwww');      //这个是图片的目录      define('RES_BASE_URL', 'http://localhost:8080/iconwww/img');        /**       * 生成背景图的函数       */      function generateIcon() {          //网站根目录          $webRoot = rtrim(ROOT, '/');          //背景图目录          $root = "$webRoot/img/bg";          //Php-SPL库中 的 目录文件遍历器          $iterator = new DirectoryIterator($root);          //开始遍历该背景图目录下的目录,我们是把想生成背景图的目录,放在bg目录中以各个模块的目录分类存放          foreach ($iterator as $file) {              //遇到目录遍历              if (!$file->isDot() && $file->isDir()) {                  //取得文件名                  $fileName = $file->getFilename();                  generateIconCallback("$root/$fileName", "$webRoot/img/$fileName", "$webRoot/css/$fileName.css");              }          }      }        /**       * 用户生成合并的背景图和css文件的函数       * @param  string $dir         生成背景图的图标所在的目录路径       * @param  string $bgSavePath  背景图所保存的路径       * @param  string $cssSavePath css保存的路径       */      function generateIconCallback($dir, $bgSavePath, $cssSavePath) {          $shortDir = str_replace('\\', '/', substr($dir, strlen(ROOT-1)));          //返回文件路径信息          $pathInfo = pathinfo($bgSavePath.'.png');            $bgSaveDir = $pathInfo['dirname'];          //确保目录可写          ensure_writable_dir($bgSaveDir);          //背景图名字          $bgName = $pathInfo['filename'];          //调用generateIconCallback_GetFileMap()函数生成每一个图标所需要的数据结构          $fileMap = array('a' => generateIconCallback_GetFileMap($dir));            $iterator = new DirectoryIterator($dir);          foreach ($iterator as $file) {              if ($file->isDot()) continue;              if ($file->isDir()) {                  //二级目录也要处理                  $fileMap['b-'.$file->getFilename()] = generateIconCallback_GetFileMap($file->getRealPath());              }           }          ksort($fileMap);            //分析一边fileMap,计算整个背景图的大小和每一个图标的offset          //初始化偏移量和背景图              $offsetX = $offsetY = $bgWidth = 0;          //设定每个小图标之间的距离          $spaceX =$spaceY = 5;          //图片最大宽度          $maxWidth = 800;          $fileMd5List =array();          //这里需要打印下$fileMap就知道它的数据结构了          foreach ($fileMap as $k1 => $innerMap) {              foreach ($innerMap as $k2 => $itemList) {                  //行高姐X轴偏移量初始化                  $offsetX = $lineHeight = 0;                  foreach ($itemList as $k3 => $item) {                      //变量分别是:图标的宽度,高度,类型,文件名,路径,MD5加密字符串                      list($imageWidth, $imageHeight, $imageType, $fileName, $filePathname, $fileMd5) = $item;                      $fileMd5List []= $fileMd5;                      //如果图片的宽度+偏移量 > 最大宽度(800) 那就换行                      if ($offsetX !== 0 && $imageWidth + $offsetX > $maxWidth) {                          $offsetY += $spaceY + $lineHeight;                          $offsetX = $lineHeight = 0;                      }                      //如果图片高度 > 当前行高  那就讲图片高度付给行高我们这的                      if ($imageHeight > $lineHeight) $lineHeight = $imageHeight;                      $fileMap[$k1][$k2][$k3] = array($imageWidth, $imageHeight, $offsetX, $offsetY, $imageType, $fileName, $filePathname);                      //X轴偏移量的计算                      $offsetX += $imageWidth + $spaceX;                      if ($offsetX > $bgWidth) $bgWidth = $offsetX;                  }                  //Y轴偏移量的计算                  $offsetY +=  $lineHeight + $spaceY;              }          }          //把右下两边多加了的空白距离给干掉          $bgWidth -= $spaceX;          $bgHeight = $offsetY - $spaceY;          $fileMd5List = implode("\n", $fileMd5List);            //生成背景图和 css文件            //资源路径          $resBaseUrl = RES_BASE_URL;          $suffix = base_convert(abs(crc32($fileMd5List)), 10, 36);          $writeHandle = fopen($cssSavePath, 'w');          fwrite($writeHandle, "/** bg in dir: $shortDir/ */\n[icon-$bgName]{background:url({$resBaseUrl}/$bgName.png?$suffix) no-repeat;display:inline-block;}");            //做图片,这些函数具体可以查看PHP手册          $destResource = imagecreatetruecolor($bgWidth, $bgHeight);          imagealphablending($destResource, false);          imagesavealpha($destResource, false);          $color = imagecolorallocatealpha($destResource, 255, 255, 255, 127);            imagefill($destResource, 0, 0, $color);            //对每一张小图片进行处理,生成在大背景图里,并生成css文件          foreach ($fileMap as $innerMap) {              foreach ($innerMap as $itemList) {                  foreach ($itemList as $item) {                       list($imageWidth, $imageHeight, $offsetX, $offsetY, $imageType, $fileName, $filePathname) = $item;                       if ($imageType === IMAGETYPE_PNG) {                          $srcResource = imagecreatefrompng($filePathname);                       } else if ($imageType === IMAGETYPE_JPEG) {                          $srcResource = imagecreatefromjpeg($filePathname);                       }                       imagecopy($destResource, $srcResource, $offsetX, $offsetY, 0, 0, $imageWidth, $imageHeight);                       imagedestroy($srcResource);                         //写入css                       $posX = $offsetX === 0 ? 0 : "-{$offsetX}px";                       $posY = $offsetY === 0 ? 0 : "-{$offsetY}px";                       fwrite($writeHandle, "\n[icon-$bgName=\"$fileName\"]{width:{$imageWidth}px;height:{$imageHeight}px;background-position:$posX $posY;}");                   }               }          }            //压缩级别 7          imagepng($destResource, "$bgSavePath.png", 7);          imagedestroy($destResource);          fclose($writeHandle);            $shortCssSavePath = substr($cssSavePath, strlen(ROOT));      }        /**       * 将图片的信息处理成我们想要的数据结构       * @param  [type] $dir [description]       * @return [type]      [description]       */      function generateIconCallback_GetFileMap($dir) {          $map = $sort = array();          $iterator = new DirectoryIterator($dir);          foreach($iterator as $file) {              if(!$file->isFile()) continue;              $filePathname = str_replace("\\", '/', $file->getRealPath());              //这些函数可以查看PHP手册              $imageInfo = getimagesize($filePathname);              $imageWidth = $imageInfo[0];              $imageHeight = $imageInfo[1];              $imageType = $imageInfo[2];                if(!in_array($imageType, array(IMAGETYPE_JPEG, IMAGETYPE_PNG))) {                  $fileShortName = substr($filePathname, strlen(ROOT) - 1);                  echo "<p> $fileShortName 图片被忽略: 因为图片类型不是png|jpg.</p>";                  continue;              }                //这是我们的图片规格,行高分别有 16 32 64 128 256 99999               foreach(array(16, 32, 64, 128, 256, 99999) as $height) {                  if($imageHeight <= $height) {                      $mapKey = $height;                      break;                  }              }              if(!isset($map[$mapKey])) $map[$mapKey] = array();              $filePathInfo = pathinfo($filePathname);              $map[$mapKey] []= array($imageWidth, $imageHeight, $imageType, $filePathInfo['filename'], $filePathname, md5_file($filePathname));              $sort[$mapKey] []= str_pad($imageHeight, 4, '0', STR_PAD_LEFT) . $filePathInfo['filename'];          }          foreach($map as $k => $v) array_multisort($map[$k], SORT_ASC, SORT_NUMERIC, $sort[$k]);          ksort($map, SORT_NUMERIC);          return $map;      }        /**       * 判断目录是否可写       * @param  string $dir 目录路径       */      function ensure_writable_dir($dir) {          if(!file_exists($dir)) {              mkdir($dir, 0766, true);              @chmod($dir, 0766);              @chmod($dir, 0777);          }          else if(!is_writable($dir)) {              @chmod($dir, 0766);              @chmod($dir, 0777);              if(!@is_writable($dir)) {                  throw new BusinessLogicException("目录不可写", $dir);              }          }      }        generateIcon();  ?>  <!DOCTYPE html>  <html>  <head>      <link rel="stylesheet" type="text/css" href="css/Pink.css">      <title></title>    </head>  <body>  <div>我们直接引入所生成的css文件,并测试一下是否成功</div>  <br>  <div>这里在span标签 添加属性 icon-Pink ,值为About-40,正常显示图片</div>  <span icon-Pink="About-40"></span>  </body>  </html>

<?php    //自己定义一个根目录    define('ROOT',$_SERVER['DOCUMENT_ROOT'].'iconwww');    //这个是图片的目录    define('RES_BASE_URL','http://localhost:8080/iconwww/img');    /**     * 生成背景图的函数     */    functiongenerateIcon(){      //网站根目录      $webRoot=rtrim(ROOT,'/');      //背景图目录      $root="$webRoot/img/bg";      //Php-SPL库中 的 目录文件遍历器      $iterator=newDirectoryIterator($root);      //开始遍历该背景图目录下的目录,我们是把想生成背景图的目录,放在bg目录中以各个模块的目录分类存放      foreach($iteratoras$file){        //遇到目录遍历        if(!$file->isDot()&&$file->isDir()){          //取得文件名          $fileName=$file->getFilename();          generateIconCallback("$root/$fileName","$webRoot/img/$fileName","$webRoot/css/$fileName.css");        }      }    }    /**     * 用户生成合并的背景图和css文件的函数     * @param  string $dir   生成背景图的图标所在的目录路径     * @param  string $bgSavePath  背景图所保存的路径     * @param  string $cssSavePath css保存的路径     */    functiongenerateIconCallback($dir,$bgSavePath,$cssSavePath){      $shortDir=str_replace('\\','/',substr($dir,strlen(ROOT-1)));      //返回文件路径信息      $pathInfo=pathinfo($bgSavePath.'.png');      $bgSaveDir=$pathInfo['dirname'];      //确保目录可写      ensure_writable_dir($bgSaveDir);      //背景图名字      $bgName=$pathInfo['filename'];      //调用generateIconCallback_GetFileMap()函数生成每一个图标所需要的数据结构      $fileMap=array('a'=>generateIconCallback_GetFileMap($dir));      $iterator=newDirectoryIterator($dir);      foreach($iteratoras$file){        if($file->isDot())continue;        if($file->isDir()){          //二级目录也要处理          $fileMap['b-'.$file->getFilename()]=generateIconCallback_GetFileMap($file->getRealPath());        }      }      ksort($fileMap);      //分析一边fileMap,计算整个背景图的大小和每一个图标的offset      //初始化偏移量和背景图       $offsetX=$offsetY=$bgWidth=0;      //设定每个小图标之间的距离      $spaceX=$spaceY=5;      //图片最大宽度      $maxWidth=800;      $fileMd5List=array();      //这里需要打印下$fileMap就知道它的数据结构了      foreach($fileMapas$k1=>$innerMap){        foreach($innerMapas$k2=>$itemList){          //行高姐X轴偏移量初始化          $offsetX=$lineHeight=0;          foreach($itemListas$k3=>$item){            //变量分别是:图标的宽度,高度,类型,文件名,路径,MD5加密字符串            list($imageWidth,$imageHeight,$imageType,$fileName,$filePathname,$fileMd5)=$item;            $fileMd5List[]=$fileMd5;            //如果图片的宽度+偏移量 > 最大宽度(800) 那就换行            if($offsetX!==0&&$imageWidth+$offsetX>$maxWidth){              $offsetY+=$spaceY+$lineHeight;              $offsetX=$lineHeight=0;            }            //如果图片高度 > 当前行高  那就讲图片高度付给行高我们这的            if($imageHeight>$lineHeight)$lineHeight=$imageHeight;            $fileMap[$k1][$k2][$k3]=array($imageWidth,$imageHeight,$offsetX,$offsetY,$imageType,$fileName,$filePathname);            //X轴偏移量的计算            $offsetX+=$imageWidth+$spaceX;            if($offsetX>$bgWidth)$bgWidth=$offsetX;          }          //Y轴偏移量的计算          $offsetY+=  $lineHeight+$spaceY;        }      }      //把右下两边多加了的空白距离给干掉      $bgWidth-=$spaceX;      $bgHeight=$offsetY-$spaceY;      $fileMd5List=implode("\n",$fileMd5List);      //生成背景图和 css文件      //资源路径      $resBaseUrl=RES_BASE_URL;      $suffix=base_convert(abs(crc32($fileMd5List)),10,36);      $writeHandle=fopen($cssSavePath,'w');      fwrite($writeHandle,"/** bg in dir: $shortDir/ */\n[icon-$bgName]{background:url({$resBaseUrl}/$bgName.png?$suffix) no-repeat;display:inline-block;}");      //做图片,这些函数具体可以查看PHP手册      $destResource=imagecreatetruecolor($bgWidth,$bgHeight);      imagealphablending($destResource,false);      imagesavealpha($destResource,false);      $color=imagecolorallocatealpha($destResource,255,255,255,127);      imagefill($destResource,0,0,$color);      //对每一张小图片进行处理,生成在大背景图里,并生成css文件      foreach($fileMapas$innerMap){        foreach($innerMapas$itemList){          foreach($itemListas$item){             list($imageWidth,$imageHeight,$offsetX,$offsetY,$imageType,$fileName,$filePathname)=$item;             if($imageType===IMAGETYPE_PNG){              $srcResource=imagecreatefrompng($filePathname);             }elseif($imageType===IMAGETYPE_JPEG){              $srcResource=imagecreatefromjpeg($filePathname);             }             imagecopy($destResource,$srcResource,$offsetX,$offsetY,0,0,$imageWidth,$imageHeight);             imagedestroy($srcResource);             //写入css             $posX=$offsetX===0?0:"-{$offsetX}px";             $posY=$offsetY===0?0:"-{$offsetY}px";             fwrite($writeHandle,"\n[icon-$bgName=\"$fileName\"]{width:{$imageWidth}px;height:{$imageHeight}px;background-position:$posX $posY;}");           }        }      }      //压缩级别 7      imagepng($destResource,"$bgSavePath.png",7);      imagedestroy($destResource);      fclose($writeHandle);      $shortCssSavePath=substr($cssSavePath,strlen(ROOT));    }    /**     * 将图片的信息处理成我们想要的数据结构     * @param  [type] $dir [description]     * @return [type]   [description]     */    functiongenerateIconCallback_GetFileMap($dir){      $map=$sort=array();      $iterator=newDirectoryIterator($dir);      foreach($iteratoras$file){        if(!$file->isFile())continue;        $filePathname=str_replace("\\",'/',$file->getRealPath());        //这些函数可以查看PHP手册        $imageInfo=getimagesize($filePathname);        $imageWidth=$imageInfo[0];        $imageHeight=$imageInfo[1];        $imageType=$imageInfo[2];        if(!in_array($imageType,array(IMAGETYPE_JPEG,IMAGETYPE_PNG))){          $fileShortName=substr($filePathname,strlen(ROOT)-1);          echo"<p> $fileShortName 图片被忽略: 因为图片类型不是png|jpg.</p>";          continue;        }        //这是我们的图片规格,行高分别有 16 32 64 128 256 99999        foreach(array(16,32,64,128,256,99999)as$height){          if($imageHeight<=$height){            $mapKey=$height;            break;          }        }        if(!isset($map[$mapKey]))$map[$mapKey]=array();        $filePathInfo=pathinfo($filePathname);        $map[$mapKey][]=array($imageWidth,$imageHeight,$imageType,$filePathInfo['filename'],$filePathname,md5_file($filePathname));        $sort[$mapKey][]=str_pad($imageHeight,4,'0',STR_PAD_LEFT).$filePathInfo['filename'];      }      foreach($mapas$k=>$v)array_multisort($map[$k],SORT_ASC,SORT_NUMERIC,$sort[$k]);      ksort($map,SORT_NUMERIC);      return$map;    }    /**     * 判断目录是否可写     * @param  string $dir 目录路径     */    functionensure_writable_dir($dir){      if(!file_exists($dir)){        mkdir($dir,0766,true);        @chmod($dir,0766);        @chmod($dir,0777);      }      elseif(!is_writable($dir)){        @chmod($dir,0766);        @chmod($dir,0777);        if(!@is_writable($dir)){          thrownewBusinessLogicException("目录不可写",$dir);        }      }    }    generateIcon();  ?>  <!DOCTYPE html>  <html>  <head>    <linkrel="stylesheet"type="text/css"href="css/Pink.css">    <title></title>  </head>  <body>  <div>我们直接引入所生成的css文件,并测试一下是否成功</div>  <br>  <div>这里在span标签 添加属性 icon-Pink ,值为About-40,正常显示图片</div>  <span icon-Pink="About-40"></span>  </body>  </html>

调用以上代码,我们的浏览器是这样显示的:

减少HTTP请求之合并图片详解(大型网站优化技术)

然后css目录生成了Pink.css文件:

减少HTTP请求之合并图片详解(大型网站优化技术)

img目录下生成了Pink.png文件:

减少HTTP请求之合并图片详解(大型网站优化技术)

看看生成的背景图是长啥样子:

减少HTTP请求之合并图片详解(大型网站优化技术)

接下来我们再看一下所生成的图片大小与Pink文件夹里所有小图片总和的大小,对它们做个比较:

减少HTTP请求之合并图片详解(大型网站优化技术)

减少HTTP请求之合并图片详解(大型网站优化技术)

从上图可以看出,我们生成的图片的大小明显小于文件夹所有图片的大小,所以在将100个小图标下载下来的速度 会明显小于 将背景图下载下来和将CSS下载下来的速度。

当访问量大时,或者小图片的量大时,会起到很明显的优化效果!!!

代码中的每一个点都基本上有注释,很方便大家去理解,只要大家用心去看,肯定能将这一网站优化技术用到自己的项目中。

本次博文就写到这!!!

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

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

互相学习,共同进步!