CodeIgniter框架源码笔记(4)——负责屏幕上呈现的内容:输出类Output.php

来源:互联网 发布:北京网络职业学院 地图 编辑:程序博客网 时间:2024/05/17 02:49
Output类参考说明(摘抄CI手册):
在一般情况下,你可能根本就不会注意到输出类,因为它无需你的干涉, 对你来说完全是透明的。例如,当你使用 加载器 加载一个视图文件时,它会自动传入到输出类,并在系统执行的最后由 CodeIgniter 自动调用。尽管如此,在你需要时,你还是可以对输出进行手工处理。

在说Output类前先说几个知识点和编程技巧:
$_SERVER['HTTP_ACCEPT_ENCODING'] :对应请求头是Accept-Encoding:"gzip, deflate"
$_SERVER['HTTP_IF_MODIFIED_SINCE'] :对应请求头是If-Modified-Since:"Sat, 16 Feb 2013 08:10:03 GMT"
$_SERVER['REQUEST_TIME'] :请求发起时间
知识点:
1、缓存:《浅谈代码级web缓存几个层次的实现 》
2、压缩 :《 实现WEB压缩的三种途径:Web服务器(Nginx/Apache)、Php扩展、Php代码 》
技巧:
在代码中想要实现这种连续调用类方法的方式,只需要在每个方法末尾加上return $this;
$output    ->set_content_type('application/json')    ->set_output(json_encode(array('foo' => 'bar')));public function set_content_type($mime_type, $charset = NULL){    ......    return $this;}   

CI要想启用缓存功能,在控制器(controller)的方法(function)内加入一句话:$this->output->cache(n),其中n是缓存更新的分钟数。
Output类主要功能负责向浏览器输出最终结果,其中包括从缓存加载内容输出,根据控制器方法产生的内容输出,还包括写缓存、设置头信息、加载CI内部分析器,其结构和方法属性功能描述如下:


现在重点分析一下其主要成员方法:

一、构造函数__construct():始始化$_compress_output和$mimes值

1、设置压缩标记$_compress_output:
在构造函数中,CI通过ini_get('zlib.output_compression')获取当前php环境是否开启了GZIP压缩。如果PHP环境没有开启,那么判断配置文件中的压缩设置(compress_output=TRUE),是不是要求框架压缩输出,如果要求的话,只要当前PHP是加载了zlib扩展的,那么就把$_compress_output标记设为TRUE。
通常情况下,我们在使用过程中会开启WEB服务器的压缩功能,而关闭程序本身压缩功能。
2、设置$mimes值:加载配置application/config/mimes.php中的MIME信息。


二、output函数簇,用于设置或获取成员变量$final_output的值

get_output()
获取$this->final_output
允许你手工获取存储在输出类中的待发送的内容。使用示例:
$string = $this->output->get_output();
注意,只有通过 CodeIgniter 输出类的某个方法设置过的数据,例如 $this->load->view() 方法,才可以使用该方法获取到。

set_output($output)
设置$this->final_output
允许你手工设置最终的输出字符串。使用示例:
$this->output->set_output($data);

append_output($output)
向输出字符串附加数据。
$this->output->append_output($data);

三、header函数簇

get_header()
返回请求的 HTTP 头,如果 HTTP 头还没设置,返回 NULL 。 例如:
$this->output->set_content_type('text/plain', 'UTF-8');echo $this->output->get_header('content-type');// Outputs: text/plain; charset=utf-8

set_header($header, $replace = TRUE)
允许你手工设置服务器的 HTTP 头,输出类将在最终显示页面时发送它。例如:
$this->output->set_header('HTTP/1.1 200 OK');$this->output->set_header('Last-Modified: '.gmdate('D, d M Y H:i:s', $last_update).' GMT');$this->output->set_header('Cache-Control: no-store, no-cache, must-revalidate');$this->output->set_header('Cache-Control: post-check=0, pre-check=0');$this->output->set_header('Pragma: no-cache');
如果php开启了zlib.output_compression压缩,就跳过content-length头的设置
这样做的理由是当压缩开启后,实际输出字节数比正常少,误设content-length头后,会使得客户端一直等待服务器发送足够字节的文本,造成无法正常响应。
public function set_header($header, $replace = TRUE){    if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)    {        return $this;    }    $this->headers[] = array($header, $replace);    return $this;}

四、Content_type函数簇:

每次服务器响应的头信息中都会包括类似这样的信息:Content-Type:"text/html; charset=utf-8"
源文件中有Meta信息<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
服务器在向客户端输出时,会告知客户端我将要给你什么类型的数据,客户端浏览器根据这个信息用对应的方式解析。
比如现在服务器要将一组excel表格数据输出给客户端,你就可以用content="application/excel"来告知客户端,这是一个excel文件,你应该用对待excel的方式来对待。有的装了插件的浏览器可能在本身就打开显示了,有的就提示下载EXCEL类型的文件了。那么application/excel就被称作Mime信息。
Mime信息与不同文件的对应关系在application/config/mimes.php中都有。

1、set_content_type($mime_type, $charset = NULL)
给Head添加Content_type信息。允许你设置你的页面的 MIME 类型,可以很方便的提供 JSON 数据、JPEG、XML 等等格式。
$this->output
    ->set_content_type('application/json')
    ->set_output(json_encode(array('foo' => 'bar')));

$mime_type是要设置MIME信息的文件扩展名,系统从$mimes数组中找出对应扩展名中的MIME信息
if (strpos($mime_type, '/') === FALSE){    $extension = ltrim($mime_type, '.');    // Is this extension supported?    if (isset($this->mimes[$extension]))    {        $mime_type =& $this->mimes[$extension];        if (is_array($mime_type))        {            $mime_type = current($mime_type);        }    }}
这里程序用了if (strpos($mime_type, '/') === FALSE)判断,表示如果参数是扩展名(pptx,jpeg)的话,就去$mimes数组进行匹配处理。
如果参数中包括了“/”,系统认为方法参数type值就是MIME信息,比如application/octet-stream,接下来就直接$this->mime_type = $mime_type;
接下来设置charset信息,如果参数没有设置,就读配置文件的charset设置。

2、get_content_type()
获取当前正在使用的 HTTP 头 Content-Type ,不包含字符集部分。
$mime = $this->output->get_content_type();
系统从一堆header信息中匹配Content-Type信息,找到了就返回其中的MIME值,没找到,就返回默认的text/html
public function get_content_type(){    for ($i = 0, $c = count($this->headers); $i < $c; $i++)    {        if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)        {            return $content_type;        }    }    return 'text/html';}

五、profiler函数簇

public function enable_profiler($val = TRUE);//设置$enable_profiler值是否开启分析器
public function set_profiler_sections($sections);//设置分析器的内容
允许你启用或禁用程序分析器 ,它可以在你的页面底部显示 基准测试的结果或其他一些数据帮助你调试和优化程序。
$this->output->enable_profiler(TRUE);

六、写入缓存:_write_cache($output)

主要流程是根据访问的URI信息生成一个MD5作为本次访问缓存的KEY,再将内容写入文件。
1、实例$CI控制器对像$CI =& get_instance();
2、获取缓存路径:

    通过配置文件获取缓存路径$CI->config->item('cache_path'),如果没有设置,那么就默认用application/cache路径
    如果路径不存在或不可写,记录错误日志,并返回。
3、生成缓存的key:
    获得$cachpath:根据uri生成唯一身份字符串,可认为是缓存的key。
    a、获取当前地址$url。这里特别用到了URI类,另外开篇详说。
           b.1 如果配置文件中设置了cache_query_string值(就是设置querystring中允许被缓存的变量),就会取_GET数组与cache_query_string中设置数组的交集。
       举个粟子:
       $config['cache_query_string'] = array('cid','page');
       那么如果当前url是:http://mysite/balabala?cid=1&page=2&sort=viewnumber&sorttype=desc
       那么最终uri.=http://mysite/balabala?cid=1&page=2
           b.2 如果没有设置cache_query_string值,那么uri.=$_SERVER['QUERY_STRING'];会把整个地址加载进来。

   c、将uri用md5()函数生成唯一身份字符串,可认为是缓存的key,得到缓存文件最终路径$cache_path。

4、创建缓存文件句柄:
    打开$cache_path文件获得句柄$fp
   
------>>>>>>对文件进行一个排它锁定flock($fp, LOCK_EX);

5、压缩处理:
    如果没有在php.ini中开启zlib.output_compression,且配置文件中要求开启压缩,那么就在程序中使用gzencode来压缩(这段是在构造函数中做的)
    $output = gzencode($output)
    压缩完成后,设置content-type

6、过期时间$expire设置

7、生成最终输出$output。

   $output分为三段:
   1、expire,headers序列化成字符串
   2、分隔符:ENDCI--->
   3、之前预输出的$output内容。

8、写入文件。
在写入网络文件流时,会出现没有把字符完全写入文件就停止工作的情况,在大文件时特别常见。这时需要用到fwrite返回值:写入的字符数来检测到底写入了多少字符,如果字符数不足,可重复执行fwrite。

文档上原话是:Writing to a network stream may end before the whole string is written. Return value of fwrite() may be checked:

for ($written = 0, $length = strlen($output); $written < $length; $written += $result){if (($result = fwrite($fp, substr($output, $written))) === FALSE){break;}}

------>>>>>>解锁flock($fp, LOCK_EX);


七、显示缓存:public function _display_cache(&$CFG, &$URI)

读cache文本,判断过期没有,调用_display输出
1、从配置中获取缓存路径

2、根据uri获取缓存key,拼凑出缓存路径$filepath

$filepath = $cache_path.md5($uri);
3、读取文件到变量$cache。如果没有缓存,返回FALSE,外部CI工作流程会继续往下执行
if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
{
    return FALSE;
}
4、获取$cache_info,也就是之前写缓存时在ENDCI--->前面加的expire,headers信息
6、判断缓存过期时间
if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)){    // If so we'll delete it.    @unlink($filepath);    log_message('debug', 'Cache file has expired. File deleted.');    return FALSE;}else{    // Or else send the HTTP cache control headers.    $this->set_cache_header($last_modified, $expire);}
   a.如果过期,则删除原缓存文件,并返回FALSE; 外部CI工作流程会继续往下执行。
   b.如果没有过期,则调用set_cache_header($last_modified, $expiration)
     b.1如果设置了HTTP_IF_MODIFIED_SINCE头,且文件最后修改时间没有超过HTTP_IF_MODIFIED_SINCE时间,则直接发304状态码给客户端,让客户端调用本地缓存,关于HTTP_IF_MODIFIED_SINCE及304状态,可以参考《HTTP权威指南》一书。
     b.2如果文件修改时间超过了HTTP_IF_MODIFIED_SINCE时间,就重新发送头信息,告诉客户端缓存该次请求的结果到本地。
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])){    $this->set_status_header(304);    exit;}else{    header('Pragma: public');    header('Cache-Control: max-age='.$max_age.', public');    header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');    header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');}

7、根据缓存中的expire设置头部信息
foreach ($cache_info['headers'] as $header){    $this->set_header($header[0], $header[1]);}
8、调用_display输出$cache中的内容部分
$this->_display(substr($cache, strlen($match[0])));

八、_display($output = '')

发送最终输出结果以及服务器的 HTTP 头到浏览器,同时它也会停止基准测试的计时器。
1、加载和实例化Benchmark,Config类
$BM =& load_class('Benchmark', 'core');
$CFG =& load_class('Config', 'core')
使用load_class()而没有使用$CI =& get_instance()控制器的实例来加载类库,原因是因为该方法有时侯是被缓存机制调用,也就是上面说的
_display_cache()函数调用,这时当前请求的上下文根本没有加载控制器类,所以无法正确实例化控制器

2、实例化CI_Controller,如果是缓存,那就不会实像化了。这个很重要,接下来都是以isset($CI)来区分是否是缓存。缓存与非缓存处理方式截然不同
if (class_exists('CI_Controller', FALSE)){    $CI =& get_instance();}

3、写缓存(响应cache(n)方法):判断$cache_expiration属性,这个值可由方法cache(n)设置;再判断控制器中有没有用到_output()扩展自己的输出,如果没有,就写缓存
if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')){    $this->_write_cache($output);}

4、解析替换伪变量{elapsed_time}, {memory_usage}
$memory    = round(memory_get_usage() / 1024 / 1024, 2).'MB';
$output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);

5、判断是否执行PHP代码端的压缩,如果是缓存就跳过(因为已经压缩过了),如果是正常控制器且_compress_output为真,同时客户端浏览器告诉服务器支持的压缩格式为gzip,就执行ob_start('ob_gzhandler');
isset($CI)表明的这一段不会对缓存生效。
if (isset($CI)  && $this->_compress_output === TRUE && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE){    ob_start('ob_gzhandler');}


6、输出头信息

if (count($this->headers) > 0){    foreach ($this->headers as $header)    {        @header($header[0], $header[1]);    }}

7、如果! isset($CI),代表是缓存。那么根据客户端$_SERVER['HTTP_ACCEPT_ENCODING'信息,判断是否输出压缩内容,还是输出原始已解压内容。
   如果是缓存,进行输出后,整个CI流程就完成了。

if ($this->_compress_output === TRUE){    //如果客户端支持压缩    if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)    {        header('Content-Encoding: gzip');        header('Content-Length: '.strlen($output));    }    else    {        //如果不支持压缩,就要把缓存文件解压输出。substr($output, 10, -8)很多人不理解为什么要这样处理        // PHP 5.4 之后新增的 gzip 解压函数 gzdecode .目前很多的空间服务商的 PHP 版本都没有达到 5.4,这也导致使用此函数之后发生函数未定义错误,PHP 官方网站用户提交的日志中有人给出了很好的解决方案,使用 gzinflate 函数代替,但对数据要进行处理。        $output = gzinflate(substr($output, 10, -8));    }}echo $output;
8、生成分析数据 ($this->enable_profiler) 将html加到$output后面,profiler类库另开篇
9、调用控制器中的自定义方法_output对最终结果进行最后一道处理。当然也可不处理。



0 0
原创粉丝点击