您好,欢迎访问一九零五行业门户网

Yii源码阅读笔记

概述 通常我们会使用模板引擎来渲染html页面,而不是使用html代码中插入php代码的方式来编写动态页面。yii框架中模板引擎也是作为组件引入的,默认id为viewrenderer,但从yii源码阅读笔记 - 组件集成可以看到yii web应用加载的核心组件中并没有viewrenderer
概述通常我们会使用模板引擎来渲染html页面,而不是使用html代码中插入php代码的方式来编写动态页面。yii框架中模板引擎也是作为组件引入的,默认id为viewrenderer,但从yii源码阅读笔记 - 组件集成可以看到yii web应用加载的核心组件中并没有viewrenderer,所以需要自己配置。yii提供了一个直接可用的模板引擎组件类cpradoviewrenderer(见文件yii/framework/web/renderers/cpradoviewrenderer.php),该模板引擎类让开发者可以使用类prado框架的模板语法。
如果你想使用smarty这种第三方模板引擎,有两种方式将模板引擎引入yii中使用(以smarty为例):
将smarty封装成一个yii的普通组件,然后配置加载到yii::app()。假设组件id为smarty,那么就可以通过yii::app()->smarty来调用组件。参考cpradoviewrenderer类的实现,将smarty封装成一个模板引擎组件,并以id为viewrenderer进行配置加载。相比而言,第二种方式更好。原因是:第一种方式由于每种第三方模板引擎的接口不一样,如果应用要替换模板引擎,就需要修改控制器类中的代码。而第二种方式由于第三方组件统一封装成yii框架定义的模板引擎接口形式,所以如果要替换模板引擎,只需修改自定义模板引擎组件类的接口实现就可以了。这样调用模板引擎的代码逻辑就只依赖接口形式,而不是依赖于接口实现,从而实现解耦。
本文主要分析第二种方式的实现。
分析yii中对页面模板进行渲染可以调用ccontroller类(见文件yii/framework/web/ccontroller.php)的方法render,实现如下:
/** * renders a view with a layout. * * this method first calls {@link renderpartial} to render the view (called content view). * it then renders the layout view which may embed the content view at appropriate place. * in the layout view, the content view rendering result can be accessed via variable * $content. at the end, it calls {@link processoutput} to insert scripts * and dynamic contents if they are available. * * by default, the layout view script is protected/views/layouts/main.php. * this may be customized by changing {@link layout}. * * @param string $view name of the view to be rendered. see {@link getviewfile} for details * about how the view script is resolved. * @param array $data data to be extracted into php variables and made available to the view script * @param boolean $return whether the rendering result should be returned instead of being displayed to end users. * @return string the rendering result. null if the rendering result is not required. * @see renderpartial * @see getlayoutfile */public function render($view,$data=null,$return=false){ // beforerender默认返回true, // 可以在自定义controller类中重写该方法,实现渲染之前的预处理 // 但和beforeaction一样,应该要返回true或false if($this->beforerender($view)) { // 渲染真正的内容部分 $output=$this->renderpartial($view,$data,true); // 获取布局文件 if(($layoutfile=$this->getlayoutfile($this->layout))!==false) // 渲染整个页面 $output=$this->renderfile($layoutfile,array('content'=>$output),true); // 渲染的后处理,默认为空,在processoutput之前调用 $this->afterrender($view,$output); // 对渲染结果进行处理 $output=$this->processoutput($output); // 可以将渲染结果作为方法的返回值返回,或者直接输出到用户浏览器 if($return) return $output; else echo $output; }}
其中方法renderpartial的实现如下所示:
/** * renders a view. * * the named view refers to a php script (resolved via {@link getviewfile}) * that is included by this method. if $data is an associative array, * it will be extracted as php variables and made available to the script. * * this method differs from {@link render()} in that it does not * apply a layout to the rendered result. it is thus mostly used * in rendering a partial view, or an ajax response. * * @param string $view name of the view to be rendered. see {@link getviewfile} for details * about how the view script is resolved. * @param array $data data to be extracted into php variables and made available to the view script * @param boolean $return whether the rendering result should be returned instead of being displayed to end users * @param boolean $processoutput whether the rendering result should be postprocessed using {@link processoutput}. * @return string the rendering result. null if the rendering result is not required. * @throws cexception if the view does not exist * @see getviewfile * @see processoutput * @see render */public function renderpartial($view,$data=null,$return=false,$processoutput=false){ // 获取目标模板文件 $viewfile=$this->getviewfile($view); echo (basename(__file__).':'.__line__.':'.__function__.'() $viewfile '. var_export($viewfile, true)); if(($viewfile)!==false) { // 渲染 $output=$this->renderfile($viewfile,$data,true); // 如果$processoutput为真,则也会对结果进行后处理 if($processoutput) $output=$this->processoutput($output); if($return) return $output; else echo $output; } else throw new cexception(yii::t('yii','{controller} cannot find the requested view {view}.', array('{controller}'=>get_class($this), '{view}'=>$view)));}
renderpartial方法并不会渲染出一个完整的页面,只是渲染页面的一部分,通常是主体部分,或者为ajax请求渲染出响应结果。其中调用的getviewfile方法实现如下:
public function getviewfile($viewname){ // 如果未配置theme项,即表示不使用theme,那么gettheme方法返回null if(($theme=yii::app()->gettheme())!==null && ($viewfile=$theme->getviewfile($this,$viewname))!==false) return $viewfile; // viewpath默认为views,可配置 $moduleviewpath=$basepath=yii::app()->getviewpath(); echo (basename(__file__).':'.__line__.':'.__function__.'() $moduleviewpath '. var_export($moduleviewpath, true)),\n; // 模块化,如果没有,则getmodule返回null if(($module=$this->getmodule())!==null) $moduleviewpath=$module->getviewpath(); // $this->getviewpath()得到的路径相比$moduleviewpath就是多了controller的id一级 return $this->resolveviewfile($viewname,$this->getviewpath(),$basepath,$moduleviewpath);}
代码中$this->getviewpath()方法的实现如下:
public function getviewpath(){ if(($module=$this->getmodule())===null) $module=yii::app(); // $this->getid()是得到当前controller的id,这个id是在controller实例化时构造方法中赋值给属性_id的。 // 这也就意味着页面模板文件需要按照controller的id分目录存放 return $module->getviewpath().directory_separator.$this->getid();}
getviewfile中最后调用的方法resolveviewfile实现如下所示:
public function resolveviewfile($viewname,$viewpath,$basepath,$moduleviewpath=null){ // 连模板文件名都不给,还玩个屁啊 if(empty($viewname)) return false; // 若$moduleviewpath未设置,则在应用的页面模板的根目录下找 if($moduleviewpath===null) $moduleviewpath=$basepath; // 获取设置的模板渲染引擎,其实就是加载id为viewrenderer的组件 if(($renderer=yii::app()->getviewrenderer())!==null) // 模板文件的扩展类型默认为'.php',可配置 $extension=$renderer->fileextension; else $extension='.php'; echo (basename(__file__).':'.__line__.':'.__function__.'() $extension '. var_export($extension, true)),\n; // 如果指定的模板文件名以/开始 if($viewname[0]==='/') { // 如果指定的模板文件名以//开始,则表示在模板的根目录下查找 if(strncmp($viewname,'//',2)===0) $viewfile=$basepath.$viewname; // 否则(以单个/开始)在模块的模板目录下查找 else $viewfile=$moduleviewpath.$viewname; } // 如果模板文件名中存在.且.不出现在第一个位置,则认为这是一个路径别名,需要转换真正的路径 elseif(strpos($viewname,'.')) $viewfile=yii::getpathofalias($viewname); else // 否则在当前controller的模板目录下找 $viewfile=$viewpath.directory_separator.$viewname; // 可能站点是需要国际化的 // 所以在找到默认的模板文件后,尝试找一下对应用户目标语言的模板文件 if(is_file($viewfile.$extension)) return yii::app()->findlocalizedfile($viewfile.$extension); // 如果不存在指定扩展类型的模板文件,且扩展类型不为'.php',则看一下'.php'类型的模板文件是否存在 elseif($extension!=='.php' && is_file($viewfile.'.php')) return yii::app()->findlocalizedfile($viewfile.'.php'); else return false;}
方法resolveviewfile中最后调用的方法findlocalizedfile,定义于抽象类capplication中,实现如下:
public function findlocalizedfile($srcfile,$srclanguage=null,$language=null){ if($srclanguage===null) // sourcelanguage为public的属性,可配置,默认为en_us $srclanguage=$this->sourcelanguage; if($language===null) // getlanguage的实现:$this->_language===null ? $this->sourcelanguage : $this->_language // 默认_language为未赋值,即null,所以取到的还是sourcelanguage属性值。 // 但因为__set,所以也是可赋值的,这个赋值不应该是配置造成的,应该是根据用户的cookie中指定的语言选项,在请求处理时设置的,表示用户的目标语言 $language=$this->getlanguage(); // 如果用户的目标语言(或者用户选择的是默认语言),则直接返回默认模板文件的路径 if($language===$srclanguage) return $srcfile; // 否则取到对应目标语言的模板文件 $desiredfile=dirname($srcfile).directory_separator.$language.directory_separator.basename($srcfile); // 如果对应目标语言的模板文件不存在,则还是返回默认的模板文件 return is_file($desiredfile) ? $desiredfile : $srcfile;}
从上述模板文件的寻找过程可以看到,最后返回的目标模板文件的路径是一个相对路径,以动态脚本根目录(默认为protected)开始。
方法renderpartial中在得到目标模板文件相对路径后,即调用renderfile方法(定义于cbasecontroller类中)来渲染模板,该方法的实现如下:
public function renderfile($viewfile,$data=null,$return=false){ $widgetcount=count($this->_widgetstack); // yii::app()->getviewrenderer() 获取模板引擎组件 if(($renderer=yii::app()->getviewrenderer())!==null && $renderer->fileextension==='.'.cfilehelper::getextension($viewfile)) $content=$renderer->renderfile($this,$viewfile,$data,$return); else // 如果没法用模板引擎来渲染(可能不是模板引擎的目标模板,也可能是没设置模板引擎组件),则当前普通的php文件(html代码中夹杂着php代码)来渲染 $content=$this->renderinternal($viewfile,$data,$return); if(count($this->_widgetstack)===$widgetcount) return $content; else { $widget=end($this->_widgetstack); throw new cexception(yii::t('yii','{controller} contains improperly nested widget tags in its view {view}. a {widget} widget does not have an endwidget() call.', array('{controller}'=>get_class($this), '{view}'=>$viewfile, '{widget}'=>get_class($widget)))); }}
renderfile中$content=$renderer->renderfile($this,$viewfile,$data,$return);一行调用的renderfile方法,在抽象类cviewrenderer中定义如下:
public function renderfile($context,$sourcefile,$data,$return){ if(!is_file($sourcefile) || ($file=realpath($sourcefile))===false) throw new cexception(yii::t('yii','view file {file} does not exist.',array('{file}'=>$sourcefile))); // 尝试从runtime目录中获取已编译好的模板,如果编译好的模板不是同一存放在runtime目录下,则默认和未编译的模板文件在同一个目录下,并且文件名多一个c后缀 // 得到$viewfile可能并不存在,第一次请求该模板 $viewfile=$this->getviewfile($sourcefile); // 如果相比已编译好的模板文件,未编译的模板已发生变更,则需要重新编译 // 如果已编译好的模板文件不存在,则@filemtime($viewfile)返回的是false,这个条件也是返回true if(@filemtime($sourcefile)>@filemtime($viewfile)) { // 抽象类cviewrenderer中generateviewfile方法并未实现,所以自己封装模板引擎组件时需要实现该方法 $this->generateviewfile($sourcefile,$viewfile); // 设置编译好的模板文件的访问权限,默认是0755 (owner rwx, group rx and others rx) @chmod($viewfile,$this->filepermission); } // 编译好的模板文件其实就是一个php脚本(html代码中夹杂php代码),所以还需要渲染一下 return $context->renderinternal($viewfile,$data,$return);}
类ccontroller的render方法在调用renderpartial得到渲染结果后,取得页面布局模板文件,然后将renderpartial的渲染结果作为数据渲染布局模板,从而得到一个完整html页面。获取布局模板文件路径的方法getlayoutfile实现如下所示(定义于类ccontroller中):
public function getlayoutfile($layoutname){ if($layoutname===false) return false; if(($theme=yii::app()->gettheme())!==null && ($layoutfile=$theme->getlayoutfile($this,$layoutname))!==false) return $layoutfile; if(empty($layoutname)) { $module=$this->getmodule(); // 递归向父级模板查找布局文件 while($module!==null) { if($module->layout===false) return false; if(!empty($module->layout)) break; $module=$module->getparentmodule(); } // 如果当前controller不属于某个module if($module===null) $module=yii::app(); // 默认为main,可配置 $layoutname=$module->layout; } elseif(($module=$this->getmodule())===null) $module=yii::app(); return $this->resolveviewfile($layoutname,$module->getlayoutpath(),yii::app()->getviewpath(),$module->getviewpath());}
其逻辑与方法getviewfile类似。
由上述分析可知,将第三方模板引擎封装成yii框架的模板引擎组件,可以继承自抽象类cviewrenderer,并实现其方法generateviewfile,然后配置该组件的id为viewrenderer。对于模板文件的存放,需要考虑web应用是否分模块、应用是否国际化、模板文件相关controller的id等,模板文件名的扩展类型应与模板引擎组件配置的一样。
原文地址:yii源码阅读笔记 - 模板引擎集成, 感谢原作者分享。
其它类似信息

推荐信息