之前的文章我們根據源碼的分析,弄清了Yii如何處理一次請求,以及根據解析的路由如何調用控制器中的action,那接下來好奇的可能就是,我在控制器action中執行了return $this->render('index')
,那render這個方法是如何完成渲染視圖文件的工作的?我們繼續從源碼入手。
1、找到視圖文件
先看我們在controller/action中視圖渲染的調用:
public function actionIndex()
{
//代碼省略
return $this->render('index',[
'model'=>$model
]);
}
1.1、找到render方法
因為所有的控制器類都繼承了yii\web\Controller
,最終找到render位於yii\web\Controller
的父類yii\base\Controller
,看定義:
//yii\base\Controller
public function render($view, $params = [])
{
$content = $this->getView()->render($view, $params, $this);
return $this->renderContent($content);
}
可以看到真正的渲染操作並非在controller中處理,而是在view對象中完成的。這里只負責調用:
- 把視圖文件給我渲染好
- 把渲染好的視圖文件放進我定義的布局文件
- 返回
1.2、獲取視圖對象
從render方法的設計可以看到Yii2.0中,控制器(C)和視圖(V)是完全分離的,各司其職。看$this->getView()
:
//yii\base\Controller
public function getView()
{
if ($this->_view === null) {
$this->_view = Yii::$app->getView();
}
return $this->_view;
}
Yii::$app
,就是我們new Application
的時候保存的一個全局application變量,在構造方法中已經保存好了Yii::$app = $this;
,可以查看yii\base\Application __construct
,不過多描述了。
這樣我們知道了Yii::$app->getView()
,getView應該位於yii\web\Application
,或者其父類中`:
//yii\base\Application
public function getView()
{
return $this->get('view');
}
get方法位於Application的父類yii\di\ServiceLocator
,根據源碼可以看到,是對當前對象中的兩個成員變量$_components
與$_definitions
做了判斷,看是否有view這個組件的對象(實例),或者定義(['class'=>'yii\web\view']
),有實例則直接返回,否則根據定義創建實例(createObject)返回。
【注】關於$_definitions變量的初始化,其實在new Application的時候,在
yii\base\Application
的__construct
構造方法中,調用了preInit,而preInit方法中對核心組件(coreComponents)進行了初始化,這里就包括了'view' => ['class' => 'yii\web\View']
,組件的初始化就是這些配置信息保存的過程,主要通過yii\base\Object
中的構造方法,調用了Yii::configure($config)
,configure的過程就是對$_definitions $_components
賦值的過程。
最終經過getView()
,我們得到的就是一個yii\web\View
object
2、渲染視圖文件
進入到yii\web\View
中的render方法,位於其父類。
//yii\base\View
public function render($view, $params = [], $context = null)
{
$viewFile = $this->findViewFile($view, $context);
return $this->renderFile($viewFile, $params, $context);
}
通過代碼可以看到,渲染視圖文件的第一步就是找到這個視圖文件在磁盤中的文件存儲位置(真實路徑)。從findViewFile方法尋找路徑的過程,也可看出我們的$view
參數支持的幾種格式。
2.1、$view
參數格式
源碼不放了,大家對照yii\base\View
中的function findViewFile,或看這里
- 按照alias別名的方式
檢查$view
中是否包含“@”,如果有說明是使用了別名的方法,直接通過Yii::getAlias($view)
獲取,因為形式是"@backend/views/site/index"這種格式,而在config文件夾下的bootstrap.php中已經對backend進行了setAlias,所以通過getAias簡單處理就可返回真實路徑了 - 以"//"開頭的
$view
,如"//site/index"
匹配到這種形式執行Yii::$app->getViewPath()
,getViewPath方法事實上是:獲取我們在配置文件中定義的basePath,再根據Yii2.0中約定的目錄結構,加上"/views"返回 - 以"/"開頭
相當於絕對路徑的方式,在當前控制器所在的module中進行尋找,這個controller對象的module成員保存的是當前控制器所在的module對象,這個保存是在createControllerByID中調用Yii::createObject()的時候進行賦值的。yii\base\Controller
中的構造方法有一句:$this->module = $module;
- controller對象調用View進行視圖渲染時,將對象自身傳遞給了view,也就是$context,如果這個控制器實現了ViewContextInterface接口(接口中就定義了一個getViewPath方法),那么直接調用控制器的getViewPath
- 最后一種情況就是根據當前視圖文件的文件路徑來返回視圖路徑(這種應該是在視圖文件中渲染其他視圖的情況)
【注】2與3其實都是調用
yii\base\Module
中的getViewPath,getViewPath中調用了getBasePath,區別在於2中用的是Yii::$app 這個對象,Application對象初始化的時候在preInit中已經對將配置文件中的basePath初始化並保存在了$_basePath
中;3中getViewPath調用的發起者是controller實例所屬的module對象,module對象創建的時候並未初始化$_basePath
,所以可以看到getBasePaht中是使用反射的方法獲取當前module對象對應文件的路徑,進而返回basePath的
2.2、renderFile
renderFile的操作就比較簡單了,根據視圖文件的后綴來判斷是否啟用了相應的模板引擎,如Smarty、Twig等,如果都沒有那就默認的php文件的方式(renderPhpFile)進行渲染,renderPhpFile的代碼非常簡單:
public function renderPhpFile($_file_, $_params_ = [])
{
ob_start();
ob_implicit_flush(false);
//將我們render的參數數組extract為本地變量
extract($_params_, EXTR_OVERWRITE);
//require 我們的視圖文件
require($_file_);
//清空並返回當前緩沖區的內容
return ob_get_clean();
}
經過以上的分析,一個視圖文件的渲染就完成了,而我們的Yii框架中大多使用了布局文件。所以回到yii\base\Controller
的render方法中,視圖文件渲染之后返回的是一個字符串,保存在了$content中,然后執行了return $this->renderContent($content)
,這其實就是把視圖文件渲染后的結果放到布局中,這也是為什么我們的布局文件中會有這么一行代碼:<?=$content?>
。renderContent其實就是獲取布局文件的路徑然后在調用View中的renderFile方法,過程跟視圖渲染的過程一樣。
至此,Yii2.0視圖渲染過程分析完畢。
【附】render渲染代碼調用流程