张大帅 2016-08-09 11:25:15 6107次浏览 7条评论 10 15 0

Hello,大家好,大帅又来了,各位,来了请点赞。
上一篇文章梳理了整个框架的大致流程,介绍了页面通过Module入口找到了controller,另外有通过controller的render()函数找到了相应的layout和view页面。但是如何找到的没有介绍,只是简单的描述了一下,本篇文章将详细介绍一下页面渲染的过程。想必各位都用Yii框架的时候,都会在controller里面写类似于render的函数,render的意思是渲染,那么渲染的类型有很多,看了源码之后,\base\Controller.php里面有

public function render($view, $params = []);
public function renderContent($content);
public function renderPartial($view, $params = []);
public function renderFile($file, $params = []);

这四种类型,各位在用的时候可以各取所需。我首先以render为入口来进行分析。

public function render($view, $params = [])
{
    $content = $this->getView()->render($view, $params, $this);
    return $this->renderContent($content);
}

在这个函数中,首先看参数,render函数接收两个参数,第一个参数描述的是要加载view文件,第二个参数是传给view文件的一些参数。接着往下看,找到getView()函数。

public function getView()
{
    if ($this->_view === null) {
        $this->_view = Yii::$app->getView();
    }
    return $this->_view;
}

也是通过Yii::$app->getView();函数获得view对象(如果不知道怎么获得,可以参考前两篇文章里的getRequest, getResponse的解释),然后通过view对象的render函数加载进去,注意getView()->render($view, $params, $this);中那个$this参数,这个$this传递过去的类型其实是controller对象,这个之后在view对象中会遇到,请先记住。另外$view参数指的是你自己代码中要加载的view文件。然后找到view类,指向的是base\View.php,

public function render($view, $params = [], $context = null)
{
    $viewFile = $this->findViewFile($view, $context);
    return $this->renderFile($viewFile, $params, $context);
}

先看参数,没错这里的context就是指向的是controller,这下你就可以联想到如何在view文件或者layout文件中获得controller里面某个变量的值,是通过$this->context->XXX获得的,原理就在这里。看代码,第一步,是findViewFile($view, $context);看名字就知道是要找到view文件,那么我们找到这个函数,

protected function findViewFile($view, $context = null)
{
    if (strncmp($view, '@', 1) === 0) {
        // e.g. "@app/views/main"
        $file = Yii::getAlias($view);
    } elseif (strncmp($view, '//', 2) === 0) {
        // e.g. "//layouts/main"
        $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
    } elseif (strncmp($view, '/', 1) === 0) {
        // e.g. "/site/index"
        if (Yii::$app->controller !== null) {
            $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
        } else {
            throw new InvalidCallException("Unable to locate view file for view '$view': no active controller.");
        }
    } elseif ($context instanceof ViewContextInterface) {
        $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view;
    } elseif (($currentViewFile = $this->getViewFile()) !== false) {
        $file = dirname($currentViewFile) . DIRECTORY_SEPARATOR . $view;
    } else {
        throw new InvalidCallException("Unable to resolve view file for view '$view': no active view context.");
    }

    if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
        return $file;
    }
    $path = $file . '.' . $this->defaultExtension;
    if ($this->defaultExtension !== 'php' && !is_file($path)) {
        $path = $file . '.php';
    }

    return $path;
}

这几个if语句中,都有实例注释,可以脑补一下自己写的代码中render函数里第一个参数是什么样子的,然后可以通过这个代码查看,这里默认分析$view = 'index'的流程,直接进入到
elseif ($context instanceof ViewContextInterface) 这里,$file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; 代码中这个$context在前文中说了指的是controller,那么回到controller里找到getViewPath函数,

public function getViewPath()
{
    if ($this->_viewPath === null) {
        $this->_viewPath = $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id;
    }
    return $this->_viewPath;
}

可以看出在controller里面是通过module函数进行getViewPath()查找的,而这里的module指向的是app,而app又继承于Module.php,通过查看Module.php里面的getViewPath()函数可以看到,

public function getViewPath()
{
    if ($this->_viewPath === null) {
        $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views';
    }
    return $this->_viewPath;
}

可以看到,就是指的是项目工程里的views文件夹。回到base\View.php继续往下走,最后renturn的就是这个文件,并默认以.php为后缀。返回到render函数继续往下走,return $this->renderFile($viewFile, $params, $context); 找到renderFile()函数,

public function renderFile($viewFile, $params = [], $context = null)
{
    $viewFile = Yii::getAlias($viewFile);

    if ($this->theme !== null) {
        $viewFile = $this->theme->applyTo($viewFile);
    }
    if (is_file($viewFile)) {
        $viewFile = FileHelper::localize($viewFile);
    } else {
        throw new InvalidParamException("The view file does not exist: $viewFile");
    }

    $oldContext = $this->context;
    if ($context !== null) {
        $this->context = $context;
    }
    $output = '';
    $this->_viewFiles[] = $viewFile;

    if ($this->beforeRender($viewFile, $params)) {
        Yii::trace("Rendering view file: $viewFile", __METHOD__);
        $ext = pathinfo($viewFile, PATHINFO_EXTENSION);
        if (isset($this->renderers[$ext])) {
            if (is_array($this->renderers[$ext]) || is_string($this->renderers[$ext])) {
                $this->renderers[$ext] = Yii::createObject($this->renderers[$ext]);
            }
            /* @var $renderer ViewRenderer */
            $renderer = $this->renderers[$ext];
            $output = $renderer->render($this, $viewFile, $params);
        } else {
            $output = $this->renderPhpFile($viewFile, $params);
        }
        $this->afterRender($viewFile, $params, $output);
    }

    array_pop($this->_viewFiles);
    $this->context = $oldContext;

    return $output;
}

在这段代码里,前面无非就是进行一些beforeRender处理,没什么好分析的,直接看$output = $this->renderPhpFile($viewFile, $params);找到renderPhpFile函数,

public function renderPhpFile($_file_, $_params_ = [])
{
    ob_start();
    ob_implicit_flush(false);
    extract($_params_, EXTR_OVERWRITE);

    require($_file_);

    return ob_get_clean();
}

这里面关系到一系列的缓冲区函数,我也不太明白,大概意思就是控制输出的。然后将view文件包含进来了。然后一路返回到base/controller.php的render函数,这样的话,

public function render($view, $params = [])
{
    $content = $this->getView()->render($view, $params, $this);
    return $this->renderContent($content);
}

$content返回的就是view文件内容,总的来看view.php文件里的render函数和renderFile函数的区别就在于,view里面的render函数是先找到view文件的位置,在调用renderfile函数进行加载,等于说比renderFile多了一层查找文件路径的过程。
接下来就要return了,但是要进行renderContent($content)函数,注意参数是将view文件的内容作为了参数传递进行了。
找到renderContent函数。

public function renderContent($content)
{
    $layoutFile = $this->findLayoutFile($this->getView());
    if ($layoutFile !== false) {
        return $this->getView()->renderFile($layoutFile, ['content' => $content], $this);
    } else {
        return $content;
    }
}

第一句应该会猜到,就是找到layout文件的路径。
找到findLayoutFile函数,注意参数传递。

public function findLayoutFile($view)
{
    $module = $this->module;

    if (is_string($this->layout)) {
        $layout = $this->layout;
    } elseif ($this->layout === null) {
        while ($module !== null && $module->layout === null) {
            $module = $module->module;
        }
        if ($module !== null && is_string($module->layout)) {
            $layout = $module->layout;
        }
    }

    if (!isset($layout)) {
        return false;
    }

    if (strncmp($layout, '@', 1) === 0) {
        $file = Yii::getAlias($layout);
    } elseif (strncmp($layout, '/', 1) === 0) {
        $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . substr($layout, 1);
    } else {
        $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $layout;
    }

    if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
        return $file;
    }
    $path = $file . '.' . $view->defaultExtension;
    if ($view->defaultExtension !== 'php' && !is_file($path)) {
        $path = $file . '.php';
    }

    return $path;
}

这个文件的内容跟getViewPath函数的内容有点类似,显示通过配置找到用户是否定义了layout文件,如果没有一切按照默认的来进行,就不做详细分析了。之后回到renderContent函数继续往下走,紧接着还是getView()->renderFile(),也是按照直接view文件查找的模式走了一遍,其实就可以把layout文件当成view文件的逻辑来进行。
通过这些就可以发现,框架是先加载view文件,再去加载layout文件的。
另外由此也可以解释,为什么render可以任意嵌套的,那是因为都只是单独通过$this触发的,相互之间不影响。
由此render的内容全部执行完毕,之后返回给controller作为一个output,接下来的事情就是上一篇文章中的内容了,将该篇内容脑补进第二篇,就可以完整的走一遍Yii框架的流程了。

补充:
controller文件在文章的开头提到了,有很几种render类型,比如renderPartical, renderFile, renderAjax, render
结合刚才的分析和代码的阅读,都是调用了一些函数进行了封装。
比如,renderPartical函数,就没有触发renderContent函数,代码中renderContent其实就是加载layout的内容的,那么可以推断,renderPartical就是加载的view文件,而不加载layout文件。同理,renderFile就是用户自定义文件路径,直接进行加载的。这里需要详细说明的是controller.php里面的renderAjax函数,这个函数renderAjax是以ajax方式渲染页面,可以配合js/css实现各种特效,如动态加载。

通过这三篇文章,各位可以了解到框架的运行机制,了解到框架是如何进行类的自动加载的,如何进行全局的配置的,如何构建单例模式的,如何解析路由的,如何查找文件的,如何进行加载的等等。整个过程还是很清晰的。以后的文章,将对框架的单个模块进行单独分析,不会有这种流程性的分析了。敬请期待...

觉得很赞
您需要登录后才可以评论。登录 | 立即注册