2 關注者

視圖

視圖是 MVC 架構的一部分。它們是負責將資料呈現給終端使用者的程式碼。在 Web 應用程式中,視圖通常以視圖模板的形式建立,這些模板是主要包含 HTML 程式碼和呈現用 PHP 程式碼的 PHP 腳本檔案。它們由 視圖 應用程式元件 管理,該元件提供常用方法來簡化視圖的組合和渲染。為了簡潔起見,我們通常將視圖模板或視圖模板檔案稱為視圖。

建立視圖

如前所述,視圖只是一個混合了 HTML 和 PHP 程式碼的 PHP 腳本。以下是一個呈現登入表單的視圖。如你所見,PHP 程式碼用於生成動態內容,例如頁面標題和表單,而 HTML 程式碼將它們組織成一個可呈現的 HTML 頁面。

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $form yii\widgets\ActiveForm */
/* @var $model app\models\LoginForm */

$this->title = 'Login';
?>
<h1><?= Html::encode($this->title) ?></h1>

<p>Please fill out the following fields to login:</p>

<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'username') ?>
    <?= $form->field($model, 'password')->passwordInput() ?>
    <?= Html::submitButton('Login') ?>
<?php ActiveForm::end(); ?>

在視圖中,你可以存取 $this,它指向管理和渲染此視圖模板的 視圖元件

除了 $this 之外,視圖中可能還有其他預定義的變數,例如上述範例中的 $model。這些變數代表由 控制器 或其他觸發 視圖渲染 的物件*推入*視圖的資料。

提示:預定義的變數列在視圖開頭的註解區塊中,以便 IDE 可以識別它們。這也是記錄視圖的好方法。

安全性

當建立生成 HTML 頁面的視圖時,重要的是在呈現使用者資料之前,先對來自終端使用者的資料進行編碼和/或過濾。否則,你的應用程式可能會遭受 跨網站腳本 攻擊。

若要顯示純文字,請先呼叫 yii\helpers\Html::encode() 對其進行編碼。例如,以下程式碼在顯示使用者名稱之前對其進行編碼

<?php
use yii\helpers\Html;
?>

<div class="username">
    <?= Html::encode($user->name) ?>
</div>

若要顯示 HTML 內容,請先使用 yii\helpers\HtmlPurifier 過濾內容。例如,以下程式碼在顯示文章內容之前對其進行過濾

<?php
use yii\helpers\HtmlPurifier;
?>

<div class="post">
    <?= HtmlPurifier::process($post->text) ?>
</div>

提示:雖然 HTMLPurifier 在確保輸出安全方面做得很好,但它速度不快。如果你的應用程式需要高效能,你應該考慮 快取 過濾結果。

組織視圖

就像 控制器模型 一樣,組織視圖也有一些慣例。

  • 對於由控制器渲染的視圖,預設情況下它們應放置在 @app/views/ControllerID 目錄下,其中 ControllerID 指的是 控制器 ID。例如,如果控制器類別是 PostController,則目錄將為 @app/views/post;如果是 PostCommentController,則目錄將為 @app/views/post-comment。如果控制器屬於模組,則目錄將是 模組目錄 下的 views/ControllerID
  • 對於在 小工具 中渲染的視圖,預設情況下它們應放置在 WidgetPath/views 目錄下,其中 WidgetPath 代表包含小工具類別檔案的目錄。
  • 對於由其他物件渲染的視圖,建議你遵循與小工具類似的慣例。

你可以透過覆寫控制器或小工具的 yii\base\ViewContextInterface::getViewPath() 方法來自訂這些預設視圖目錄。

渲染視圖

你可以在 控制器小工具 或任何其他地方透過呼叫視圖渲染方法來渲染視圖。這些方法共享一個類似的簽名,如下所示:

/**
 * @param string $view view name or file path, depending on the actual rendering method
 * @param array $params the data to be passed to the view
 * @return string rendering result
 */
methodName($view, $params = [])

在控制器中渲染

控制器 中,你可以呼叫以下控制器方法來渲染視圖:

例如:

namespace app\controllers;

use Yii;
use app\models\Post;
use yii\web\Controller;
use yii\web\NotFoundHttpException;

class PostController extends Controller
{
    public function actionView($id)
    {
        $model = Post::findOne($id);
        if ($model === null) {
            throw new NotFoundHttpException;
        }

        // renders a view named "view" and applies a layout to it
        return $this->render('view', [
            'model' => $model,
        ]);
    }
}

在小工具中渲染

小工具 中,你可以呼叫以下小工具方法來渲染視圖。

例如:

namespace app\components;

use yii\base\Widget;
use yii\helpers\Html;

class ListWidget extends Widget
{
    public $items = [];

    public function run()
    {
        // renders a view named "list"
        return $this->render('list', [
            'items' => $this->items,
        ]);
    }
}

在視圖中渲染

你可以在另一個視圖中渲染一個視圖,方法是呼叫 視圖元件 提供的以下方法之一:

例如,視圖中的以下程式碼渲染了 _overview.php 視圖檔案,該檔案與目前正在渲染的視圖位於同一個目錄中。請記住,視圖中的 $this 指的是 視圖 元件。

<?= $this->render('_overview') ?>

在其他地方渲染

在任何地方,你都可以透過表達式 Yii::$app->view 存取 視圖 應用程式元件,然後呼叫其上述方法來渲染視圖。

// displays the view file "@app/views/site/license.php"
echo \Yii::$app->view->renderFile('@app/views/site/license.php');

具名視圖

當你渲染視圖時,你可以使用視圖名稱或視圖檔案路徑/別名來指定視圖。在大多數情況下,你會使用前者,因為它更簡潔且更具彈性。我們將使用名稱指定的視圖稱為具名視圖

視圖名稱會根據以下規則解析為對應的視圖檔案路徑:

  • 視圖名稱可以省略檔案副檔名。在這種情況下,.php 將用作副檔名。例如,視圖名稱 about 對應於檔案名稱 about.php
  • 如果視圖名稱以雙斜線 // 開頭,則對應的視圖檔案路徑將為 @app/views/ViewName。也就是說,視圖會在應用程式的 視圖路徑 下尋找。例如,//site/about 將解析為 @app/views/site/about.php
  • 如果視圖名稱以單斜線 / 開頭,則視圖檔案路徑是透過在視圖名稱前加上目前活動 模組視圖路徑 來形成的。如果沒有活動模組,則將使用 @app/views/ViewName。例如,如果目前活動的模組是 user,則 /user/create 將解析為 @app/modules/user/views/user/create.php。如果沒有活動模組,則視圖檔案路徑將為 @app/views/user/create.php
  • 如果視圖是使用 上下文 渲染的,並且上下文實作了 yii\base\ViewContextInterface,則視圖檔案路徑是透過在視圖名稱前加上上下文的 視圖路徑 來形成的。這主要適用於在控制器和小工具中渲染的視圖。例如,如果上下文是控制器 SiteController,則 about 將解析為 @app/views/site/about.php
  • 如果視圖是在另一個視圖中渲染的,則包含另一個視圖檔案的目錄將加在新的視圖名稱之前,以形成實際的視圖檔案路徑。例如,如果 item 是在視圖 @app/views/post/index.php 中渲染的,則 item 將解析為 @app/views/post/item.php

根據上述規則,在控制器 app\controllers\PostController 中呼叫 $this->render('view') 實際上會渲染視圖檔案 @app/views/post/view.php,而在該視圖中呼叫 $this->render('_overview') 將渲染視圖檔案 @app/views/post/_overview.php

在視圖中存取資料

有兩種在視圖中存取資料的方法:推入和拉取。

透過將資料作為第二個參數傳遞給視圖渲染方法,你正在使用推入方法。資料應表示為名稱-值對的陣列。當視圖正在渲染時,將對此陣列呼叫 PHP extract() 函數,以便將陣列提取到視圖中的變數中。例如,控制器中的以下視圖渲染程式碼將把兩個變數推入到 report 視圖:$foo = 1$bar = 2

echo $this->render('report', [
    'foo' => 1,
    'bar' => 2,
]);

拉取方法主動從 視圖元件 或視圖中可存取的其他物件 (例如 Yii::$app) 檢索資料。以下面的程式碼為例,在視圖中,你可以透過表達式 $this->context 取得控制器物件。因此,你可以在 report 視圖中存取控制器的任何屬性或方法,例如以下顯示的控制器 ID。

The controller ID is: <?= $this->context->id ?>

推入方法通常是在視圖中存取資料的首選方法,因為它使視圖較不依賴上下文物件。其缺點是你需要一直手動建立資料陣列,如果視圖在不同地方共享和渲染,這可能會變得乏味且容易出錯。

在視圖之間共享資料

視圖元件 提供了 params 屬性,你可以使用它在視圖之間共享資料。

例如,在 about 視圖中,你可以使用以下程式碼來指定麵包屑的目前區段。

$this->params['breadcrumbs'][] = 'About Us';

然後,在 版型 檔案(也是一個視圖)中,你可以使用沿著 params 傳遞的資料來顯示麵包屑。

<?= yii\widgets\Breadcrumbs::widget([
    'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
]) ?>

版型

版型是一種特殊類型的視圖,它代表多個視圖的共同部分。例如,大多數 Web 應用程式的頁面共享相同的頁首和頁尾。雖然你可以在每個視圖中重複相同的頁首和頁尾,但更好的方法是在版型中執行一次,並將內容視圖的渲染結果嵌入到版型中的適當位置。

建立版型

因為版型也是視圖,所以它們可以以與普通視圖類似的方式建立。預設情況下,版型儲存在 @app/views/layouts 目錄中。對於在 模組 中使用的版型,它們應儲存在 模組目錄 下的 views/layouts 目錄中。你可以透過設定應用程式或模組的 yii\base\Module::$layoutPath 屬性來自訂預設版型目錄。

以下範例顯示了版型的外觀。請注意,為了說明目的,我們大大簡化了版型中的程式碼。實際上,你可能想要在其中新增更多內容,例如 head 標籤、主選單等。

<?php
use yii\helpers\Html;

/* @var $this yii\web\View */
/* @var $content string */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <?= Html::csrfMetaTags() ?>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
    <header>My Company</header>
    <?= $content ?>
    <footer>&copy; 2014 by My Company</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

如你所見,版型生成了所有頁面通用的 HTML 標籤。在 <body> 區段中,版型輸出了 $content 變數,它代表內容視圖的渲染結果,並在呼叫 yii\base\Controller::render() 時推入版型中。

大多數版型都應呼叫以下方法,如上述程式碼所示。這些方法主要觸發有關渲染過程的事件,以便在其他地方註冊的腳本和標籤可以正確地注入到呼叫這些方法的位置。

  • beginPage():此方法應在版型的最開頭呼叫。它觸發 EVENT_BEGIN_PAGE 事件,表示頁面開始。
  • endPage():此方法應在版型的結尾呼叫。它觸發 EVENT_END_PAGE 事件,表示頁面結束。
  • head():此方法應在 HTML 頁面的 <head> 區段中呼叫。它生成一個佔位符,當頁面完成渲染時,該佔位符將被替換為已註冊的 head HTML 程式碼(例如 link 標籤、meta 標籤)。
  • beginBody():此方法應在 <body> 區段的開頭呼叫。它觸發 EVENT_BEGIN_BODY 事件,並生成一個佔位符,該佔位符將被替換為針對 body 開頭位置註冊的 HTML 程式碼(例如 JavaScript)。
  • endBody():此方法應在 <body> 區段的結尾呼叫。它觸發 EVENT_END_BODY 事件,並生成一個佔位符,該佔位符將被替換為針對 body 結尾位置註冊的 HTML 程式碼(例如 JavaScript)。

在版型中存取資料

在版型中,你可以存取兩個預定義的變數:$this$content。前者指的是 視圖元件,就像在普通視圖中一樣,而後者包含內容視圖的渲染結果,該結果是透過在控制器中呼叫 render() 方法來渲染的。

如果你想要在版型中存取其他資料,你必須使用在「在視圖中存取資料」小節中描述的拉取方法。如果你想要將資料從內容視圖傳遞到版型,你可以使用在「在視圖之間共享資料」小節中描述的方法。

使用版型

如「在控制器中渲染」小節中所述,當你透過在控制器中呼叫 render() 方法來渲染視圖時,版型將應用於渲染結果。預設情況下,將使用版型 @app/views/layouts/main.php

你可以透過設定 yii\base\Application::$layoutyii\base\Controller::$layout 來使用不同的版型。前者控制所有控制器使用的版型,而後者覆寫個別控制器的前者。例如,以下程式碼使 post 控制器在渲染其視圖時使用 @app/views/layouts/post.php 作為版型。其他控制器,假設它們的 layout 屬性未被觸及,仍將使用預設的 @app/views/layouts/main.php 作為版型。

namespace app\controllers;

use yii\web\Controller;

class PostController extends Controller
{
    public $layout = 'post';
    
    // ...
}

對於屬於模組的控制器,你也可以設定模組的 layout 屬性,以便為這些控制器使用特定的版型。

因為 layout 屬性可以在不同層級(控制器、模組、應用程式)設定,所以在幕後 Yii 採取兩個步驟來決定特定控制器正在使用的實際版型檔案。

在第一步中,它確定版型值和上下文模組:

  • 如果控制器的 yii\base\Controller::$layout 屬性不是 null,則將其用作版型值,並將控制器的 模組 用作上下文模組。
  • 如果控制器的 yii\base\Controller::$layout 屬性為 null,則搜尋控制器的所有祖先模組(包括應用程式本身),並找到第一個 layout 屬性不是 null 的模組。使用該模組及其 layout 值作為上下文模組和選定的版型值。如果找不到這樣的模組,則表示不會套用任何版型。

在第二步中,它根據在第一步中確定的版型值和上下文模組來確定實際的版型檔案。版型值可以是:

  • 路徑別名(例如 @app/views/layouts/main)。
  • 絕對路徑(例如 /main):版型值以斜線開頭。實際的版型檔案將在應用程式的 版型路徑 下尋找,預設值為 @app/views/layouts
  • 相對路徑(例如 main):實際的版型檔案將在上下文模組的版型路徑下尋找,預設值為模組目錄下的 views/layouts 目錄。
  • 布林值 false:不會套用任何版型。

如果版型值不包含檔案副檔名,則將使用預設的 .php

巢狀版型

有時候您可能會想要將一個版型(layout)嵌套在另一個版型中。例如,在網站的不同區塊,您想要使用不同的版型,但所有這些版型都共用產生整體 HTML5 頁面結構的相同基本版型。您可以透過在子版型中呼叫 beginContent()endContent() 來達成此目標,如下所示

<?php $this->beginContent('@app/views/layouts/base.php'); ?>

...child layout content here...

<?php $this->endContent(); ?>

如上所示,子版型的內容應該被包含在 beginContent()endContent() 之間。傳遞給 beginContent() 的參數指定了父版型是什麼。它可以是一個版型檔案或別名。

使用上述方法,您可以將版型嵌套超過一個層級。

使用區塊(Blocks)

區塊允許您在一個地方指定視圖內容,同時在另一個地方顯示它。它們經常與版型一起使用。例如,您可以在內容視圖中定義一個區塊,並在版型中顯示它。

您呼叫 beginBlock()endBlock() 來定義一個區塊。然後可以透過 $view->blocks[$blockID] 存取該區塊,其中 $blockID 代表您在定義區塊時為其指定的唯一 ID。

以下範例示範如何在內容視圖中使用區塊來自訂版型中的特定部分。

首先,在內容視圖中,定義一個或多個區塊

...

<?php $this->beginBlock('block1'); ?>

...content of block1...

<?php $this->endBlock(); ?>

...

<?php $this->beginBlock('block3'); ?>

...content of block3...

<?php $this->endBlock(); ?>

然後,在版型視圖中,如果區塊可用則渲染區塊,如果未定義區塊則顯示一些預設內容。

...
<?php if (isset($this->blocks['block1'])): ?>
    <?= $this->blocks['block1'] ?>
<?php else: ?>
    ... default content for block1 ...
<?php endif; ?>

...

<?php if (isset($this->blocks['block2'])): ?>
    <?= $this->blocks['block2'] ?>
<?php else: ?>
    ... default content for block2 ...
<?php endif; ?>

...

<?php if (isset($this->blocks['block3'])): ?>
    <?= $this->blocks['block3'] ?>
<?php else: ?>
    ... default content for block3 ...
<?php endif; ?>
...

使用視圖元件(View Components)

視圖元件提供了許多與視圖相關的功能。雖然您可以透過建立 yii\base\View 或其子類別的個別實例來取得視圖元件,但在大多數情況下,您主要會使用 view 應用程式元件。您可以在 應用程式配置中配置此元件,如下所示

[
    // ...
    'components' => [
        'view' => [
            'class' => 'app\components\View',
        ],
        // ...
    ],
]

視圖元件提供以下有用的視圖相關功能,每個功能都在單獨的章節中詳細說明

當您開發網頁時,您也可能會經常使用以下較小但有用的功能。

設定頁面標題(Setting Page Titles)

每個網頁都應該有一個標題。通常標題標籤顯示在版型中。然而,實際上標題通常在內容視圖而不是版型中決定。為了解決這個問題,yii\web\View 提供了 title 屬性,讓您可以將標題資訊從內容視圖傳遞到版型。

為了使用此功能,在每個內容視圖中,您可以像下面這樣設定頁面標題

<?php
$this->title = 'My page title';
?>

然後在版型中,請確保您在 <head> 區段中有以下程式碼

<title><?= Html::encode($this->title) ?></title>

註冊 Meta 標籤(Registering Meta Tags)

網頁通常需要產生不同方需要的各種 meta 標籤。與頁面標題類似,meta 標籤出現在 <head> 區段中,通常在版型中產生。

如果您想要指定在內容視圖中產生哪些 meta 標籤,您可以像下面這樣在內容視圖中呼叫 yii\web\View::registerMetaTag()

<?php
$this->registerMetaTag(['name' => 'keywords', 'content' => 'yii, framework, php']);
?>

上面的程式碼將向視圖元件註冊一個 "keywords" meta 標籤。註冊的 meta 標籤會在版型完成渲染後渲染。以下 HTML 程式碼將被產生並插入到您在版型中呼叫 yii\web\View::head() 的位置

<meta name="keywords" content="yii, framework, php">

請注意,如果您多次呼叫 yii\web\View::registerMetaTag(),它將註冊多個 meta 標籤,無論 meta 標籤是否相同。

為了確保只有一個 meta 標籤類型的實例,您可以在呼叫該方法時指定一個鍵作為第二個參數。例如,以下程式碼註冊了兩個 "description" meta 標籤。但是,只會渲染第二個。

$this->registerMetaTag(['name' => 'description', 'content' => 'This is my cool website made with Yii!'], 'description');
$this->registerMetaTag(['name' => 'description', 'content' => 'This website is about funny raccoons.'], 'description');

meta 標籤類似,link 標籤在許多情況下都很有用,例如自訂 favicon、指向 RSS 訂閱或將 OpenID 委派給另一個伺服器。您可以像處理 meta 標籤一樣使用 link 標籤,方法是使用 yii\web\View::registerLinkTag()。例如,在內容視圖中,您可以像下面這樣註冊一個 link 標籤,

$this->registerLinkTag([
    'title' => 'Live News for Yii',
    'rel' => 'alternate',
    'type' => 'application/rss+xml',
    'href' => 'https://yii.dev.org.tw/rss.xml/',
]);

上面的程式碼將產生

<link title="Live News for Yii" rel="alternate" type="application/rss+xml" href="https://yii.dev.org.tw/rss.xml/">

registerMetaTag() 類似,您可以在呼叫 registerLinkTag() 時指定一個鍵,以避免產生重複的 link 標籤。

視圖事件(View Events)

視圖元件在視圖渲染過程中觸發多個事件。您可以回應這些事件,將內容注入到視圖中,或在將渲染結果發送給最終使用者之前處理渲染結果。

例如,以下程式碼在頁面主體的末尾注入目前的日期

\Yii::$app->view->on(View::EVENT_END_BODY, function () {
    echo date('Y-m-d');
});

渲染靜態頁面(Rendering Static Pages)

靜態頁面指的是那些主要內容大多是靜態的網頁,不需要存取從控制器推送的動態資料。

您可以透過將靜態頁面的程式碼放在視圖中來輸出靜態頁面,然後在控制器中使用如下程式碼

public function actionAbout()
{
    return $this->render('about');
}

如果網站包含許多靜態頁面,則重複類似的程式碼會非常繁瑣。為了解決這個問題,您可以在控制器中引入一個名為 yii\web\ViewAction獨立動作(standalone action)。例如,

namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'page' => [
                'class' => 'yii\web\ViewAction',
            ],
        ];
    }
}

現在,如果您在目錄 @app/views/site/pages 下建立一個名為 about 的視圖,您將能夠透過以下 URL 顯示此視圖

http://127.0.0.1/index.php?r=site%2Fpage&view=about

GET 參數 view 告訴 yii\web\ViewAction 請求哪個視圖。然後,該動作將在目錄 @app/views/site/pages 下尋找此視圖。您可以配置 yii\web\ViewAction::$viewPrefix 來變更搜尋這些視圖的目錄。

最佳實務(Best Practices)

視圖負責以最終使用者期望的格式呈現模型。一般來說,視圖

  • 應主要包含呈現程式碼,例如 HTML,以及簡單的 PHP 程式碼來遍歷、格式化和渲染資料。
  • 不應包含執行資料庫查詢的程式碼。此類程式碼應在模型中完成。
  • 應避免直接存取請求資料,例如 $_GET$_POST。這屬於控制器。如果需要請求資料,則應由控制器將其推送至視圖。
  • 可以讀取模型屬性,但不應修改它們。

為了使視圖更易於管理,請避免建立過於複雜或包含過多冗餘程式碼的視圖。您可以使用以下技術來達成此目標

  • 使用 版型 來表示常見的呈現區段(例如頁首、頁尾)。
  • 將複雜的視圖劃分為幾個較小的視圖。可以使用我們描述的渲染方法來渲染較小的視圖並將它們組裝成較大的視圖。
  • 建立和使用 小工具(widgets) 作為視圖的建構區塊。
  • 建立和使用輔助類別來轉換和格式化視圖中的資料。

發現錯字或您認為此頁面需要改進嗎?
在 github 上編輯 !