Phalcon View 渲染原理及过程

西山雨

本文主要讨论 Phalcon 视图层的渲染。

原理

  • 把 action 中传过来的数据定义成变量。

  • 加载视图文件,此时视图里的变量已经有值了 (如果是volt的话,把volt语法编译成原生php,加载编译后的文件),它是一个 php 和 html 混写的文件,php 的代码会执行,最后只剩下 html。

  • 把执行完后的内容返回出去。

控制器传值给视图

使用方法:

// 直接设置变量,会调用 __set() 的魔术方法
$this->view->products = $products;

// setParamToView()
$this->view->setParamToView('products', $products);

// setVar()
$this->view->setVar('products', $products);

// setVars()
$this->view->setVars(array('products' => $products));

下面我们分别看一下源码中这些方法是如何实现的

/**
 * Magic method to pass variables to the views
 *
 * @param string key
 * @param mixed value
 */
public function __set(string! key, value)
{
    let this->_viewParams[key] = value;
}
/**
 * Adds parameters to views (alias of setVar)
 *
 * @param string key
 * @param mixed value
 * @return \Phalcon\Mvc\View
 */
public function setParamToView(string! key, value) -> <View>
{
    let this->_viewParams[key] = value;
    return this;
}
/**
 * Set all the render params
 *
 * @param array params
 * @param boolean merge
 * @return \Phalcon\Mvc\View
 */
public function setVars(array! params, boolean merge = true) -> <View>
{
    var viewParams;

    if merge {
        let viewParams = this->_viewParams;
        if typeof viewParams == "array" {
            let this->_viewParams = array_merge(viewParams, params);
        } else {
            let this->_viewParams = params;
        }
    } else {
        let this->_viewParams = params;
    }
    
    return this;
}
/**
 * Set a single view param
 *
 * @param string key
 * @param mixed value
 * @return \Palcon\Mvc\View
 */
public function setVar(string! key, value) -> <View>
{
    let this->_viewParams[key] = value;
    return this;
}

这几个方法都是把值设置给了 view 实例的一个属性 _viewParams ,在调用 _engineRender() 方法时,它取出 _viewParams 的值作为参数传给了具体的渲染引擎。

视图

在 index.php 中,实例化出一个 application 对象,执行 application->handle() 方法,在这个方法里,实例化了 view,调 view->render() 方法,我们追踪下去,就可以找出 view 层是如何处理渲染的。

视图渲染逻辑

/**
 * Executes render process from dispatching data
 *
 * @param string controllerName
 * @param string actionName
 * @param array params
 */
public function render(string! controllerName, string! actionName, params = null) -> <View>|boolean
{

}

下面我以主要的业务逻辑过一下源码

  • _disabled 检查视图是否禁用,如果禁用,输出内容, return false

  • layout 检查是否设置了layout

  • _loadTemplateEngines() 加载模板引擎

  • pickView 检查用户是否指定了一个不同于当前url对应的 controller/action 视图

  • _cacheLevel 检查缓存

  • create_symbol_table() 创建符号表

  • view:beforeRender 抛出事件

  • Inserts view related to action

  • Inserts templates before layout

  • Inserts controller layout

  • Inserts templates after layout

  • Inserts main view

  • Store the data in the cache

  • view:afterRender 抛出事件

关于多次插入视图,他们之间的关系可以参考我的另一篇博文:http://segmentfault.com/a/1190000004363452

在插入视图时,都是调用的这个方法this->_engineRender(engines, renderView, silence, mustClean, cache);

_engineRender

在 Phalcon 框架中,所有的模板引擎适配器都必须继承Engine类,这个类提供了引擎和视图组件的基本接口,每一个引擎都必须实现自己的render方法。

当插入视图调用 _engineRender() 方法时,Phalcon 又做了如下处理

注:本文仅是示例代码

/**
 * Checks whether view exists on registered extensions and render it
 *
 * @param array engines
 * @param string viewPath
 * @param boolean silence
 * @param boolean mustClean
 * @param \Phalcon\Cache\BackendInterface $cache
 */
protected function _engineRender(engines, string viewPath, boolean silence, boolean mustClean, <BackendInterface> cache = null)
{
    let viewsDir = this->_viewDir,
        basePath = this->_basePath,
        viewsDirPath = basePath . viewsDir . viewPath;

    let viewParams = this->_viewParams;
    
    for extension, engine in engines {
        let viewEnginePath = viewsDirPath . extension;
        if file_exists(viewEnginePath) {
            
            if typeof eventsManager == "object" {
                let this->_activeRenderPath = viewEnginePath;
                if (eventsManager->fire("view:beforeRenderView", this, viewEnginePath) === false) {
                    continue;
                }
            }
        
            engine->render(viewEnginePath, viewParams, mustClean);
        
            let notExists = false;
            if typeof eventsManager == "object" {
                eventsManager->fire("view:afterRenderView", this);
            }
            break;
        }
    }
}

它在调用具体模板引擎渲染的前后分别抛出两个事件,便于开发者使用。

此时的 engine 可能是 php engine,也可以是 volt engine,或者其他任何你注册的渲染引擎。
下面是这些参数的含义。

  • viewEnginePath: 视图文件的路径。

  • viewParams: action 传给 view 的数据。

  • mustClean: 是否必须清除输出缓冲区。

这里有几个要点需要注意:

  • 如果侦听 view:beforeRenderView 事件,处理后返回 false,将不进入渲染阶段。

  • engine->render() 方法调用后,任何输出都将忽略,原因参考下面模板引擎渲染的代码。

  • view:afterRenderView 事件不可中止。

php engine

以 php 默认渲染引擎为例,当上面调用engine->render()方法时,会执行以下几步

  1. 清空输出缓冲区。

  2. 把 action 传给 view 的数据,以变量的形式声明出来。

  3. 加载视图文件,这时候在视图中php代码会把变量的值输出。

  4. 使用 setContent() 方法把输出缓冲区的内容放入 view 对象 _content 属性中。这样在 视图文件里就可以使用 getContent() 方法取出。

    /**
     * Externally sets the view content
     *
     *<code>
     *    $this->view->setContent("<h1>hello</h1>");
     *</code>
     */
    public function setContent(string content) -> <View>
    {
        let this->_content = content;
        return this;
    }
    
    /**
     * Returns cached output from another view stage
     */
    public function getContent() -> string
    {
        return this->_content;
    }

php engine render 源码分析

/**
 * Adapter to use PHP itself as templating engine
 */
class Php Engine implements EngineInterface
{
    /**
     * Renders a view using the template engine
     */
    public function render(string! path, var params, boolean mustClean = false)
    {
        var key, value;
        
        if mustClean === true {
            ob_clean();
        }

        /**
         * Create the variables in local symbol table
         */
        for key, value in params {
            let {key} = value;
        }

        /**
         * Require the file
         */
        require path;

        if mustClean == true {
            $this->_view->setContent(ob_get_contents());
        }
    }
}

参数解析:

  • path:视图文件 .phtml.volt 文件的路径

  • mustClean:是否必须清除输出缓冲区

  • params: array 类型,存储的是 action 传给 view 的变量。

如:在 action

$this->view->test = 123;
$this->view->haha = "hello world";

params

array (size=2)
 'test' => int 123
 'haha' => string 'hello world'

volt

volt 是 Phalcon 自带的模板渲染引擎。如果我们注册的是 volt 引擎,那么_engineRender调的是 volt 引擎,我们看一下它是如何处理的。

render

/**
 * Renders a view using the template engine
 */
public function render(string! templatePath, var params, boolean mustClean = false)
{
    var compiler, compiledTemplatePath, key, value;

    if mustClean {
        ob_clean();
    }

    /**
     * The compilation process is done by Phalcon\Mvc\View\Engine\Volt\Compiler
     */
    let compiler = this->getCompiler();

    compiler->compile(templatePath);

    let compiledTemplatePath = compiler->getCompiledTemplatePath();

    /**
     * Export the variables the current symbol table
     */
    if typeof params == "array"    {
        for key, value in params {
            let {key} = value;
        }
    }

    require compiledTemplatePath;

    if mustClean {
        this->_view->setContent(ob_get_contents());
    }
}

volt 和 php engine 处理差不多,只是中间多了一步编译,而编译所做的工作就是把 volt 语法转换成原生 php 语法。

我们可以看出,它首先得到一个 compiler 的实例,然后调用compiler的compile()方法来编译 volt 文件,最后是得到编译后文件的路径。

下面我们通过源代码验证我们的设想


首先看 getCompiler()

/**
 * Returns the Volt's compiler
 */
public function getCompiler() -> <Compiler>
{
    var compiler, dependencyInjector, options;

    let compiler = this->_compiler;
    if typeof compiler != "object" {

        let compiler = new Compiler(this->_view);

        /**
         * Pass the IoC to the compiler only of it's an object
         */
        let dependencyInjector = <DiInterface> this->_dependencyInjector;
        if typeof dependencyInjector == "object" {
            compiler->setDi(dependencyInjector);
        }

        /**
         * Pass the options to the compiler only if they're an array
         */
        let options = this->_options;
        if typeof options == "array" {
            compiler->setOptions(options);
        }

        let this->_compiler = compiler;
    }
    return compiler;
}

然后看compile(),这个方法内容很多,我们忽略大部分细节,只看关键部分。

/**
 * Compiles a template into a file applying the compiler options
 * This method does not return the compiled path if the template was not compiled
 *
 *<code>
 *    $compiler->compile('views/layouts/main.volt');
 *    require $compiler->getCompiledTemplatePath();
 *</code>
 */
public function compile(string! templatePath, boolean extendsMode = false)
{
    let compilation = this->compileFile(templatePath, realCompiledPath, extendsMode);

    let this->_compiledTemplatePath = realCompiledPath;

    return compilation;
}

最后我们看getCompiledTemplatePath()

/**
 * Returns the path to the last compiled template
 */
public function getCompiledTemplatePath() -> string
{
    return this->_compiledTemplatePath;
}

compileFile

compile() ,我们就可以看出 compileFile() 方法是下一个突破口.

/**
 * Compiles a template into a file forcing the destination path
 *
 *<code>
 *    $compiler->compile('views/layouts/main.volt', 'views/layouts/main.volt.php');
 *</code>
 *
 * @param string path
 * @param string compiledPath
 * @param boolean extendsMode
 * @return string|array
 */
public function compileFile(string! path, string! compiledPath, boolean extendsMode = false)
{
    var viewCode, compilation, finalCompilation;

    /**
     * Always use file_get_contents instead of read the file directly, this respect the open_basedir directive
     */
    let viewCode = file_get_contents(path);

    let this->_currentPath = path;
    let compilation = this->_compileSource(viewCode, extendsMode);

    /**
     * We store the file serialized if it's an array of blocks
     */
    if typeof compilation == "array" {
        let finalCompilation = serialize(compilation);
    } else {
        let finalCompilation = compilation;
    }

    /**
     * Always use file_put_contents to write files instead of write the file directly, this respect the open_basedir directive
     */
    if file_put_contents(compiledPath, finalCompilation) === false {
        throw new Exception("Volt directory can't be written");
    }

    return compilation;
}

从这里我们又找到线索_compileSource(),继续前行。

_compileSource

/**
 * Compiles a Volt source code returning a PHP plain version
 */
protected function _compileSource(string! viewCode, boolean extendsMode = false) -> string
{
    let currentPath = this->_currentPath;

    // Check for compilation options
    // ......

    let intermediate = phvolt_parse_view(viewCode, currentPath);

    let compilation = this->_statementList(intermediate, extendsMode);
    
    // Check if the template is extending another
    // ......
    
    return compilation;
}

看到方法注释的时候,我们以为终于找到正主,看完却发现并没有。

_statementList

/**
 * Traverses a statement list compiling each of its nodes
 */
final protected function _statementList(array! statements, boolean extendsMode = false) -> string
{
    // ......
}

这个方法有246行,我把关键部分截图了,方便查看。

图片描述

它通过 switch case,匹配不同表达式的类型,调用不同的方法具体编译某段代码。

我以 compileEcho() 和 compileIf() 为例给大家看一下.

图片描述

图片描述

进行到这里,我们终于找出了 volt 模板引擎的大致渲染思路,它比php engine 多了一步就是编译,把它自身语法翻译成原生php,然后就和 php engine 处理是一样的了。

volt 总结

volt 比 phtml 在处理渲染时多了一步编译,虽然它已编译成机器码在服务器端运行,但多了一步编译,肯定比 phtml 慢一些的;而且前期使用 volt 必然需要阅读官方文档,了解其规则,这些都是时间成本。

但是熟练使用它的话,在开发过程中能够简化代码,提高可读性和开发效率。

总结

我们在处理视图渲染时,先把控制器传过来的数据,通过变量声明出来,这样视图文件的变量就有值了,随着 php 代码的执行,最后只剩下一个 html 的内容返回出去。

阅读 8k

fighting

1.3k 声望
26 粉丝
0 条评论
你知道吗?

fighting

1.3k 声望
26 粉丝
文章目录
宣传栏