CodeIgniter源码阅读笔记(9)——输出组件Output.php

Output组件负责向浏览器发送最终输出,是ci框架中使用非常广泛的一个组件,在之前的几篇源码阅读中,也使用到了这个组件,例如:load->view()
在阅读输出组件源码的时候,我们主要关注以下几个问题:

  1. Output是如何输出页面的,在最终输出的之前做了哪些处理?
  2. 页面缓存是怎么实现的?

带着以上几个问题我们开始来阅读它的源码吧。

1.构造函数

    public function __construct()
    {
        //判断是否可压缩输出内容,zlib是压缩内容的扩展组件
        $this->_zlib_oc = (bool) ini_get('zlib.output_compression');
        $this->_compress_output = (
            $this->_zlib_oc === FALSE
            && config_item('compress_output') === TRUE
            && extension_loaded('zlib')
        );
        //mbstring是php的标准扩展库,查看标准扩展库的函数是否可以重载
        isset(self::$func_overload) OR self::$func_overload = 
                                    (extension_loaded('mbstring') &&
                                         ini_get('mbstring.func_overload'));
        //获取mimes配置文件中的mimes列表
        $this->mimes =& get_mimes();

        log_message('info', 'Output Class Initialized');
    }

这个构造函数中一共做了三件事:

  1. 判断是否可以压缩输出内容,这是为了在之后的输出缓存中决定是否压缩;
  2. mbstring是否可以重载,因为ci框架中对mbstring标准库的一些函数进行了重载,而输出组件中正好要使用重载的函数,详细了解可以查看’./system/core/compat/mbstring.php’文件;
  3. ci框架中定义了一个mimes列表,将mimes列表取出,为了之后设置请求头时使用;

    从构造函数中我们可以看出,output在输出内容之前,会对要输出的内容进行压缩。

2.输出函数——_display()

    public function _display($output = '')
    {
        //加载Benchmark组件和Config组件
        $BM =& load_class('Benchmark', 'core');
        $CFG =& load_class('Config', 'core');
        //获取控制器,如果是缓存则不会实例化控制器
        if (class_exists('CI_Controller', FALSE))
        {
            $CI =& get_instance();
        }
        //如果output为空,则将final_output赋值给output
        if ($output === '')
        {
            $output =& $this->final_output;
        }
        //当控制器没有自己的输出方法,且没有处理过缓存文件时,写缓存
        if ($this->cache_expiration > 0 && isset($CI) 
                                        && ! method_exists($CI, '_output'))
        {
            $this->_write_cache($output);
        }
        //mark开始时间
        $elapsed = $BM->elapsed_time('total_execution_time_start',
                                                 'total_execution_time_end');
        //$this->parse_exec_vars为true,将输出内容的
        //'{elapsed_time}'和'{memory_usage}'替换成实际的时间和内存使用情况
        if ($this->parse_exec_vars === TRUE)
        {
            $memory    = round(memory_get_usage() / 1024 / 1024, 2).'MB';
            $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
        }
        //控制器已加载,会压缩输出内容且请求头中Accept-Encoding中有gzip
        //将输出内容压缩再放入缓冲区
        if (isset($CI)
            && $this->_compress_output === TRUE
            && isset($_SERVER['HTTP_ACCEPT_ENCODING']) 
            && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
        {
            ob_start('ob_gzhandler');
        }
        //发送http请求头
        if (count($this->headers) > 0)
        {
            foreach ($this->headers as $header)
            {
                @header($header[0], $header[1]);
            }
        }
        //如果没有控制器初始化,则说明当前是一个缓冲的输出,发送数据并退出
        if ( ! isset($CI))
        {
            //如果会压缩输出内容
            if ($this->_compress_output === TRUE)
            {
                //请求头中Accept-Encoding中有gzip,发送http请求头
                if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
                {
                    header('Content-Encoding: gzip');
                    header('Content-Length: '.self::strlen($output));
                }
                else
                {
                    //解压压缩数据
                    $output = gzinflate(self::substr($output, 10, -8));
                }
            }
            //输出内容
            echo $output;
            log_message('info', 'Final output sent to browser');
            log_message('debug', 'Total execution time: '.$elapsed);
            return;
        }
        //如果开启了数据分析,会生成一些性能报告,帮助我们调试
        if ($this->enable_profiler === TRUE)
        {
            $CI->load->library('profiler');
            if ( ! empty($this->_profiler_sections))
            {
                $CI->profiler->set_sections($this->_profiler_sections);
            }

            $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
            if ($count > 0)
            {
                $output .= '</body></html>';
            }
        }
        //控制器中有输出函数,则调用控制器中的函数,否则直接输出
        if (method_exists($CI, '_output'))
        {
            $CI->_output($output);
        }
        else
        {
            echo $output;
        }

        log_message('info', 'Final output sent to browser');
        log_message('debug', 'Total execution time: '.$elapsed);
    }

这里主要注意的是压缩问题。

  1. $_SERVER[‘HTTP_ACCEPT_ENCODING’]是判断客户端是否支持压缩
  2. 如果支持压缩且需要执行php压缩代码,如果是缓存,发送相应请求头,不是缓存,执行php的压缩代码
  3. 如果不支持压缩,如果是缓存,则在执行php的解压缩代码再输出(因为缓存的内容会进行压缩),不是缓存,不执行压缩代码

3.写缓存_write_cache

缓存的目录可以是自定义目录,默认是’application/cache’目录

$path = $CI->config->item('cache_path');
        $cache_path = ($path === '') ? APPPATH.'cache/' : $path;

缓存的key是根据uri信息生成的md5字符串

        $uri = $CI->config->item('base_url')
            .$CI->config->item('index_page')
            .$CI->uri->uri_string();
        if (($cache_query_string = $CI->config->item('cache_query_string'))
                                     && ! empty($_SERVER['QUERY_STRING']))
        {
            if (is_array($cache_query_string))
            {
                $uri .= '?'.http_build_query(array_intersect_key
                                ($_GET, array_flip($cache_query_string)));
            }
            else
            {
                $uri .= '?'.$_SERVER['QUERY_STRING'];
            }
        }
        $cache_path .= md5($uri);

$this->_compress_output为true对输出内容执行php压缩代码

        if ($this->_compress_output === TRUE)
        {
            $output = gzencode($output);

            if ($this->get_header('content-type') === NULL)
            {
                $this->set_content_type($this->mime_type);
            }
        }

缓存有效时间以秒为单位,缓存有效时间和请求头信息,以序列化后的数组存入缓存中,并以’ENDCI—>’字符串为分割符合输出内容分开

        $expire = time() + ($this->cache_expiration * 60);

        $cache_info = serialize(array(
            'expire'    => $expire,
            'headers'   => $this->headers
        ));

        $output = $cache_info.'ENDCI--->'.$output;

最后将缓存内容写入缓存文件

4.输出缓存内容——_display_cache函数

_display_cache会通过Key找到对应的缓存文件,然后取出缓存文件中的内容,查看缓存信息,反序列化缓存信息,得到缓存有效时间,判断缓存是否有效,无效则删除缓存,返回FALSE
*$_SERVER['REQUEST_TIME']参数是请求时间

        if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
        {
            return FALSE;
        }

        $cache_info = unserialize($match[1]);
        $expire = $cache_info['expire'];

        $last_modified = filemtime($filepath);

        if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
        {
            @unlink($filepath);
            log_message('debug', 'Cache file has expired. File deleted.');
            return FALSE;
        }

设置缓存请求头

    $this->set_cache_header($last_modified, $expire);

    foreach ($cache_info['headers'] as $header)
    {
        $this->set_header($header[0], $header[1]);
    }

输出缓存
在阅读CodeIngiter.php的时候,我们知道,如果对应请求有缓存的话,则不会初始化控制器,所以_display就是通过是否加载了控制器来判断输出的内容是不是缓存的

$this->_display(self::substr($cache, self::strlen($match[0])));

5.设置缓存请求头——set_cache_header函数

    public function set_cache_header($last_modified, $expiration)
    {
        $max_age = $expiration - $_SERVER['REQUEST_TIME'];

        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && 
            $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
        {
            $this->set_status_header(304);
            exit;
        }

        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');
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值