输出类是个核心类,它的功能只有一个:发送 Web 页面内容到请求的浏览器。如果你开启缓存,它也负责缓存你的 Web 页面。
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']; // :请求发起时间
在代码中想要实现这种连续调用类方法的方式,只需要在每个方法末尾加上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内部分析器,其结构和方法属性功能描述如下:
现在重点分析一下其主要成员方法:
1、构造函数(__construct())
① 设置压缩标记$_compress_output:
在构造函数中,CI通过ini_get('zlib.output_compression')获取当前php环境是否开启了GZIP压缩。如果PHP环境没有开启,那么判断配置文件中的压缩设置(compress_output=TRUE),是不是要求框架压缩输出,如果要求的话,只要当前PHP是加载了zlib扩展的,那么就把$_compress_output标记设为TRUE。通常情况下,我们在使用过程中会开启WEB服务器的压缩功能,而关闭程序本身压缩功能。
② 设置$mimes值:
加载配置application/config/mimes.php中的MIME信息。
2、output函数簇(get_output()、set_output($output)、append_output($output))
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);
3、header函数簇(get_header()、set_header($header, $replace = TRUE))
① 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; }
4、Content_type函数簇(set_content_type($mime_type, $charset = NULL)、get_content_type())
每次服务器响应的头信息中都会包括类似这样的信息:Content-Type:"text/html; charset=utf-8",源文件中有Meta信息,服务器在向客户端输出时,会告知客户端我将要给你什么类型的数据,客户端浏览器根据这个信息用对应的方式解析。比如现在服务器要将一组excel表格数据输出给客户端,你就可以用content="application/excel"来告知客户端,这是一个excel文件,你应该用对待excel的方式来对待。有的装了插件的浏览器可能在本身就打开显示了,有的就提示下载EXCEL类型的文件了。那么application/excel就被称作Mime信息。Mime信息与不同文件的对应关系在application/config/mimes.php中都有。
① 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, '.'); 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设置。
② get_content_type()
获取当前正在使用的 HTTP 头 Content-Type ,不包含字符集部分。$mime = $this->output->get_content_type();系统从一堆header信息中匹配Content-Type信息,找到了就返回其中的MIME值,没找到,就返回默认的text/html。
5、profiler函数簇(enable_profiler($val = TRUE)、set_profiler_sections($sections))
① enable_profiler($val = TRUE)
public function enable_profiler($val = TRUE);设置$enable_profiler值是否开启分析器。
② set_profiler_sections($sections)
public function set_profiler_sections($sections);//设置分析器的内容。允许你启用或禁用程序分析器 ,它可以在你的页面底部显示 基准测试的结果或其他一些数据帮助你调试和优化程序。$this->output->enable_profiler(TRUE);
6、写入缓存(_write_cache($output))
主要流程是根据访问的URI信息生成一个MD5作为本次访问缓存的KEY,再将内容写入文件。方法流程如下:
Ⅰ 实例$CI控制器对像$CI =& get_instance();
Ⅱ 获取缓存路径:通过配置文件获取缓存路径$CI->config->item('cache_path'),如果没有设置,那么就默认用application/cache路径,如果路径不存在或不可写,记录错误日志,并返回。
Ⅲ 生成缓存的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。Ⅳ 创建缓存文件句柄: 打开$cache_path文件获得句柄$fp,对文件进行一个排它锁定flock($fp, LOCK_EX);。
Ⅴ 压缩处理:如果没有在php.ini中开启zlib.output_compression,且配置文件中要求开启压缩,那么就在程序中使用gzencode来压缩(这段是在构造函数中做的),$output = gzencode($output),压缩完成后,设置content-type。
Ⅵ 过期时间$expire设置。
Ⅶ 生成最终输出$output:$output分为三段:a.expire,headers序列化成字符串;b.分隔符:ENDCI--->;c.之前预输出的$output内容。
Ⅷ 写入文件。
(注:代码就不贴了,放在最后一起贴。)
7、显示缓存(_display_cache(&$CFG, &$URI))
读cache文本,判断过期没有,调用_display输出。方法执行流程如下:
Ⅰ 从配置中获取缓存路径。
Ⅱ 根据uri获取缓存key,拼凑出缓存路径$filepath:$filepath = $cache_path.md5($uri);。
Ⅲ 读取文件到变量$cache。如果没有缓存,返回FALSE,外部CI工作流程会继续往下执行。
Ⅳ 获取$cache_info,也就是之前写缓存时在ENDCI--->前面加的expire,headers信息。
Ⅴ 判断缓存过期时间:
if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) { @unlink($filepath); log_message('debug', 'Cache file has expired. File deleted.'); return FALSE; } else { $this->set_cache_header($last_modified, $expire); }
如果过期,则删除原缓存文件,并返回FALSE; 外部CI工作流程会继续往下执行。如果没有过期,则调用set_cache_header($last_modified, $expiration)。如果设置了HTTP_IF_MODIFIED_SINCE头,且文件最后修改时间没有超过HTTP_IF_MODIFIED_SINCE时间,则直接发304状态码给客户端,让客户端调用本地缓存,关于HTTP_IF_MODIFIED_SINCE及304状态,可以参考《HTTP权威指南》一书。如果文件修改时间超过了HTTP_IF_MODIFIED_SINCE时间,就重新发送头信息,告诉客户端缓存该次请求的结果到本地。
Ⅵ 根据缓存中的expire设置头部信息。
Ⅶ 调用_display输出$cache中的内容部分:$this->_display(substr($cache, strlen($match[0])));。
8、输出(_display($output = ''))
发送最终输出结果以及服务器的 HTTP 头到浏览器,同时它也会停止基准测试的计时器。方法执行流程如下:
Ⅰ 加载和实例化Benchmark,Config类。使用load_class()而没有使用$CI =& get_instance()控制器的实例来加载类库,原因是因为该方法有时侯是被缓存机制调用,也就是上面说的,_display_cache()函数调用,这时当前请求的上下文根本没有加载控制器类,所以无法正确实例化控制器。
Ⅱ 实例化CI_Controller,如果是缓存,那就不会实像化了。这个很重要,接下来都是以isset($CI)来区分是否是缓存。缓存与非缓存处理方式截然不同。
if (class_exists('CI_Controller', FALSE)) { $CI =& get_instance(); }
Ⅲ 写缓存(响应cache(n)方法):判断$cache_expiration属性,这个值可由方法cache(n)设置;再判断控制器中有没有用到_output()扩展自己的输出,如果没有,就写缓存。
Ⅳ 解析替换伪变量0.2048, 2.81MB。
Ⅴ 判断是否执行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'); }
Ⅵ 输出头信息。
Ⅶ 如果! 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;
Ⅷ 生成分析数据 ($this->enable_profiler) 将html加到$output后面,profiler类库另开篇。
Ⅸ 调用控制器中的自定义方法_output对最终结果进行最后一道处理。当然也可不处理。
最后,贴一下整个输出类Output.php文件的源码(注释版):
_zlib_oc = (bool)ini_get('zlib.output_compression');
$this->_compress_output = ($this->_zlib_oc === FALSE && config_item('compress_output') === TRUE && extension_loaded('zlib'));
//引入mime类型配置文件
$this->mimes =& get_mimes();
log_message('info', 'Output Class Initialized');
}
/**
* 获取$this->final_output
* 允许你手工获取存储在输出类中的待发送的内容。
*/
public function get_output()
{
return $this->final_output;
}
/**
* 设置$this->final_output
* 允许你手工设置最终的输出字符串
*/
public function set_output($output)
{
$this->final_output = $output;
return $this;
}
/**
* 向输出字符串附加数据。
*/
public function append_output($output)
{
// 将数据追加到最终输出结果,此方法在Loader.php中被调用。
$this->final_output .= $output;
return $this;
}
/**
* 允许你手工设置服务器的 HTTP 头,输出类将在最终显示页面时发送它
*/
public function set_header($header, $replace = TRUE)
{
//如果php开启了zlib.output_compression压缩,就跳过content-length头的设置
//这样做的理由是当压缩开启后,实际输出字节数比正常少,误设content-length头后,
//会使得客户端一直等待服务器发送足够字节的文本,造成无法正常响应。
if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) {
return $this;
}
$this->headers[] = array($header, $replace);
return $this;
}
/**
* 设置Content Type
* 给Head添加Content_type信息。允许你设置你的页面的 MIME 类型,
* 可以很方便的提供 JSON 数据、JPEG、XML 等等格式。
*/
public function set_content_type($mime_type, $charset = NULL)
{
//$mime_type是要设置MIME信息的文件扩展名,
//系统从$mimes数组中找出对应扩展名中的MIME信息
if (strpos($mime_type, '/') === FALSE) {
$extension = ltrim($mime_type, '.');
if (isset($this->mimes[$extension])) {
$mime_type =& $this->mimes[$extension];
if (is_array($mime_type)) {
$mime_type = current($mime_type);
}
}
}
$this->mime_type = $mime_type;
if (empty($charset)) {
$charset = config_item('charset');
}
$header = 'Content-Type: ' . $mime_type . (empty($charset) ? '' : '; charset=' . $charset);
$this->headers[] = array($header, TRUE);
return $this;
}
/**
* 获取当前正在使用的 HTTP 头 Content-Type ,不包含字符集部分。
*/
public function get_content_type()
{
//系统从一堆header信息中匹配Content-Type信息,找到了就返回其中的MIME值,
//没找到,就返回默认的text/html
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';
}
/**
* 返回请求的 HTTP 头,如果 HTTP 头还没设置,返回 NULL 。
*/
public function get_header($header)
{
$headers = array_merge(
array_map('array_shift', $this->headers),
headers_list()
);
if (empty($headers) OR empty($header)) {
return NULL;
}
for ($i = 0, $c = count($headers); $i < $c; $i++) {
if (strncasecmp($header, $headers[$i], $l = strlen($header)) === 0) {
return trim(substr($headers[$i], $l + 1));
}
}
return NULL;
}
/**
* Set HTTP Status Header
*/
public function set_status_header($code = 200, $text = '')
{
set_status_header($code, $text);
return $this;
}
/**
* 设置$enable_profiler值是否开启分析器
*/
public function enable_profiler($val = TRUE)
{
$this->enable_profiler = is_bool($val) ? $val : TRUE;
return $this;
}
/**
* 设置分析器的内容
*/
public function set_profiler_sections($sections)
{
//允许你启用或禁用程序分析器 ,
//它可以在你的页面底部显示
//基准测试的结果或其他一些数据帮助你调试和优化程序。
if (isset($sections['query_toggle_count'])) {
$this->_profiler_sections['query_toggle_count'] = (int)$sections['query_toggle_count'];
unset($sections['query_toggle_count']);
}
foreach ($sections as $section => $enable) {
$this->_profiler_sections[$section] = ($enable !== FALSE);
}
return $this;
}
/**
* 设置缓存时长,开启文件缓存
*/
public function cache($time)
{
$this->cache_expiration = is_numeric($time) ? $time : 0;
return $this;
}
/**
* 将最终结果输出到浏览器
*/
public function _display($output = '')
{
$BM =& load_class('Benchmark', 'core');
$CFG =& load_class('Config', 'core');
//当然如果可以拿到超级控制器,我们先拿过来。
if (class_exists('CI_Controller', FALSE)) {
$CI =& get_instance();
}
//如果$output为空,其实往往这是非缓存方式调用的时候。我们将使用Output::final_output。
//(如果是正常流程的输出方式,而不是缓存的话,
//这个属性其实在Loader::view()的时候调用Output::append_output()获得输出内容。)
if ($output === '') {
$output =& $this->final_output;
}
//Output::$cache_expiration其实就是缓存时长,就是平时我们在控制器里面$this->output->cache(n)设置的时长
//现实手段就是使这个Output::$cache_expiration有一定的值,然后程序执行到这里时根据此值判断是否要缓存,
//如果要缓存就生成缓存文件。(注意如果是_display_cache间接调用的话,$this->cache_expiraton是一定为0的,因为
//没有经历过在控制器中调用$this->output->cache(n)。)
if ($this->cache_expiration > 0 && isset($CI) && !method_exists($CI, '_output')) {
//上面有个判断$CI是否有_output方法,其实是提供一个机会让我们自定义处理输出。
//生成缓存文件。
$this->_write_cache($output);
}
$elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
if ($this->parse_exec_vars === TRUE) {
//系统的总体运行时间和内存消耗就是在这里替换的。
//上面的Output::$parse_exec_vars就是设置要不要替换。
$memory = round(memory_get_usage() / 1024 / 1024, 2) . 'MB';
$output = str_replace(array('0.2048', '2.81MB'), array($elapsed, $memory), $output);
}
//压缩传输的处理。
if (isset($CI) && $this->_compress_output === TRUE
&& isset($_SERVER['HTTP_ACCEPT_ENCODING'])
&& strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE
) {
ob_start('ob_gzhandler');
}
if (count($this->headers) > 0) {
foreach ($this->headers as $header) {
@header($header[0], $header[1]);
}
}
//如果没有超级控制器,可以证明当前是在处理一个缓存的输出。不过利用这个方式来判断,真的有点那个。。。
if (!isset($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 {
$output = gzinflate(substr($output, 10, -8));
}
}
//输出缓存内容。结束本函数。
echo $output;
log_message('info', 'Final output sent to browser');
log_message('debug', 'Total execution time: ' . $elapsed);
return;
}
//这里是一个评测器,如果有开启就调用,会生成一些报告到页面尾部用于辅助我们调试。
//我用CI的时候其实没有开启过,厄。
if ($this->enable_profiler === TRUE) {
$CI->load->library('profiler');
if (!empty($this->_profiler_sections)) {
$CI->profiler->set_sections($this->_profiler_sections);
}
$output = preg_replace('|
.*?